From 2a6ee28aa0095a4afa691f0cc514d2671cff7437 Mon Sep 17 00:00:00 2001
From: Grollicus <cccvgitlab.db5c7b60@grollmann.eu>
Date: Wed, 25 Dec 2024 15:37:41 +0100
Subject: [PATCH] added max lock duration after which a lock won't be extended
 anymore

---
 src/core/migrations/0173_lock_created_at.py | 20 ++++++++++++++++++++
 src/core/models/locks.py                    |  1 +
 src/hub/settings/base.py                    |  2 ++
 src/plainui/tests/test_views.py             |  9 ++++++++-
 src/plainui/views/static_pages.py           |  2 ++
 5 files changed, 33 insertions(+), 1 deletion(-)
 create mode 100644 src/core/migrations/0173_lock_created_at.py

diff --git a/src/core/migrations/0173_lock_created_at.py b/src/core/migrations/0173_lock_created_at.py
new file mode 100644
index 000000000..67fb78444
--- /dev/null
+++ b/src/core/migrations/0173_lock_created_at.py
@@ -0,0 +1,20 @@
+# Generated by Django 5.1.3 on 2024-12-25 14:36
+
+import django.utils.timezone
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('core', '0172_assembly_location_state'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='lock',
+            name='created_at',
+            field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
+            preserve_default=False,
+        ),
+    ]
diff --git a/src/core/models/locks.py b/src/core/models/locks.py
index 2815cacdd..51673fcf0 100644
--- a/src/core/models/locks.py
+++ b/src/core/models/locks.py
@@ -13,6 +13,7 @@ class Lock(models.Model):
     content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
     object_id = models.UUIDField()
     content_object = GenericForeignKey('content_type', 'object_id')
+    created_at = models.DateTimeField(auto_now_add=True)
     timeout = models.DateTimeField()
     lock_holder = models.ForeignKey(PlatformUser, on_delete=models.CASCADE, related_name='locks')
 
diff --git a/src/hub/settings/base.py b/src/hub/settings/base.py
index c93dee613..ddfc524ff 100644
--- a/src/hub/settings/base.py
+++ b/src/hub/settings/base.py
@@ -616,6 +616,8 @@ _handle_hostpattern_list(DEREFERRER_COUNT_ACCESS, env('PLAINUI_DEREFERER_COUNTLI
 STATIC_PAGE_LOCALIZED_BY_DEFAULT = env.bool('STATICPAGES_LOCALIZED_BY_DEFAULT', False)
 # Time after which a non-refreshed static page lock will expire (in seconds)
 STATIC_PAGE_LOCK_TIMEOUT = env.int('STATICPAGES_LOCK_TIMEOUT', 5 * 60)
+# Time after a which a lock will no longer be extended and will therefore expire (in seconds)
+STATIC_PAGE_LOCK_MAX_DURATION = env.int('STATIC_PAGE_LOCK_MAX_DURATION', 30 * 60)
 
 PLAINUI_THEME_SUPPORT = env('PLAINUI_THEME_SUPPORT')
 
diff --git a/src/plainui/tests/test_views.py b/src/plainui/tests/test_views.py
index 8873c9291..373612288 100644
--- a/src/plainui/tests/test_views.py
+++ b/src/plainui/tests/test_views.py
@@ -1013,7 +1013,7 @@ class ViewsTest(ViewsTestBase):
             lock.refresh_from_db()
             self.assertEqual(lock.timeout, lock_timeout)
 
-    @override_settings(RATELIMIT_ENABLE=False)
+    @override_settings(RATELIMIT_ENABLE=False, STATIC_PAGE_LOCK_MAX_DURATION=60)
     def test_StaticPageLockKeepalive(self):
         user2 = PlatformUser(username='user2')
         user2.save()
@@ -1040,6 +1040,13 @@ class ViewsTest(ViewsTestBase):
         self.assertGreater(lock.timeout, lock_timeout)
         lock_timeout = lock.timeout
 
+        # does not refresh lock after max duration has expired
+        with freeze_time(lock.created_at + timedelta(seconds=61)):
+            resp = self.client.post(reverse('plainui:static_page_refresh_lock'), {'page_slug': sp.slug, 'lock_id': str(lock.pk)})
+        self.assertEqual(resp.status_code, 200)
+        lock.refresh_from_db()
+        self.assertEqual(lock.timeout, lock_timeout)
+
         # does not refresh lock when called for nonexistent page
         resp = self.client.post(reverse('plainui:static_page_refresh_lock'), {'page_slug': 'something_else', 'lock_id': str(lock.pk)})
         self.assertEqual(resp.status_code, 200)
diff --git a/src/plainui/views/static_pages.py b/src/plainui/views/static_pages.py
index a4da70392..da24b7c84 100644
--- a/src/plainui/views/static_pages.py
+++ b/src/plainui/views/static_pages.py
@@ -444,6 +444,7 @@ class StaticPageGlobalHistoryView(ConferenceRequiredMixin, TemplateView):
 class StaticPageLockKeepalive(ConferenceRequiredMixin, View):
     def post(self, request):
         LOCK_TIMEOUT = settings.STATIC_PAGE_LOCK_TIMEOUT
+        LOCK_MAX_TIMEOUT = settings.STATIC_PAGE_LOCK_MAX_DURATION
 
         page_slug = request.POST['page_slug']
         lock_id = request.POST['lock_id']
@@ -458,6 +459,7 @@ class StaticPageLockKeepalive(ConferenceRequiredMixin, View):
             content_type=ContentType.objects.get_for_model(StaticPage),
             object_id=static_page.pk,
             lock_holder=self.request.user,
+            created_at__gte=self.now - timedelta(seconds=LOCK_MAX_TIMEOUT),
         ).update(timeout=self.now + timedelta(seconds=LOCK_TIMEOUT))
 
         return HttpResponse('')
-- 
GitLab