Skip to content

Commit

Permalink
Merge pull request #328 from CTPUG/feature/better-slot-management
Browse files Browse the repository at this point in the history
Feature/better slot management
  • Loading branch information
drnlm authored Nov 24, 2016
2 parents 426a611 + fd91f9b commit 2341643
Show file tree
Hide file tree
Showing 4 changed files with 240 additions and 10 deletions.
42 changes: 38 additions & 4 deletions wafer/schedule/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ class Meta:
def __init__(self, *args, **kwargs):
super(ScheduleItemAdminForm, self).__init__(*args, **kwargs)
self.fields['talk'].queryset = Talk.objects.filter(
Q(status=ACCEPTED) | Q(status=CANCELLED))
Q(status=ACCEPTED) | Q(status=CANCELLED))
# Present all pages as possible entries in the schedule
self.fields['page'].queryset = Page.objects.all()

Expand Down Expand Up @@ -273,14 +273,48 @@ class SlotAdminAddForm(SlotAdminForm):
"this one"))


class SlotDayFilter(admin.SimpleListFilter):
# Allow filtering slots by the day, to make editing slots easier
# We need to do this as a filter, since we can't use sorting since
# day is dynamic (either the model field or the previous_slot)
title = _('Day')
parameter_name = 'day'

def lookups(self, request, model_admin):
# List filter wants the value to be a string, so we use
# pk to avoid bouncing through strptime.
return [('%d' % day.pk, str(day)) for day in Day.objects.all()]

def queryset(self, request, queryset):
if self.value():
day_pk = int(self.value())
day = Day.objects.get(pk=day_pk)
# Find all slots that have the day explicitly set
slots = list(Slot.objects.filter(day=day))
all_slots = slots[:]
# Recursively find slots with a previous_slot set to one of these
while Slot.objects.filter(previous_slot__in=slots).exists():
slots = list(Slot.objects.filter(
previous_slot__in=slots).all())
all_slots.extend(slots)
# Return the filtered list
return queryset.filter(Q(previous_slot__in=all_slots) |
Q(day=day))
# No value, so no filtering
return queryset


class SlotAdmin(admin.ModelAdmin):
form = SlotAdminForm

list_display = ('__str__', 'day', 'end_time')
list_display = ('__str__', 'get_day', 'get_formatted_start_time',
'end_time')
list_editable = ('end_time',)

change_list_template = 'admin/slot_list.html'

list_filter = (SlotDayFilter, )

