Merge pull request #10 from pyohio/6-schedule-builder
[6] Schedule Builder
This commit is contained in:
		
						commit
						6b9fb3a49d
					
				
					 9 changed files with 388 additions and 4 deletions
				
			
		| 
						 | 
					@ -19,6 +19,7 @@ Apps:
 | 
				
			||||||
   sponsorship
 | 
					   sponsorship
 | 
				
			||||||
   speakers
 | 
					   speakers
 | 
				
			||||||
   proposals
 | 
					   proposals
 | 
				
			||||||
 | 
					   schedule
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Indices and tables
 | 
					Indices and tables
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										49
									
								
								docs/schedule.rst
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								docs/schedule.rst
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,49 @@
 | 
				
			||||||
 | 
					Schedule App
 | 
				
			||||||
 | 
					===========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The ``schedule`` app allows staff members to create the schedule for the 
 | 
				
			||||||
 | 
					conference's presentations, breaks, lunches, etc.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The ```schedule``` app has a number of models that facilitate building the
 | 
				
			||||||
 | 
					structured schedule:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  * Schedule: A high level container that maps to each Conference Section.
 | 
				
			||||||
 | 
					  * Day: A Day associated with a Schedule.
 | 
				
			||||||
 | 
					  * Room: A Room associated with a Schedule. 
 | 
				
			||||||
 | 
					  * Slot Kind: A type of Slot associated with a Schedule.
 | 
				
			||||||
 | 
					  * Slot: A discrete time period for a Schedule.
 | 
				
			||||||
 | 
					  * Slot Room: A mapping of a Room and Slot for a given Schedule.
 | 
				
			||||||
 | 
					  * Presentation: A mapping of a Slot to an approved Proposal from the ```proposals``` app.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Schedule Builder Form
 | 
				
			||||||
 | 
					---------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It can be cumbersome to generate a schedule through the admin. With that in mind,
 | 
				
			||||||
 | 
					a generic schedule builder is available via a Schedule's edit view. For instance,
 | 
				
			||||||
 | 
					if a Conference site has a Talks Section and Schedule, the form would be
 | 
				
			||||||
 | 
					available for Staff at::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/schedule/talks/edit
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The form consumes a structured CSV file, from which it will build the schedule. 
 | 
				
			||||||
 | 
					Sample CSV data is included below::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"date","time_start","time_end","kind"," room "
 | 
				
			||||||
 | 
					"12/12/2013","10:00 AM","11:00 AM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/12/2013","10:00 AM","11:00 AM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","11:00 AM","12:00 PM","talk","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","11:00 AM","12:00 PM","talk","Room2"
 | 
				
			||||||
 | 
					"12/12/2013","12:00 PM","12:45 PM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","12:00 PM","12:45 PM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","10:00 AM","11:00 AM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","10:00 AM","11:00 AM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","11:00 AM","12:00 PM","talk","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","11:00 AM","12:00 PM","talk","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","12:00 PM","12:45 PM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","12:00 PM","12:45 PM","plenary","Room2"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It is worth noting that this generates the **structure** of the schedule. It 
 | 
				
			||||||
 | 
					does not create Presentation objects. This will need to be done manually.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					One can also **delete** an existing schedule via the delete action. This is
 | 
				
			||||||
 | 
					irreversible (save for a database restore).
 | 
				
			||||||
| 
						 | 
					@ -1,9 +1,17 @@
 | 
				
			||||||
 | 
					import csv
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django import forms
 | 
					from django import forms
 | 
				
			||||||
 | 
					from django.contrib import messages
 | 
				
			||||||
 | 
					from django.db import IntegrityError, transaction
 | 
				
			||||||
from django.db.models import Q
 | 
					from django.db.models import Q
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from markitup.widgets import MarkItUpWidget
 | 
					from markitup.widgets import MarkItUpWidget
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from symposion.schedule.models import Presentation
 | 
					from symposion.schedule.models import (Day, Presentation, Room, SlotKind, Slot,
 | 
				
			||||||
 | 
					                                       SlotRoom)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SlotEditForm(forms.Form):
 | 
					class SlotEditForm(forms.Form):
 | 
				
			||||||
