symposion_app/vendor/symposion/schedule/forms.py
James Polley 3239bbe29a Don't duplicate existing rooms when importing schedule CSV
* Old implementation needs to see exactly the same rooms in exactly
  the same order every time it loads new data, otherwise it will
  create a duplicate entry for the room that differs only in display
  order.
* New implementation ignores the display order when checking to see
  if the room already exists.
2017-12-03 16:15:19 +11:00

172 lines
6.2 KiB
Python

import csv
import time
from io import TextIOWrapper
from datetime import datetime
from django import forms
from django.contrib import messages
from django.db import IntegrityError, transaction
from django.db.models import Q
from symposion.schedule.models import (Day, Presentation, Room, SlotKind, Slot,
SlotRoom)
class SlotEditForm(forms.Form):
required_css_class = 'label-required'
def __init__(self, *args, **kwargs):
self.slot = kwargs.pop("slot")
super(SlotEditForm, self).__init__(*args, **kwargs)
# @@@ TODO - Make this configurable
if self.slot.kind.label in ["talk", "tutorial", "keynote"]:
self.fields["presentation"] = self.build_presentation_field()
else:
self.fields["content_override"] = self.build_content_override_field()
def build_presentation_field(self):
kwargs = {}
queryset = Presentation.objects.all()
queryset = queryset.exclude(cancelled=True)
queryset = queryset.order_by("proposal_base__pk")
if self.slot.content:
queryset = queryset.filter(Q(slot=None) | Q(pk=self.slot.content.pk))
kwargs["required"] = False
kwargs["initial"] = self.slot.content
else:
queryset = queryset.filter(slot=None)
kwargs["required"] = True
kwargs["queryset"] = queryset
return forms.ModelChoiceField(**kwargs)
def build_content_override_field(self):
kwargs = {
"label": "Content",
"required": False,
"initial": self.slot.content_override,
}
return forms.CharField(**kwargs)
class ScheduleSectionForm(forms.Form):
required_css_class = 'label-required'
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")
if 'encoding' in kwargs:
self.encoding = kwargs['encoding']
kwargs.pop('encoding')
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):
try:
room = Room.objects.get(schedule=self.schedule, name=room)
created = False
except Room.DoesNotExist:
room = Room.objects.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 = []
f = TextIOWrapper(self.cleaned_data.get('filename'), encoding=self.encoding)
reader = csv.DictReader(f)
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.'