From 121a81509a80972f5965877cc7e39efdd6ffd613 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Sat, 11 Oct 2025 11:06:09 +0200 Subject: [PATCH 1/7] Add duplicate number fields --- .../0007_page_schedule_duplicates.py | 20 +++++++++++++++++ wafer/pages/models.py | 6 +++++ .../0025_talk_schedule_duplicates.py | 22 +++++++++++++++++++ wafer/talks/models.py | 3 +++ 4 files changed, 51 insertions(+) create mode 100644 wafer/pages/migrations/0007_page_schedule_duplicates.py create mode 100644 wafer/talks/migrations/0025_talk_schedule_duplicates.py diff --git a/wafer/pages/migrations/0007_page_schedule_duplicates.py b/wafer/pages/migrations/0007_page_schedule_duplicates.py new file mode 100644 index 00000000..0fa57536 --- /dev/null +++ b/wafer/pages/migrations/0007_page_schedule_duplicates.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.3 on 2025-10-11 09:07 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("pages", "0006_menu_order"), + ] + + operations = [ + migrations.AddField( + model_name="page", + name="schedule_duplicates", + field=models.IntegerField( + default=0, validators=[django.core.validators.MinValueValidator(0)] + ), + ), + ] diff --git a/wafer/pages/models.py b/wafer/pages/models.py index 88487c1a..1ce64e31 100644 --- a/wafer/pages/models.py +++ b/wafer/pages/models.py @@ -2,6 +2,7 @@ import logging from django.conf import settings +from django.core import validators from django.core.cache import caches from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.db import models @@ -81,6 +82,11 @@ class Page(models.Model): "is not dynamic and it will be not be regenerated " "until it is next edited.")) + # By default, Pages aren't allowed in the schedule + # this helps keep the list in the schedule editor sane + schedule_duplicates = models.IntegerField( + default=0, validators=[validators.MinValueValidator(0)]) + def __str__(self): return u'%s' % (self.name,) diff --git a/wafer/talks/migrations/0025_talk_schedule_duplicates.py b/wafer/talks/migrations/0025_talk_schedule_duplicates.py new file mode 100644 index 00000000..ce15a551 --- /dev/null +++ b/wafer/talks/migrations/0025_talk_schedule_duplicates.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2025-10-11 09:07 + +import django.core.validators +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("talks", "0024_alter_review_options"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="talk", + name="schedule_duplicates", + field=models.IntegerField( + default=1, validators=[django.core.validators.MinValueValidator(0)] + ), + ), + ] diff --git a/wafer/talks/models.py b/wafer/talks/models.py index d78300c7..69710d12 100644 --- a/wafer/talks/models.py +++ b/wafer/talks/models.py @@ -246,6 +246,9 @@ class Meta: submission_time = models.DateTimeField(auto_now_add=True) + schedule_duplicates = models.IntegerField( + default=1, validators=[validators.MinValueValidator(0)]) + @property def slug(self): return slugify(self.title) From 790a03b616eabb422839d1ab91ede26da630f20d Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Sat, 11 Oct 2025 11:24:58 +0200 Subject: [PATCH 2/7] Rework duplicate check --- wafer/schedule/admin.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/wafer/schedule/admin.py b/wafer/schedule/admin.py index 56b3e002..c1e3acd1 100644 --- a/wafer/schedule/admin.py +++ b/wafer/schedule/admin.py @@ -57,17 +57,19 @@ def validate_items(all_items): def find_duplicate_schedule_items(all_items): """Find talks / pages assigned to mulitple schedule items""" duplicates = [] - seen_talks = {} + occurances = {} for item in all_items: - if item.talk and item.talk in seen_talks: - duplicates.append(item) - if seen_talks[item.talk] not in duplicates: - duplicates.append(seen_talks[item.talk]) + if item.talk: + key = item.talk + else: + key = item.page + if key in occurances: + occurances[key].append(item) else: - seen_talks[item.talk] = item - # We currently allow duplicate pages for cases were we need disjoint - # schedule items, like multiple open space sessions on different - # days and similar cases. This may be revisited later + occurances[key] = [item] + for cand in occurances: + if len(occurances[cand]) > cand.schedule_duplicates: + duplicates.extend(occurances[cand]) return duplicates @@ -166,7 +168,7 @@ def register_schedule_item_validator(function, err_type, msg): _('Clashes found in schedule.')) register_schedule_item_validator( find_duplicate_schedule_items, 'duplicates', - _('Duplicate schedule items found in schedule.')) + _('Schedule items found in schedule which exceed allowed number of duplicates.')) register_schedule_item_validator( validate_items, 'validation', _('Invalid schedule items found in schedule.')) From 92c15e7782fb3d74f2c967deaad9c132779adcbd Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Sat, 11 Oct 2025 11:49:09 +0200 Subject: [PATCH 3/7] Skip null items in duplicate checks --- wafer/schedule/admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wafer/schedule/admin.py b/wafer/schedule/admin.py index c1e3acd1..6ccaefb3 100644 --- a/wafer/schedule/admin.py +++ b/wafer/schedule/admin.py @@ -61,8 +61,10 @@ def find_duplicate_schedule_items(all_items): for item in all_items: if item.talk: key = item.talk - else: + elif item.page: key = item.page + else: + continue if key in occurances: occurances[key].append(item) else: From b7003cd06cd8bf9134f4d08f201cd9b9a347eb78 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Sat, 11 Oct 2025 11:16:07 +0200 Subject: [PATCH 4/7] Set appropriate duplicate value in tests --- wafer/schedule/tests/test_views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wafer/schedule/tests/test_views.py b/wafer/schedule/tests/test_views.py index 49cee54f..613760ee 100644 --- a/wafer/schedule/tests/test_views.py +++ b/wafer/schedule/tests/test_views.py @@ -23,7 +23,8 @@ def make_pages(n): pages = [] for x in range(n): page = Page.objects.create(name="test page %s" % x, - slug="test%s" % x) + slug="test%s" % x, + schedule_duplicates=1) pages.append(page) return pages From a9f3c8eac61f827ec5f46fad65b59031195caf88 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Sat, 11 Oct 2025 11:49:33 +0200 Subject: [PATCH 5/7] Update tests to use new field --- wafer/schedule/tests/test_admin.py | 15 ++++++++------- wafer/schedule/tests/test_validation.py | 4 +++- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/wafer/schedule/tests/test_admin.py b/wafer/schedule/tests/test_admin.py index 6570f925..c1a0cb21 100644 --- a/wafer/schedule/tests/test_admin.py +++ b/wafer/schedule/tests/test_admin.py @@ -493,7 +493,7 @@ def test_validation(self): slot2 = Slot.objects.create(start_time=start1, end_time=end) talk = create_talk('Test talk', status=ACCEPTED, username='john') - page = Page.objects.create(name="test page", slug="test") + page = Page.objects.create(name="test page", slug="test", schedule_duplicates=1) item1 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk, @@ -568,7 +568,7 @@ def test_non_contiguous(self): slot3 = Slot.objects.create(start_time=start3, end_time=end) talk = create_talk('Test Talk', status=ACCEPTED, username='john') - page = Page.objects.create(name="test page", slug="test") + page = Page.objects.create(name="test page", slug="test", schedule_duplicates=1) item1 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk) @@ -618,8 +618,8 @@ def test_duplicates(self): slot2 = Slot.objects.create(start_time=start1, end_time=end) talk = create_talk('Test talk', status=ACCEPTED, username='john') - page1 = Page.objects.create(name="test page", slug="test") - page2 = Page.objects.create(name="test page 2", slug="test2") + page1 = Page.objects.create(name="test page", slug="test", schedule_duplicates=1) + page2 = Page.objects.create(name="test page 2", slug="test2", schedule_duplicates=1) item1 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk) @@ -674,7 +674,7 @@ def test_venues(self): slot1 = Slot.objects.create(start_time=start1, end_time=start2) - page = Page.objects.create(name="test page", slug="test") + page = Page.objects.create(name="test page", slug="test", schedule_duplicates=1) item1 = ScheduleItem.objects.create(venue=venue1, page_id=page.pk) @@ -732,7 +732,8 @@ def test_validate_schedule(self): tzinfo=D.timezone.utc)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.blocks.add(day1) - page = Page.objects.create(name="test page", slug="test") + page = Page.objects.create(name="test page", slug="test", schedule_duplicates=1) + page2 = Page.objects.create(name="test page 2", slug="test2", schedule_duplicates=1) start1 = D.datetime(2013, 9, 22, 10, 0, 0, tzinfo=D.timezone.utc) start2 = D.datetime(2013, 9, 22, 11, 0, 0, tzinfo=D.timezone.utc) @@ -756,7 +757,7 @@ def test_validate_schedule(self): errors = validate_schedule() self.assertEqual(len(errors), 2) # Fix the invalid item - item2.page_id = page.pk + item2.page_id = page2.pk item2.save() # Schedule is still invalid, but only the clash remains check_schedule.invalidate() diff --git a/wafer/schedule/tests/test_validation.py b/wafer/schedule/tests/test_validation.py index ec53c55f..1dfdcc2a 100644 --- a/wafer/schedule/tests/test_validation.py +++ b/wafer/schedule/tests/test_validation.py @@ -360,8 +360,10 @@ def test_find_duplicates(self): c = create_client('super', superuser=True) response = c.get('/schedule/api/validate/') self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.data['Validation Status']), 1) + # We have a clash and 2 duplicates reported + self.assertEqual(len(response.data['Validation Status']), 3) self.assertIn('Clashes found in schedule', response.data['Validation Status'][0]) + self.assertIn('exceed allowed number of duplicates', response.data['Validation Status'][1]) def test_find_speaker_clashes(self): """Test that speaker clashes are reported""" From e951da2786bc826851e6d652cfa3513b6e1955ac Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Sat, 11 Oct 2025 11:58:41 +0200 Subject: [PATCH 6/7] Update pot file --- wafer/locale/django.pot | 99 +++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/wafer/locale/django.pot b/wafer/locale/django.pot index ac360348..41bdea83 100644 --- a/wafer/locale/django.pot +++ b/wafer/locale/django.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-05-07 14:22+0000\n" +"POT-Creation-Date: 2025-10-11 09:58+0000\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -102,50 +102,50 @@ msgid "" "admin site." msgstr "" -#: wafer/pages/models.py:49 +#: wafer/pages/models.py:50 msgid "Last component of the page URL" msgstr "" -#: wafer/pages/models.py:53 +#: wafer/pages/models.py:54 msgid "Markdown contents for the page." msgstr "" -#: wafer/pages/models.py:55 +#: wafer/pages/models.py:56 msgid "Whether to include in menus." msgstr "" -#: wafer/pages/models.py:58 +#: wafer/pages/models.py:59 msgid "Ordering in the menu (smaller numbers come first)" msgstr "" -#: wafer/pages/models.py:63 +#: wafer/pages/models.py:64 msgid "" "Whether to exclude this page from the static version of the site (Container " "pages, etc.)" msgstr "" -#: wafer/pages/models.py:68 +#: wafer/pages/models.py:69 msgid "Images and other files for use in the content markdown field." msgstr "" -#: wafer/pages/models.py:74 +#: wafer/pages/models.py:75 msgid "" "People associated with this page for display in the schedule (Session " "chairs, panelists, etc.)" msgstr "" -#: wafer/pages/models.py:79 +#: wafer/pages/models.py:80 msgid "" "Length of time (in seconds) to cache the page for dynamic page content. A " "negative value means this page is not dynamic and it will be not be " "regenerated until it is next edited." msgstr "" -#: wafer/pages/models.py:156 +#: wafer/pages/models.py:162 msgid "Circular reference in parent." msgstr "" -#: wafer/pages/models.py:172 +#: wafer/pages/models.py:178 msgid "Duplicate parent/slug combination." msgstr "" @@ -306,57 +306,58 @@ msgstr "" msgid "You can register after your account is activated." msgstr "" -#: wafer/schedule/admin.py:166 +#: wafer/schedule/admin.py:170 msgid "Clashes found in schedule." msgstr "" -#: wafer/schedule/admin.py:169 -msgid "Duplicate schedule items found in schedule." +#: wafer/schedule/admin.py:173 +msgid "" +"Schedule items found in schedule which exceed allowed number of duplicates." msgstr "" -#: wafer/schedule/admin.py:172 +#: wafer/schedule/admin.py:176 msgid "Invalid schedule items found in schedule." msgstr "" -#: wafer/schedule/admin.py:175 +#: wafer/schedule/admin.py:179 msgid "Non contiguous slots found in schedule." msgstr "" -#: wafer/schedule/admin.py:178 +#: wafer/schedule/admin.py:182 msgid "Invalid venues found in schedule." msgstr "" -#: wafer/schedule/admin.py:181 +#: wafer/schedule/admin.py:185 #: wafer/schedule/templates/admin/scheduleitem_list.html:61 msgid "Common speaker in simultaneous schedule items" msgstr "" -#: wafer/schedule/admin.py:221 +#: wafer/schedule/admin.py:225 msgid "Block" msgstr "" -#: wafer/schedule/admin.py:273 wafer/schedule/models.py:327 +#: wafer/schedule/admin.py:277 wafer/schedule/models.py:327 msgid "Start Time" msgstr "" -#: wafer/schedule/admin.py:329 +#: wafer/schedule/admin.py:333 msgid "Venue" msgstr "" -#: wafer/schedule/admin.py:415 +#: wafer/schedule/admin.py:419 msgid "Additional slots" msgstr "" -#: wafer/schedule/admin.py:416 +#: wafer/schedule/admin.py:420 msgid "Create this number of additional slots following this one" msgstr "" -#: wafer/schedule/admin.py:477 +#: wafer/schedule/admin.py:481 #, python-format msgid "Additional slot %(obj)s added sucessfully" msgstr "" -#: wafer/schedule/admin.py:484 +#: wafer/schedule/admin.py:488 #, python-format msgid "Failed to create new slot - %s" msgstr "" @@ -692,7 +693,7 @@ msgstr "" msgid "Mark selected talks as Accepted" msgstr "" -#: wafer/talks/forms.py:124 wafer/users/forms.py:23 wafer/users/forms.py:72 +#: wafer/talks/forms.py:124 wafer/users/forms.py:23 wafer/users/forms.py:77 msgid "Save" msgstr "" @@ -731,7 +732,7 @@ msgid "" "" msgstr "" -#: wafer/talks/models.py:70 wafer/talks/models.py:121 wafer/talks/models.py:427 +#: wafer/talks/models.py:70 wafer/talks/models.py:121 wafer/talks/models.py:438 msgid "name" msgstr "" @@ -799,7 +800,7 @@ msgstr "" msgid "tracks" msgstr "" -#: wafer/talks/models.py:151 wafer/talks/models.py:390 +#: wafer/talks/models.py:151 wafer/talks/models.py:393 msgid "talk" msgstr "" @@ -854,7 +855,7 @@ msgid "" "syntax." msgstr "" -#: wafer/talks/models.py:192 wafer/talks/models.py:397 +#: wafer/talks/models.py:192 wafer/talks/models.py:400 msgid "notes" msgstr "" @@ -900,8 +901,8 @@ msgstr "" #, python-brace-format msgid "" "By checking this, you are giving permission for the talk to be videoed, and " -"distributed by the conference, under the {license_name} license." +"distributed by the conference, under the {license_name} license." msgstr "" #: wafer/talks/models.py:235 @@ -916,70 +917,70 @@ msgid "" "who would care." msgstr "" -#: wafer/talks/models.py:269 +#: wafer/talks/models.py:272 msgid "NO CONTACT INFO" msgstr "" -#: wafer/talks/models.py:271 +#: wafer/talks/models.py:274 msgid "Contact Details" msgstr "" -#: wafer/talks/models.py:277 +#: wafer/talks/models.py:280 msgid "Corresponding Author" msgstr "" -#: wafer/talks/models.py:288 +#: wafer/talks/models.py:291 #, python-format msgid "%s, et al." msgstr "" -#: wafer/talks/models.py:295 +#: wafer/talks/models.py:298 msgid "Added to schedule" msgstr "" -#: wafer/talks/models.py:304 +#: wafer/talks/models.py:307 msgid "Has URL" msgstr "" -#: wafer/talks/models.py:393 +#: wafer/talks/models.py:396 msgid "reviewer" msgstr "" -#: wafer/talks/models.py:399 +#: wafer/talks/models.py:402 msgid "Comments on the proposal (markdown)" msgstr "" -#: wafer/talks/models.py:402 +#: wafer/talks/models.py:405 #, python-format msgid "Review of %(title)s by %(reviewer)s (%(average_score)s)" msgstr "" -#: wafer/talks/models.py:421 +#: wafer/talks/models.py:428 msgid "review" msgstr "" -#: wafer/talks/models.py:422 +#: wafer/talks/models.py:429 msgid "reviews" msgstr "" -#: wafer/talks/models.py:433 +#: wafer/talks/models.py:444 msgid "review aspect" msgstr "" -#: wafer/talks/models.py:434 +#: wafer/talks/models.py:445 msgid "review aspects" msgstr "" -#: wafer/talks/models.py:450 +#: wafer/talks/models.py:461 #, python-format msgid "Review of %(title)s by %(reviewer)s on %(aspect)s: %(score)i" msgstr "" -#: wafer/talks/models.py:458 +#: wafer/talks/models.py:469 msgid "score" msgstr "" -#: wafer/talks/models.py:459 +#: wafer/talks/models.py:470 msgid "scores" msgstr "" @@ -1310,11 +1311,11 @@ msgstr "" msgid "profiles" msgstr "" -#: wafer/users/forms.py:66 +#: wafer/users/forms.py:71 msgid "

Code Hosting Profiles

" msgstr "" -#: wafer/users/forms.py:70 +#: wafer/users/forms.py:75 msgid "

Social Profiles

" msgstr "" From 6a5344a1575ba0d117c04edf7905a2e0acccfc92 Mon Sep 17 00:00:00 2001 From: Neil Muller Date: Sat, 11 Oct 2025 13:01:38 +0200 Subject: [PATCH 7/7] Extend duplicate validation test a bit --- wafer/schedule/tests/test_validation.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/wafer/schedule/tests/test_validation.py b/wafer/schedule/tests/test_validation.py index 1dfdcc2a..8dd25589 100644 --- a/wafer/schedule/tests/test_validation.py +++ b/wafer/schedule/tests/test_validation.py @@ -364,6 +364,15 @@ def test_find_duplicates(self): self.assertEqual(len(response.data['Validation Status']), 3) self.assertIn('Clashes found in schedule', response.data['Validation Status'][0]) self.assertIn('exceed allowed number of duplicates', response.data['Validation Status'][1]) + # Check that changing the number of duplicates allowed works as expected + self.pages[0].schedule_duplicates = 2 + self.pages[0].save() + response = c.get('/schedule/api/validate/') + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data['Validation Status']), 1) + self.assertIn('Clashes found in schedule', response.data['Validation Status'][0]) + self.pages[0].schedule_duplicates = 1 + self.pages[0].save() def test_find_speaker_clashes(self): """Test that speaker clashes are reported"""