| 
						 | 
					@ -40,3 +48,114 @@ class SlotEditForm(forms.Form):
 | 
				
			||||||
            "initial": self.slot.content_override,
 | 
					            "initial": self.slot.content_override,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return forms.CharField(**kwargs)
 | 
					        return forms.CharField(**kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ScheduleSectionForm(forms.Form):
 | 
				
			||||||
 | 
					    ROOM_KEY = 'room'
 | 
				
			||||||
 | 
					    DATE_KEY = 'date'
 | 
				
			||||||
 | 
					    START_KEY = 'time_start'
 | 
				
			||||||
 | 
					    END_KEY = 'time_end'
 | 
				
			||||||
 | 
					    KIND = 'kind'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    filename = forms.FileField(
 | 
				
			||||||
 | 
					        label='Select a CSV file to import:',
 | 
				
			||||||
 | 
					        required=False
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(self, *args, **kwargs):
 | 
				
			||||||
 | 
					        self.schedule = kwargs.pop("schedule")
 | 
				
			||||||
 | 
					        super(ScheduleSectionForm, self).__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def clean_filename(self):
 | 
				
			||||||
 | 
					        if 'submit' in self.data:
 | 
				
			||||||
 | 
					            fname = self.cleaned_data.get('filename')
 | 
				
			||||||
 | 
					            if not fname or not fname.name.endswith('.csv'):
 | 
				
			||||||
 | 
					                raise forms.ValidationError(u'Please upload a .csv file')
 | 
				
			||||||
 | 
					            return fname
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _get_start_end_times(self, data):
 | 
				
			||||||
 | 
					        "Return start and end time objects"
 | 
				
			||||||
 | 
					        times = []
 | 
				
			||||||
 | 
					        for x in [data[self.START_KEY], data[self.END_KEY]]:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                time_obj = time.strptime(x, '%I:%M %p')
 | 
				
			||||||
 | 
					            except:
 | 
				
			||||||
 | 
					                return messages.ERROR, u'Malformed time found: %s.' % x
 | 
				
			||||||
 | 
					            time_obj = datetime(100, 1, 1, time_obj.tm_hour, time_obj.tm_min, 00)
 | 
				
			||||||
 | 
					            times.append(time_obj.time())
 | 
				
			||||||
 | 
					        return times
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _build_rooms(self, data):
 | 
				
			||||||
 | 
					        "Get or Create Rooms based on schedule type and set of Tracks"
 | 
				
			||||||
 | 
					        created_rooms = []
 | 
				
			||||||
 | 
					        rooms = sorted(set([x[self.ROOM_KEY] for x in data]))
 | 
				
			||||||
 | 
					        for i, room in enumerate(rooms):
 | 
				
			||||||
 | 
					            room, created = Room.objects.get_or_create(
 | 
				
			||||||
 | 
					                schedule=self.schedule, name=room, order=i
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if created:
 | 
				
			||||||
 | 
					                created_rooms.append(room)
 | 
				
			||||||
 | 
					        return created_rooms
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _build_days(self, data):
 | 
				
			||||||
 | 
					        "Get or Create Days based on schedule type and set of Days"
 | 
				
			||||||
 | 
					        created_days = []
 | 
				
			||||||
 | 
					        days = set([x[self.DATE_KEY] for x in data])
 | 
				
			||||||
 | 
					        for day in days:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                date = datetime.strptime(day, "%m/%d/%Y")
 | 
				
			||||||
 | 
					            except ValueError:
 | 
				
			||||||
 | 
					                [x.delete() for x in created_days]
 | 
				
			||||||
 | 
					                return messages.ERROR, u'Malformed data found: %s.' % day
 | 
				
			||||||
 | 
					            day, created = Day.objects.get_or_create(
 | 
				
			||||||
 | 
					                schedule=self.schedule, date=date
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if created:
 | 
				
			||||||
 | 
					                created_days.append(day)
 | 
				
			||||||
 | 
					        return created_days
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def build_schedule(self):
 | 
				
			||||||
 | 
					        created_items = []
 | 
				
			||||||
 | 
					        reader = csv.DictReader(self.cleaned_data.get('filename'))
 | 
				
			||||||
 | 
					        data = [dict((k.strip(), v.strip()) for k, v in x.items()) for x in reader]
 | 
				
			||||||
 | 
					        # build rooms
 | 
				
			||||||
 | 
					        created_items.extend(self._build_rooms(data))
 | 
				
			||||||
 | 
					        # build_days
 | 
				
			||||||
 | 
					        created_items.extend(self._build_days(data))
 | 
				
			||||||
 | 
					        # build Slot  -> SlotRoom
 | 
				
			||||||
 | 
					        for row in data:
 | 
				
			||||||
 | 
					            room = Room.objects.get(
 | 
				
			||||||
 | 
					                schedule=self.schedule, name=row[self.ROOM_KEY]
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            date = datetime.strptime(row[self.DATE_KEY], "%m/%d/%Y")
 | 
				
			||||||
 | 
					            day = Day.objects.get(schedule=self.schedule, date=date)
 | 
				
			||||||
 | 
					            start, end = self._get_start_end_times(row)
 | 
				
			||||||
 | 
					            slot_kind, created = SlotKind.objects.get_or_create(
 | 
				
			||||||
 | 
					                label=row[self.KIND], schedule=self.schedule
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            if created:
 | 
				
			||||||
 | 
					                created_items.append(slot_kind)
 | 
				
			||||||
 | 
					            if row[self.KIND] == 'plenary':
 | 
				
			||||||
 | 
					                slot, created = Slot.objects.get_or_create(
 | 
				
			||||||
 | 
					                    kind=slot_kind, day=day, start=start, end=end
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                if created:
 | 
				
			||||||
 | 
					                    created_items.append(slot)
 | 
				
			||||||
 | 
					            else:
 | 
				
			||||||
 | 
					                slot = Slot.objects.create(
 | 
				
			||||||
 | 
					                    kind=slot_kind, day=day, start=start, end=end
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					                created_items.append(slot)
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                with transaction.atomic():
 | 
				
			||||||
 | 
					                    SlotRoom.objects.create(slot=slot, room=room)
 | 
				
			||||||
 | 
					            except IntegrityError:
 | 
				
			||||||
 | 
					                # delete all created objects and report error
 | 
				
			||||||
 | 
					                for x in created_items:
 | 
				
			||||||
 | 
					                    x.delete()
 | 
				
			||||||
 | 
					                return messages.ERROR, u'An overlap occurred; the import was cancelled.'
 | 
				
			||||||
 | 
					        return messages.SUCCESS, u'Your schedule has been imported.'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def delete_schedule(self):
 | 
				
			||||||
 | 
					        self.schedule.day_set.all().delete()
 | 
				
			||||||
 | 
					        return messages.SUCCESS, u'Your schedule has been deleted.'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										0
									
								
								symposion/schedule/tests/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								symposion/schedule/tests/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										13
									
								
								symposion/schedule/tests/data/schedule.csv
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								symposion/schedule/tests/data/schedule.csv
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,13 @@
 | 
				
			||||||
 | 
					"date","time_start","time_end","kind"," room "
 | 
				
			||||||
 | 
					"12/12/2013","10:00 AM","11:00 AM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/12/2013","10:00 AM","11:00 AM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","11:00 AM","12:00 PM","talk","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","11:00 AM","12:00 PM","talk","Room2"
 | 
				
			||||||
 | 
					"12/12/2013","12:00 PM","12:45 PM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","12:00 PM","12:45 PM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","10:00 AM","11:00 AM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","10:00 AM","11:00 AM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","11:00 AM","12:00 PM","talk","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","11:00 AM","12:00 PM","talk","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","12:00 PM","12:45 PM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","12:00 PM","12:45 PM","plenary","Room2"
 | 
				
			||||||
		
		
			
  | 
							
								
								
									
										14
									
								
								symposion/schedule/tests/data/schedule_overlap.csv
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								symposion/schedule/tests/data/schedule_overlap.csv
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					"date","time_start","time_end","kind"," room "
 | 
				
			||||||
 | 
					"12/12/2013","10:00 AM","11:00 AM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/12/2013","10:00 AM","11:00 AM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","11:00 AM","12:00 PM","talk","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","11:00 AM","12:00 PM","talk","Room2"
 | 
				
			||||||
 | 
					"12/12/2013","12:00 PM","12:45 PM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/12/2013","12:00 PM","12:45 PM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","10:00 AM","11:00 AM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","10:00 AM","11:00 AM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","11:00 AM","12:00 PM","talk","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","11:00 AM","12:00 PM","talk","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","12:00 PM","12:45 PM","plenary","Room1"
 | 
				
			||||||
 | 
					"12/13/2013","12:00 PM","12:45 PM","plenary","Room2"
 | 
				
			||||||
 | 
					"12/13/2013","12:00 PM","12:45 PM","plenary","Room2"
 | 
				
			||||||
		
		
			
  | 
							
								
								
									
										156
									
								
								symposion/schedule/tests/test_forms.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										156
									
								
								symposion/schedule/tests/test_forms.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,156 @@
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.core.files.uploadedfile import SimpleUploadedFile
 | 
				
			||||||
 | 
					from django.test import TestCase
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from symposion.conference.models import Conference, Section
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ..forms import ScheduleSectionForm
 | 
				
			||||||
 | 
					from ..models import Day, Room, Schedule, Slot, SlotKind
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ScheduleSectionFormTests(TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setUp(self):
 | 
				
			||||||
 | 
					        self.conference = Conference.objects.create(title='test')
 | 
				
			||||||
 | 
					        self.section = Section.objects.create(
 | 
				
			||||||
 | 
					            conference=self.conference,
 | 
				
			||||||
 | 
					            name='test')
 | 
				
			||||||
 | 
					        self.schedule = Schedule.objects.create(section=self.section)
 | 
				
			||||||
 | 
					        self.today = datetime.now()
 | 
				
			||||||
 | 
					        self.tomorrow = self.today + timedelta(days=1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_clean_filename(self):
 | 
				
			||||||
 | 
					        """Ensure a file is provided if the submit action was utilized"""
 | 
				
			||||||
 | 
					        data = {'submit': 'Submit'}
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(data=data, schedule=self.schedule)
 | 
				
			||||||
 | 
					        self.assertIn('filename', form.errors)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_clean_filename_not_required(self):
 | 
				
			||||||
 | 
					        """Ensure file is not required if the delete action was utilize"""
 | 
				
			||||||
 | 
					        data = {'delete': 'Delete'}
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(data=data, schedule=self.schedule)
 | 
				
			||||||
 | 
					        self.assertTrue(form.is_valid())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_delete(self):
 | 
				
			||||||
 | 
					        """Delete schedule (Days) for supplied section"""
 | 
				
			||||||
 | 
					        Day.objects.create(schedule=self.schedule, date=self.today)
 | 
				
			||||||
 | 
					        Day.objects.create(schedule=self.schedule, date=self.tomorrow)
 | 
				
			||||||
 | 
					        other_section = Section.objects.create(conference=self.conference, name='other')
 | 
				
			||||||
 | 
					        other_schedule = Schedule.objects.create(section=other_section)
 | 
				
			||||||
 | 
					        other_day = Day.objects.create(schedule=other_schedule, date=self.tomorrow)
 | 
				
			||||||
 | 
					        self.assertEqual(3, Day.objects.all().count())
 | 
				
			||||||
 | 
					        data = {'delete': 'Delete'}
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(data=data, schedule=self.schedule)
 | 
				
			||||||
 | 
					        form.delete_schedule()
 | 
				
			||||||
 | 
					        days = Day.objects.all()
 | 
				
			||||||
 | 
					        self.assertEqual(1, days.count())
 | 
				
			||||||
 | 
					        self.assertIn(other_day, days)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_build_days(self):
 | 
				
			||||||
 | 
					        """Test private method to build days based off ingested CSV"""
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(schedule=self.schedule)
 | 
				
			||||||
 | 
					        data = (
 | 
				
			||||||
 | 
					            {'date': datetime.strftime(self.today, "%m/%d/%Y")},
 | 
				
			||||||
 | 
					            {'date': datetime.strftime(self.today, "%m/%d/%Y")},
 | 
				
			||||||
 | 
					            {'date': datetime.strftime(self.tomorrow, "%m/%d/%Y")},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(0, Day.objects.all().count())
 | 
				
			||||||
 | 
					        form._build_days(data)
 | 
				
			||||||
 | 
					        self.assertEqual(2, Day.objects.all().count())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_build_days_malformed(self):
 | 
				
			||||||
 | 
					        """Test failure for malformed date in CSV"""
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(schedule=self.schedule)
 | 
				
			||||||
 | 
					        data = (
 | 
				
			||||||
 | 
					            {'date': datetime.strftime(self.today, "%m/%d/%Y")},
 | 
				
			||||||
 | 
					            {'date': '12-12-12'}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(0, Day.objects.all().count())
 | 
				
			||||||
 | 
					        msg_type, msg = form._build_days(data)
 | 
				
			||||||
 | 
					        self.assertEqual(0, Day.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(40, msg_type)
 | 
				
			||||||
 | 
					        self.assertIn('12-12-12', msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_build_rooms(self):
 | 
				
			||||||
 | 
					        """Test private method to build rooms based off ingested CSV"""
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(schedule=self.schedule)
 | 
				
			||||||
 | 
					        data = (
 | 
				
			||||||
 | 
					            {'room': 'foo'},
 | 
				
			||||||
 | 
					            {'room': 'bar'},
 | 
				
			||||||
 | 
					            {'room': 'foo'},
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.assertEqual(0, Room.objects.all().count())
 | 
				
			||||||
 | 
					        form._build_rooms(data)
 | 
				
			||||||
 | 
					        self.assertEqual(2, Room.objects.all().count())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_start_end_times(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test private method to convert start and end times based off
 | 
				
			||||||
 | 
					        ingested CSV
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(schedule=self.schedule)
 | 
				
			||||||
 | 
					        start = '12:00 PM'
 | 
				
			||||||
 | 
					        end = '01:00 PM'
 | 
				
			||||||
 | 
					        data = {'time_start': start, 'time_end': end}
 | 
				
			||||||
 | 
					        start_time, end_time = form._get_start_end_times(data)
 | 
				
			||||||
 | 
					        self.assertEqual(start, start_time.strftime('%I:%M %p'))
 | 
				
			||||||
 | 
					        self.assertEqual(end, end_time.strftime('%I:%M %p'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_get_start_end_times_malformed(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test private method for malformed time based off ingested CSV
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(schedule=self.schedule)
 | 
				
			||||||
 | 
					        start = '12:00'
 | 
				
			||||||
 | 
					        end = '01:00'
 | 
				
			||||||
 | 
					        data = {'time_start': start, 'time_end': end}
 | 
				
			||||||
 | 
					        msg_type, msg = form._get_start_end_times(data)
 | 
				
			||||||
 | 
					        self.assertEqual(40, msg_type)
 | 
				
			||||||
 | 
					        self.assertIn('Malformed', msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_build_schedule(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test successful schedule build based off ingested CSV
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.assertEqual(0, Day.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, Room.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, Slot.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, SlotKind.objects.all().count())
 | 
				
			||||||
 | 
					        schedule_csv = open(os.path.join(DATA_DIR, 'schedule.csv'), 'rb')
 | 
				
			||||||
 | 
					        file_data = {'filename': SimpleUploadedFile(schedule_csv.name, schedule_csv.read())}
 | 
				
			||||||
 | 
					        data = {'submit': 'Submit'}
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(data, file_data, schedule=self.schedule)
 | 
				
			||||||
 | 
					        form.is_valid()
 | 
				
			||||||
 | 
					        msg_type, msg = form.build_schedule()
 | 
				
			||||||
 | 
					        self.assertEqual(25, msg_type)
 | 
				
			||||||
 | 
					        self.assertIn('imported', msg)
 | 
				
			||||||
 | 
					        self.assertEqual(2, Day.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(2, Room.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(8, Slot.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(2, SlotKind.objects.all().count())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_build_schedule_overlap(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        Test rolledback schedule build based off ingested CSV with Slot overlap
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        self.assertEqual(0, Day.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, Room.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, Slot.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, SlotKind.objects.all().count())
 | 
				
			||||||
 | 
					        schedule_csv = open(os.path.join(DATA_DIR, 'schedule_overlap.csv'), 'rb')
 | 
				
			||||||
 | 
					        file_data = {'filename': SimpleUploadedFile(schedule_csv.name, schedule_csv.read())}
 | 
				
			||||||
 | 
					        data = {'submit': 'Submit'}
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(data, file_data, schedule=self.schedule)
 | 
				
			||||||
 | 
					        form.is_valid()
 | 
				
			||||||
 | 
					        msg_type, msg = form.build_schedule()
 | 
				
			||||||
 | 
					        self.assertEqual(40, msg_type)
 | 
				
			||||||
 | 
					        self.assertIn('overlap', msg)
 | 
				
			||||||
 | 
					        self.assertEqual(0, Day.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, Room.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, Slot.objects.all().count())
 | 
				
			||||||
 | 
					        self.assertEqual(0, SlotKind.objects.all().count())
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,9 @@ from django.shortcuts import render, get_object_or_404, redirect
 | 
				
			||||||
from django.template import loader, Context
 | 
					from django.template import loader, Context
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.auth.decorators import login_required
 | 
					from django.contrib.auth.decorators import login_required
 | 
				
			||||||
 | 
					from django.contrib import messages
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from symposion.schedule.forms import SlotEditForm
 | 
					from symposion.schedule.forms import SlotEditForm, ScheduleSectionForm
 | 
				
			||||||
from symposion.schedule.models import Schedule, Day, Slot, Presentation
 | 
					from symposion.schedule.models import Schedule, Day, Slot, Presentation
 | 
				
			||||||
from symposion.schedule.timetable import TimeTable
 | 
					from symposion.schedule.timetable import TimeTable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -100,11 +101,24 @@ def schedule_edit(request, slug=None):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    schedule = fetch_schedule(slug)
 | 
					    schedule = fetch_schedule(slug)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if request.method == "POST":
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(
 | 
				
			||||||
 | 
					            request.POST, request.FILES, schedule=schedule
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        if form.is_valid():
 | 
				
			||||||
 | 
					            if 'submit' in form.data:
 | 
				
			||||||
 | 
					                msg = form.build_schedule()
 | 
				
			||||||
 | 
					            elif 'delete' in form.data:
 | 
				
			||||||
 | 
					                msg = form.delete_schedule()
 | 
				
			||||||
 | 
					            messages.add_message(request, msg[0], msg[1])
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        form = ScheduleSectionForm(schedule=schedule)
 | 
				
			||||||
    days_qs = Day.objects.filter(schedule=schedule)
 | 
					    days_qs = Day.objects.filter(schedule=schedule)
 | 
				
			||||||
    days = [TimeTable(day) for day in days_qs]
 | 
					    days = [TimeTable(day) for day in days_qs]
 | 
				
			||||||
    ctx = {
 | 
					    ctx = {
 | 
				
			||||||
        "schedule": schedule,
 | 
					        "schedule": schedule,
 | 
				
			||||||
        "days": days,
 | 
					        "days": days,
 | 
				
			||||||
 | 
					        "form": form
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return render(request, "schedule/schedule_edit.html", ctx)
 | 
					    return render(request, "schedule/schedule_edit.html", ctx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,11 @@
 | 
				
			||||||
                {% include "schedule/_edit_grid.html" %}
 | 
					                {% include "schedule/_edit_grid.html" %}
 | 
				
			||||||
            {% endfor %}
 | 
					            {% endfor %}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        
 | 
					        <form id="schedule-builder" action="." method="post" enctype="multipart/form-data">{% csrf_token %}
 | 
				
			||||||
 | 
					            {{ form.as_p }}
 | 
				
			||||||
 | 
					            <input type="submit" name="submit" value="Submit" />
 | 
				
			||||||
 | 
					            <input type="submit" id="delete" name="delete" value="Delete Schedule" />
 | 
				
			||||||
 | 
					        </form>
 | 
				
			||||||
        <div class="modal fade hide in" id="slotEditModal"></div>
 | 
					        <div class="modal fade hide in" id="slotEditModal"></div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					@ -41,5 +45,19 @@
 | 
				
			||||||
                e.preventDefault();
 | 
					                e.preventDefault();
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					        $(function() {
 | 
				
			||||||
 | 
					           //submit event handler
 | 
				
			||||||
 | 
					            $("form#schedule-builder :submit").click(function(e) {
 | 
				
			||||||
 | 
					                var name = this.name;
 | 
				
			||||||
 | 
					                if(name == 'delete') {
 | 
				
			||||||
 | 
					                    if (!confirm("Are you sure you want to delete the schedule?"))
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
 | 
					                            e.preventDefault();
 | 
				
			||||||
 | 
					                            return;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue