Repository: CTPUG/wafer

Issue:
Pentabarf export calculates duration incorrectly for items spanning multiple slots
The pentabarf,xml export does the wrong thing if an item spans multiple slots. It calculates the duration from just the first slot, rather than the entire item.



Discussion:
#130 fixes this in the easy case.

Leaving this open for now so we can consider what to do if the slots aren't contigious.

The full schedule does the wrong thing with non-contigious slots already, and I'm not sure there's a good use case allowing a schedule item with non-contigious slots, so the obvious fix is just to make schedule validation in that case.


Resolving patch:
diff --git a/wafer/schedule/admin.py b/wafer/schedule/admin.py
--- a/wafer/schedule/admin.py
+++ b/wafer/schedule/admin.py
@@ -45,6 +45,25 @@ def find_overlapping_slots():
     return overlaps
 
 
+def find_non_contiguous(all_items=None):
+    """Find any items that have slots that aren't contiguous"""
+    if all_items is None:
+        all_items = prefetch_schedule_items()
+    non_contiguous = []
+    for item in all_items:
+        if item.slots.count() < 2:
+            # No point in checking
+            continue
+        last_slot = None
+        for slot in item.slots.all().order_by('end_time'):
+            if last_slot:
+                if last_slot.end_time != slot.get_start_time():
+                    non_contiguous.append(item)
+                    break
+            last_slot = slot
+    return non_contiguous
+
+
 def validate_items(all_items=None):
     """Find errors in the schedule. Check for:
          - pending / rejected talks in the schedule
@@ -143,6 +162,8 @@ def check_schedule():
         return False
     if find_overlapping_slots():
         return False
+    if find_non_contiguous(all_items):
+        return False
     if find_invalid_venues(all_items):
         return False
     return True
@@ -174,10 +195,12 @@ class ScheduleItemAdmin(admin.ModelAdmin):
     def changelist_view(self, request, extra_context=None):
         extra_context = extra_context or {}
         # Find issues in the schedule
+        all_items = None
         clashes = find_clashes()
-        validation = validate_items()
+        validation = validate_items(all_items)
         venues = find_invalid_venues()
-        duplicates = find_duplicate_schedule_items()
+        duplicates = find_duplicate_schedule_items(all_items)
+        non_contiguous = find_non_contiguous(all_items)
         errors = {}
         if clashes:
             errors['clashes'] = clashes
@@ -187,6 +210,8 @@ def changelist_view(self, request, extra_context=None):
             errors['validation'] = validation
         if venues:
             errors['venues'] = venues
+        if non_contiguous:
+            errors['non_contiguous'] = non_contiguous
         extra_context['errors'] = errors
         return super(ScheduleItemAdmin, self).changelist_view(request,
                                                               extra_context)


Test patch:
diff --git a/wafer/schedule/tests/test_admin.py b/wafer/schedule/tests/test_admin.py
--- a/wafer/schedule/tests/test_admin.py
+++ b/wafer/schedule/tests/test_admin.py
@@ -7,7 +7,8 @@
 from wafer.pages.models import Page
 from wafer.schedule.admin import (
     SlotAdmin, find_overlapping_slots, validate_items,
-    find_duplicate_schedule_items, find_clashes, find_invalid_venues)
+    find_duplicate_schedule_items, find_clashes, find_invalid_venues,
+    find_non_contiguous)
 from wafer.schedule.models import Day, Venue, Slot, ScheduleItem
 from wafer.talks.models import Talk, ACCEPTED, REJECTED, PENDING
 
@@ -299,6 +300,53 @@ def test_validation(self):
         invalid = validate_items()
         assert set(invalid) == set([item1, item3])
 
+    def test_non_contiguous(self):
+        """Test that we detect items with non contiguous slots"""
+        # Create a item with a gap in the slots assigned to it
+        day1 = Day.objects.create(date=D.date(2013, 9, 22))
+        venue1 = Venue.objects.create(order=1, name='Venue 1')
+        venue1.days.add(day1)
+
+        start1 = D.time(10, 0, 0)
+        start2 = D.time(11, 0, 0)
+        start3 = D.time(12, 0, 0)
+        end = D.time(13, 0, 0)
+
+        slot1 = Slot.objects.create(start_time=start1, end_time=start2,
+                                    day=day1)
+        slot2 = Slot.objects.create(start_time=start2, end_time=start3,
+                                    day=day1)
+        slot3 = Slot.objects.create(start_time=start3, end_time=end,
+                                    day=day1)
+
+        user = get_user_model().objects.create_user('john', 'best@wafer.test',
+                                                    'johnpassword')
+        talk = Talk.objects.create(title="Test talk", status=ACCEPTED,
+                                   corresponding_author_id=user.id)
+        page = Page.objects.create(name="test page", slug="test")
+
+        item1 = ScheduleItem.objects.create(venue=venue1,
+                                            talk_id=talk.pk)
+        item1.slots.add(slot1)
+        item1.slots.add(slot3)
+
+        item2 = ScheduleItem.objects.create(venue=venue1,
+                                            page_id=page.pk)
+        item2.slots.add(slot2)
+
+        invalid = find_non_contiguous()
+        # Only item1 is invalid
+        assert set(invalid) == set([item1])
+
+        item1.slots.add(slot2)
+        item1.slots.remove(slot1)
+        item2.slots.add(slot1)
+        item2.slots.remove(slot2)
+
+        invalid = validate_items()
+        # Everything is valid now
+        assert set(invalid) == set([])
+
     def test_duplicates(self):
         """Test that we can detect duplicates talks and pages"""
         # Final chedule is