Repository: pretalx/pretalx

Issue:
resetting schedule to an old version can make submissions unavailable
1. create submission: sub1
2. publish a schedule containing only sub1: ScheduleA
3. create submission: sub2
4. publish a schedule containing both sub1 and sub2: ScheduleB
5. reset the current draft to ScheduleA

Sub2 is not part of the current draft and should therefore be visible in the list on the right. But the list is empty. Resetting the draft to ScheduleB restores the talk.
resetting schedule to an old version can make submissions unavailable
1. create submission: sub1
2. publish a schedule containing only sub1: ScheduleA
3. create submission: sub2
4. publish a schedule containing both sub1 and sub2: ScheduleB
5. reset the current draft to ScheduleA

Sub2 is not part of the current draft and should therefore be visible in the list on the right. But the list is empty. Resetting the draft to ScheduleB restores the talk.
resetting schedule to an old version can make submissions unavailable
1. create submission: sub1
2. publish a schedule containing only sub1: ScheduleA
3. create submission: sub2
4. publish a schedule containing both sub1 and sub2: ScheduleB
5. reset the current draft to ScheduleA

Sub2 is not part of the current draft and should therefore be visible in the list on the right. But the list is empty. Resetting the draft to ScheduleB restores the talk.


Discussion:
[`Schedule.unfreeze`](https://github.com/openeventstack/pretalx/blob/master/src/pretalx/schedule/models/schedule.py#L63-L74) gets rid of all talks in the current working draft. It then adds all talks, which are present in ScheduleA. Since sub2 did not exist back then, it does not get added in. I think the most practical solution would be to also add any talks out of the old working draft, which can't be found in ScheduleA. All talks added this way should have their room set to `null`. This way they'll show up in the sidebar as expected.
[`Schedule.unfreeze`](https://github.com/openeventstack/pretalx/blob/master/src/pretalx/schedule/models/schedule.py#L63-L74) gets rid of all talks in the current working draft. It then adds all talks, which are present in ScheduleA. Since sub2 did not exist back then, it does not get added in. I think the most practical solution would be to also add any talks out of the old working draft, which can't be found in ScheduleA. All talks added this way should have their room set to `null`. This way they'll show up in the sidebar as expected.
[`Schedule.unfreeze`](https://github.com/openeventstack/pretalx/blob/master/src/pretalx/schedule/models/schedule.py#L63-L74) gets rid of all talks in the current working draft. It then adds all talks, which are present in ScheduleA. Since sub2 did not exist back then, it does not get added in. I think the most practical solution would be to also add any talks out of the old working draft, which can't be found in ScheduleA. All talks added this way should have their room set to `null`. This way they'll show up in the sidebar as expected.

Resolving patch:
diff --git a/src/pretalx/schedule/models/schedule.py b/src/pretalx/schedule/models/schedule.py
--- a/src/pretalx/schedule/models/schedule.py
+++ b/src/pretalx/schedule/models/schedule.py
@@ -69,13 +69,25 @@ def unfreeze(self, user=None):
         from pretalx.schedule.models import TalkSlot
         if not self.version:
             raise Exception('Cannot unfreeze schedule version: not released yet.')
+
+        # collect all talks, which have been added since this schedule (#72)
+        submission_ids = self.talks.all().values_list('submission_id', flat=True)
+        talks = self.event.wip_schedule.talks \
+            .exclude(submission_id__in=submission_ids) \
+            .union(self.talks.all())
+
+        wip_schedule = Schedule.objects.create(event=self.event)
+        new_talks = []
+        for talk in talks:
+            new_talks.append(talk.copy_to_schedule(wip_schedule, save=False))
+        TalkSlot.objects.bulk_create(new_talks)
+
         self.event.wip_schedule.talks.all().delete()
         self.event.wip_schedule.delete()
-        wip_schedule = Schedule.objects.create(event=self.event)
-        talks = []
-        for talk in self.talks.all():
-            talks.append(talk.copy_to_schedule(wip_schedule, save=False))
-        TalkSlot.objects.bulk_create(talks)
+
+        with suppress(AttributeError):
+            del wip_schedule.event.wip_schedule
+
         return self, wip_schedule
 
     @cached_property


Test patch:
diff --git a/src/tests/unit/schedule/test_schedule_model.py b/src/tests/unit/schedule/test_schedule_model.py
--- a/src/tests/unit/schedule/test_schedule_model.py
+++ b/src/tests/unit/schedule/test_schedule_model.py
@@ -3,6 +3,7 @@
 import pytest
 
 from pretalx.schedule.models import Schedule, TalkSlot
+from pretalx.submission.models import Submission
 
 
 @pytest.mark.django_db
@@ -42,6 +43,45 @@ def test_freeze_cache(talk_slot):
     assert talk_slot.event.wip_schedule.version is None
 
 
+@pytest.mark.django_db
+def test_unfreeze(talk_slot):
+    event = talk_slot.event
+
+    old1, new1 = talk_slot.schedule.freeze('Version 1')
+    talk_slot2 = event.wip_schedule.talks.first()
+    talk_slot2.room = None
+    talk_slot2.save()
+    old2, new2 = event.wip_schedule.freeze('Version 2')
+
+    assert new1 == old2
+
+    schedules = event.schedules.exclude(version=None).order_by('published')
+    assert len(schedules) == 2
+    assert schedules[0].version == 'Version 1'
+    assert schedules[1].version == 'Version 2'
+
+    assert schedules[0].talks.first().room
+    assert not schedules[1].talks.first().room
+
+
+@pytest.mark.django_db
+def test_unfreeze_bug72(talk_slot):
+    # https://github.com/openeventstack/pretalx/issues/72
+    event = talk_slot.event
+
+    schedule1, _ = talk_slot.schedule.freeze('Version 1')
+
+    sub = Submission.objects.create(title='Submission 2', event=event, submission_type=event.cfp.default_type)
+    TalkSlot.objects.create(submission=sub, room=talk_slot.room, schedule=event.wip_schedule, is_visible=True)
+    schedule2, _ = event.wip_schedule.freeze('Version 2')
+
+    schedule1.unfreeze()
+
+    assert event.wip_schedule.talks.count() == 2
+    # make sure the cache for wip_schedule is invalidated
+    assert event.wip_schedule.version is None
+
+
 @pytest.mark.django_db
 def test_scheduled_talks(talk_slot, room):
     assert talk_slot.schedule.scheduled_talks.count() == 0