def changelist_view(self, request, extra_context=None):
extra_context = extra_context or {}
# Find issues with the slots
Expand Down Expand Up @@ -308,8 +342,8 @@ def save_model(self, request, obj, form, change):
# created , and we specify them as a sequence using
# "previous_slot" so tweaking start times is simple.
prev = obj
end = datetime.datetime.combine(prev.day.date, prev.end_time)
start = datetime.datetime.combine(prev.day.date,
end = datetime.datetime.combine(prev.get_day().date, prev.end_time)
start = datetime.datetime.combine(prev.get_day().date,
prev.get_start_time())
slot_len = end - start
for loop in range(form.cleaned_data['additional']):
Expand Down
15 changes: 12 additions & 3 deletions wafer/schedule/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,22 @@ def __str__(self):
slot = u'Slot %s' % self.name
else:
slot = u'Slot'
start = self.get_start_time().strftime('%H:%M')
end = self.end_time.strftime('%H:%M')
start = self.get_formatted_start_time()
end = self.get_formatted_end_time()
return u'%s: %s: %s - %s' % (slot, self.get_day(), start, end)

def get_start_time(self):
if self.previous_slot:
return self.previous_slot.end_time
return self.start_time

def get_formatted_start_time(self):
return self.get_start_time().strftime('%H:%M')
get_formatted_start_time.short_description = 'Start Time'

def get_formatted_end_time(self):
return self.end_time.strftime('%H:%M')

def get_duration(self):
"""Return the duration of the slot as hours and minutes.
Expand All @@ -112,6 +119,7 @@ def get_day(self):
if self.previous_slot:
return self.previous_slot.get_day()
return self.day
get_day.short_description = 'Day'

def clean(self):
"""Ensure we have start_time < end_time"""
Expand Down Expand Up @@ -196,11 +204,12 @@ def get_details(self):
def get_start_time(self):
slots = list(self.slots.all())
if slots:
start = slots[0].get_start_time().strftime('%H:%M')
start = slots[0].get_formatted_start_time()
day = slots[0].get_day()
return u'%s, %s' % (day, start)
else:
return 'WARNING: No Time and Day Specified'
get_start_time.short_description = 'Start Time'

def __str__(self):
return u'%s in %s at %s' % (self.get_desc(), self.venue,
Expand Down
2 changes: 0 additions & 2 deletions wafer/schedule/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,3 @@ def create(self, validated_data):
existing_schedule_item.save()
return existing_schedule_item
return super(ScheduleItemSerializer, self).create(validated_data)


191 changes: 190 additions & 1 deletion wafer/schedule/tests/test_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@

from wafer.pages.models import Page
from wafer.schedule.admin import (
SlotAdmin, find_overlapping_slots, validate_items,
SlotAdmin, SlotDayFilter,
find_overlapping_slots, validate_items,
find_duplicate_schedule_items, find_clashes, find_invalid_venues,
find_non_contiguous)
from wafer.schedule.models import Day, Venue, Slot, ScheduleItem
Expand Down Expand Up @@ -130,6 +131,194 @@ def test_save_model_new_additional(self):
self.assertEqual(slot4.end_time, D.time(20, 30, 0))
self.assertEqual(slot4.day, slot.day)

def test_save_model_prev_slot_additional(self):
"""Test save_model changing a new slot with some additional slots,
starting from a slot specified via previous slot"""
prev_slot = Slot(day=self.day, start_time=D.time(11, 0, 0),
end_time=D.time(11, 30, 0))
prev_slot.save()
self.assertEqual(Slot.objects.count(), 1)
slot = Slot(previous_slot=prev_slot, end_time=D.time(12, 00, 0))
# Check that newly added slot isn't in the database
self.assertEqual(Slot.objects.count(), 1)
request = HttpRequest()
dummy = make_dummy_form(2)
self.admin.save_model(request, slot, dummy, False)
self.assertEqual(Slot.objects.count(), 4)

# check the hierachy is created correctly
slot1 = Slot.objects.filter(previous_slot=slot).get()
self.assertEqual(slot1.get_start_time(), slot.end_time)
self.assertEqual(slot1.end_time, D.time(12, 30, 0))
slot2 = Slot.objects.filter(previous_slot=slot1).get()
self.assertEqual(slot2.get_start_time(), slot1.end_time)
self.assertEqual(slot2.end_time, D.time(13, 00, 0))


class ListFilterTest(TestCase):
"""Test the list filter"""
# Because of how we implement the filter, the order of results
# won't match order of creation or time order. This isn't
# serious, because the admin interface will sort things
# anyway by the user's chosen key, but it does mean we use sets
# in several test case to avoid issues with this.

def setUp(self):
"""Create some data for use in the actual tests."""
self.day1 = Day.objects.create(date=D.date(2013, 9, 22))
self.day2 = Day.objects.create(date=D.date(2013, 9, 23))
self.day3 = Day.objects.create(date=D.date(2013, 9, 24))
self.admin = SlotAdmin(Slot, None)

def _make_filter(self, day):
"""create a list filter for testing."""
# We can get away with request None, since SimpleListFilter
# doesn't use request in the bits we want to test
if day:
return SlotDayFilter(None, {'day': str(day.pk)}, Slot, self.admin)
else:
return SlotDayFilter(None, {'day': None}, Slot, self.admin)

def test_filter_lookups(self):
"""Test that filter lookups are sane."""
TestFilter = self._make_filter(self.day1)
# Check lookup details
lookups = TestFilter.lookups(None, self.admin)
self.assertEqual(len(lookups), 3)
self.assertEqual(lookups[0], ('%d' % self.day1.pk, str(self.day1)))
TestFilter = self._make_filter(self.day3)
lookups2 = TestFilter.lookups(None, self.admin)
self.assertEqual(lookups, lookups2)

def test_queryset_day_time(self):
"""Test queries with slots created purely by day + start_time"""
slots = {}
slots[self.day1] = [Slot(day=self.day1, start_time=D.time(11, 0, 0),
end_time=D.time(12, 00, 0))]
slots[self.day2] = [Slot(day=self.day2, start_time=D.time(11, 0, 0),
end_time=D.time(12, 00, 0))]
# Day1 slots
for x in range(12, 17):
slots[self.day1].append(Slot(day=self.day1,
start_time=D.time(x, 0, 0),
end_time=D.time(x+1, 0, 0)))
if x < 15:
# Fewer slots for day 2
slots[self.day2].append(Slot(day=self.day2,
start_time=D.time(x, 0, 0),
end_time=D.time(x+1, 0, 0)))
for d in slots:
for s in slots[d]:
s.save()
# Check Null filter
TestFilter = self._make_filter(None)
self.assertEqual(list(TestFilter.queryset(None, Slot.objects.all())),
list(Slot.objects.all()))
# Test Day1
TestFilter = self._make_filter(self.day1)
queries = set(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, set(slots[self.day1]))
# Test Day2
TestFilter = self._make_filter(self.day2)
queries = set(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, set(slots[self.day2]))

# Check no match case
TestFilter = self._make_filter(self.day3)
queries = list(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, [])

def test_queryset_prev_slot(self):
"""Test lookup with a chain of previous slots."""
slots = {}
prev = Slot(day=self.day1, start_time=D.time(11, 0, 0),
end_time=D.time(12, 00, 0))
prev.save()
slots[self.day1] = [prev]
prev = Slot(day=self.day2, start_time=D.time(11, 0, 0),
end_time=D.time(12, 00, 0))
prev.save()
slots[self.day2] = [prev]
# Day1 slots
for x in range(12, 17):
prev1 = slots[self.day1][-1]
slots[self.day1].append(Slot(previous_slot=prev1,
end_time=D.time(x+1, 0, 0)))
slots[self.day1][-1].save()
if x < 15:
prev2 = slots[self.day2][-1]
# Fewer slots for day 2
slots[self.day2].append(Slot(previous_slot=prev2,
end_time=D.time(x+1, 0, 0)))
slots[self.day2][-1].save()
# Check Null filter
TestFilter = self._make_filter(None)
self.assertEqual(list(TestFilter.queryset(None, Slot.objects.all())),
list(Slot.objects.all()))
# Test Day1
TestFilter = self._make_filter(self.day1)
queries = set(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, set(slots[self.day1]))
# Test Day2
TestFilter = self._make_filter(self.day2)
queries = set(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, set(slots[self.day2]))

# Check no match case
TestFilter = self._make_filter(self.day3)
queries = list(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, [])

def test_queryset_mixed(self):
"""Test with a mix of day+time and previous slot cases."""
slots = {}
prev = Slot(day=self.day1, start_time=D.time(11, 0, 0),
end_time=D.time(12, 00, 0))
prev.save()
slots[self.day1] = [prev]
prev = Slot(day=self.day2, start_time=D.time(11, 0, 0),
end_time=D.time(12, 00, 0))
prev.save()
slots[self.day2] = [prev]
# Day1 slots
for x in range(12, 20):
prev1 = slots[self.day1][-1]
if x % 2:
slots[self.day1].append(Slot(previous_slot=prev1,
end_time=D.time(x+1, 0, 0)))
else:
slots[self.day1].append(Slot(day=self.day1,
start_time=D.time(x, 0, 0),
end_time=D.time(x+1, 0, 0)))

slots[self.day1][-1].save()
prev2 = slots[self.day2][-1]
if x % 5:
slots[self.day2].append(Slot(previous_slot=prev2,
end_time=D.time(x+1, 0, 0)))
else:
slots[self.day2].append(Slot(day=self.day2,
start_time=D.time(x, 0, 0),
end_time=D.time(x+1, 0, 0)))
slots[self.day2][-1].save()
# Check Null filter
TestFilter = self._make_filter(None)
self.assertEqual(list(TestFilter.queryset(None, Slot.objects.all())),
list(Slot.objects.all()))
# Test Day1
TestFilter = self._make_filter(self.day1)
queries = set(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, set(slots[self.day1]))
# Test Day2
TestFilter = self._make_filter(self.day2)
queries = set(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, set(slots[self.day2]))

# Check no match case
TestFilter = self._make_filter(self.day3)
queries = list(TestFilter.queryset(None, Slot.objects.all()))
self.assertEqual(queries, [])


class ValidationTests(TestCase):

Expand Down

0 comments on commit 2341643

Please sign in to comment.