From a75c29c231303d8be9bf212af69f2d786ac59b72 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Thu, 28 May 2020 12:59:44 +0000 Subject: [PATCH 01/17] Add partially completed automatic scheduler. - Legacy-Id: 17891 --- .../management/commands/schedule_generator.py | 636 ++++++++++++++++++ ietf/meeting/test_schedule_generator.py | 124 ++++ 2 files changed, 760 insertions(+) create mode 100644 ietf/meeting/management/commands/schedule_generator.py create mode 100644 ietf/meeting/test_schedule_generator.py diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py new file mode 100644 index 000000000..12f902a83 --- /dev/null +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -0,0 +1,636 @@ +# Copyright The IETF Trust 2020, All Rights Reserved +from __future__ import absolute_import, print_function, unicode_literals + +import calendar +import datetime +import math +import random +import functools +from collections import defaultdict + +from django.core.management.base import BaseCommand, CommandError +from django.db.models import Q +from ietf.meeting import models + +OPTIMISER_MAX_CYCLES = 100 + + +class Command(BaseCommand): + help = 'Create a meeting schedule' + + def add_arguments(self, parser): + parser.add_argument('--meeting', default=None, dest='meeting', + help='Number of the meeting to generate schedule for') + + def handle(self, meeting, verbosity, *args, **kwargs): + ScheduleHandler(meeting, verbosity).run() + + +class ScheduleHandler(object): + def __init__(self, meeting_number, verbosity): + self.verbosity = verbosity + self.meeting = models.Meeting.objects.get(number=meeting_number) + self.load_meeting() + + def run(self): + """Schedule all sessions""" + self.schedule.fill_initial_schedule() + violations, cost = self.schedule.total_schedule_cost() + if self.verbosity >= 1: + print('Initial schedule completed with {} violations, total cost {}' + .format(len(violations), cost)) + + self.schedule.optimise_schedule() + violations, cost = self.schedule.total_schedule_cost() + if self.verbosity >= 1: + print('Optimisation completed with {} violations, total cost {}' + .format(len(violations), cost)) + if self.verbosity >= 1 and violations: + print('Remaining violations:') + for v in violations: + print(v) + + # TODO: actually save the schedule + return violations, cost + + def load_meeting(self): + """Load all timeslots and sessions into in-memory objects.""" + # TODO: ensure these filters are correct + timeslots_db = models.TimeSlot.objects.filter( + meeting=self.meeting, + type_id='regular', + ).exclude(location__capacity=None).select_related('location') + + timeslots = {TimeSlot(t, self.verbosity) for t in timeslots_db} + for timeslot in timeslots: + timeslot.store_relations(timeslots) + + sessions_db = models.Session.objects.filter( + meeting=self.meeting, + type_id='regular', + schedulingevent__status_id='schedw', + ).select_related('group') + + sessions = {Session(self.meeting, s, self.verbosity) for s in sessions_db} + for session in sessions: + # The complexity of a session also depends on how many + # sessions have declared a conflict towards this session. + session.update_complexity(sessions) + + self.schedule = Schedule(timeslots, sessions, self.verbosity) + self.schedule.adjust_for_timeslot_availability() + + +class Schedule(object): + """ + The Schedule object represents the schedule, and contains code to generate/optimise it. + The schedule is internally represented as a dict, timeslots being keys, sessions being values. + Note that "timeslot" means the combination of a timeframe and a location. + """ + def __init__(self, timeslots, sessions, verbosity): + self.timeslots = timeslots + self.sessions = sessions + self.verbosity = verbosity + self.schedule = dict() + self.best_cost = math.inf + self.best_schedule = None + self.initial_random = False + self.fixed_cost = 0 + self.fixed_violations = [] + + def adjust_for_timeslot_availability(self): + """ + Check the number of sessions, their required capacity and duration against availability. + If there are too many sessions, the generator exits. + If sessions can't fit, they are trimmed, and a fixed cost is applied. + """ + if len(self.sessions) > len(self.timeslots): + raise ValueError('More sessions ({}) than timeslots ({})' + .format(len(self.sessions), len(self.timeslots))) + + def make_capacity_adjustments(t_attr, s_attr): + availables = [getattr(timeslot, t_attr) for timeslot in self.timeslots] + availables.sort() + sessions = sorted(self.sessions, key=lambda s: getattr(s, s_attr), reverse=True) + for session in sessions: + found_fit = False + for idx, available in enumerate(availables): + if getattr(session, s_attr) <= available: + availables.pop(idx) + found_fit = True + break + if not found_fit: + largest_available = availables[-1] + f = 'No timeslot with sufficient {} available for {}, requested {}, trimmed to {}' + msg = f.format(t_attr, session.group, getattr(session, s_attr), largest_available) + setattr(session, s_attr, largest_available) + availables.pop(-1) + self.fixed_cost += 10000 # TODO + self.fixed_violations.append(msg) + + make_capacity_adjustments('duration', 'requested_duration') + make_capacity_adjustments('capacity', 'attendees') + + def total_schedule_cost(self): + """ + Calculate the total cost of the current schedule in self.schedule. + This includes the dynamic cost, which can be affected by scheduling choices, + and the fixed cost, which can not be improved upon (e.g. sessions that had + to be trimmed in duration). + Returns a tuple of violations (list of strings) and the total cost (integer). + """ + violations, cost = self.calculate_dynamic_cost() + violations += self.fixed_violations + cost += self.fixed_cost + return violations, cost + + def calculate_dynamic_cost(self, schedule=None): + """ + Calculate the dynamic cost of the current schedule in self.schedule, + or a different provided schedule. "Dynamic" cost means these are costs + that can be affected by scheduling choices. + Returns a tuple of violations (list of strings) and the total cost (integer). + """ + if not schedule: + schedule = self.schedule + violations, cost = [], 0 + + # For performance, a few values are pre-calculated in bulk + group_sessions = defaultdict(set) + overlapping_sessions = defaultdict(set) + for timeslot, session in schedule.items(): + group_sessions[session.group].add((timeslot, session)) + overlapping_sessions[timeslot].update({schedule.get(t) for t in timeslot.overlaps}) + + for timeslot, session in schedule.items(): + session_violations, session_cost = session.calculate_cost( + schedule, timeslot, overlapping_sessions[timeslot], group_sessions[session.group]) + violations += session_violations + cost += session_cost + + return violations, cost + + def fill_initial_schedule(self): + """ + Create an initial schedule, which is stored in self.schedule. + + The initial schedule is created by going through all sessions in order of highest + complexity first. Each sessions is placed in a timeslot chosen by: + - First: lowest cost, taking all sessions into account that have already been scheduled + - Second: shortest duration that still fits + - Third: smallest room that still fits + If there are multiple options with equal value, a random one is picked. + + If self.initial_random is set, each session is assigned to a completely random + timeslot, that still fits. This is used in testing. + """ + if self.verbosity >= 2: + print('== Initial scheduler starting, scheduling {} sessions in {} timeslots =='. + format(len(self.sessions), len(self.timeslots))) + sessions = sorted(self.sessions, key=lambda s: s.complexity, reverse=True) + + for session in sessions: + possible_slots = [t for t in self.timeslots if + t not in self.schedule.keys() and session.fits_in_timeslot(t)] + random.shuffle(possible_slots) + if not len(possible_slots): + # TODO: this needs better handling + raise Exception('No timeslot was left for {} ({})' + .format(session.group, session.session_pk)) + + def timeslot_preference(t): + proposed_schedule = self.schedule.copy() + proposed_schedule[t] = session + return self.calculate_dynamic_cost(proposed_schedule)[1], t.duration, t.capacity + + possible_slots.sort(key=timeslot_preference) + if self.initial_random: + random.shuffle(possible_slots) + self._schedule_session(session, possible_slots[0]) + if self.verbosity >= 3: + print('Scheduled {} at {} in location {}' + .format(session.group, possible_slots[0].start, possible_slots[0].location_pk)) + + def optimise_schedule(self): + """ + Optimise the schedule in self.schedule. Expects fill_initial_schedule() to already + have run - this only moves sessions around that were already scheduled. + + The optimising algorithm performs up to OPTIMISER_MAX_CYCLES runs. In each run, each + scheduled session is considered for a switch with each other scheduled session. + If the switch reduces the total cost of the schedule, the switch is made. + + If the optimiser finishes a whole run without finding any improvements, the schedule + can not be improved further by switching, and sessions are shuffled with + _shuffle_conflicted_sessions() and the continues. + + If the total schedule cost reaches 0 at any time, the schedule is perfect and the + optimiser returns. + """ + last_run_violations = [] + best_cost = math.inf + shuffle_next_run = False + last_run_cost = None + + for run_count in range(OPTIMISER_MAX_CYCLES): + items = list(self.schedule.items()) + random.shuffle(items) + + if self.verbosity >= 2: + print('== Optimiser starting run {}, dynamic cost after last optimiser run {} ==' + .format(run_count, last_run_cost)) + print('Dynamic violations in last optimiser run: {}'.format(last_run_violations)) + if shuffle_next_run: + shuffle_next_run = False + last_run_cost = None # After a shuffle, attempt at least two regular runs + self._shuffle_conflicted_sessions(items) + + for original_timeslot, session in items: + best_cost = self.calculate_dynamic_cost()[1] + if best_cost == 0: + if self.verbosity >= 2: + print('Optimiser found an optimal schedule') + return + best_timeslot = None + + for possible_new_slot in self.timeslots: + cost = self._cost_for_switch(original_timeslot, possible_new_slot) + if cost < best_cost: + best_cost = cost + best_timeslot = possible_new_slot + + if best_timeslot: + switched_with = self._switch_sessions(original_timeslot, best_timeslot) + switched_with = switched_with.group if switched_with else '' + if self.verbosity >= 3: + print('Found cost reduction to {} by switching {} with {}' + .format(best_cost, session.group, switched_with)) + + if last_run_cost == best_cost: + shuffle_next_run = True + last_run_violations, last_run_cost = self.calculate_dynamic_cost() + self._save_schedule() + + if self.verbosity >= 2: + print('Optimiser did not find an optimal schedule, using best schedule at dynamic cost {}' + .format(self.best_cost)) + self.schedule = self.best_schedule + + def _shuffle_conflicted_sessions(self, items): + """ + Shuffle sessions that currently have conflicts. + All sessions that had conflicts in their last run, are shuffled to + an entirely random timeslot, in which they fit. + Parameter is an iterable of (timeslot, session) tuples. + """ + to_reschedule = [(t, s) for t, s in items if s.last_cost] + random.shuffle(to_reschedule) + if self.verbosity >= 2: + print('Optimiser has no more improvements, shuffling sessions {}' + .format(', '.join([s.group for t, s in to_reschedule]))) + + for original_timeslot, rescheduling_session in to_reschedule: + possible_new_slots = list(self.timeslots) + possible_new_slots.remove(original_timeslot) + random.shuffle(possible_new_slots) + + for possible_new_slot in possible_new_slots: + switched_with = self._switch_sessions(original_timeslot, possible_new_slot) + if switched_with is not False: + switched_group = switched_with.group if switched_with else '' + if self.verbosity >= 3: + print('Shuffled {} to random new slot, previously in slot was {}' + .format(rescheduling_session.group, switched_group)) + break + + def _schedule_session(self, session, timeslot): + if not session.fits_in_timeslot(timeslot): + raise ValueError + self.schedule[timeslot] = session + + def _cost_for_switch(self, timeslot1, timeslot2): + """ + Calculate the total cost of self.schedule, if the sessions in timeslot1 and timeslot2 + would be switched. Does not perform the switch, self.schedule remains unchanged. + """ + proposed_schedule = self.schedule.copy() + session1 = proposed_schedule.get(timeslot1) + session2 = proposed_schedule.get(timeslot2) + if session1 and not session1.fits_in_timeslot(timeslot2): + return math.inf + if session2 and not session2.fits_in_timeslot(timeslot1): + return math.inf + if session1: + proposed_schedule[timeslot2] = session1 + elif session2: + del proposed_schedule[timeslot2] + if session2: + proposed_schedule[timeslot1] = session2 + elif session1: + del proposed_schedule[timeslot1] + return self.calculate_dynamic_cost(proposed_schedule)[1] + + def _switch_sessions(self, timeslot1, timeslot2): + """ + Switch the sessions currently in timeslot1 and timeslot2. + If timeslot2 had a session scheduled, returns that Session instance. + """ + session1 = self.schedule.get(timeslot1) + session2 = self.schedule.get(timeslot2) + if timeslot1 == timeslot2: + return False + if session1 and not session1.fits_in_timeslot(timeslot2): + return False + if session2 and not session2.fits_in_timeslot(timeslot1): + return False + if session1: + self.schedule[timeslot2] = session1 + elif session2: + del self.schedule[timeslot2] + if session2: + self.schedule[timeslot1] = session2 + elif session1: + del self.schedule[timeslot1] + return session2 + + def _save_schedule(self): + violations, cost = self.calculate_dynamic_cost() + if cost < self.best_cost: + self.best_cost = cost + self.best_schedule = self.schedule.copy() + + +class TimeSlot(object): + """ + This TimeSlot class is analogous to the TimeSlot class in the models, + i.e. it represents a timeframe in a particular location. + """ + def __init__(self, timeslot_db, verbosity): + """Initialise this object from a TimeSlot model instance.""" + self.verbosity = verbosity + self.timeslot_pk = timeslot_db.pk + self.location_pk = timeslot_db.location.pk + self.capacity = timeslot_db.location.capacity + self.start = timeslot_db.time + self.duration = timeslot_db.duration + self.end = self.start + self.duration + self.day = calendar.day_name[self.start.weekday()].lower() + if self.start.time() < datetime.time(12, 30): + self.time_of_day = 'morning' + elif self.start.time() < datetime.time(15, 30): + self.time_of_day = 'early-afternoon' + else: + self.time_of_day = 'late-afternoon' + self.time_group = self.day + '-' + self.time_of_day + self.overlaps = set() + self.adjacent = set() + + def store_relations(self, other_timeslots): + """ + Store relations to all other timeslots. This should be called + after all TimeSlot objects have been created. This allows fast + lookups of which TimeSlot objects overlap or are adjacent. + """ + for other in other_timeslots: + if any([ + self.start < other.start < self.end, + self.start < other.end < self.end, + self.start >= other.start and self.end <= other.end, + ]) and other != self: + self.overlaps.add(other) + if ( + abs(self.start - other.end) <= datetime.timedelta(minutes=30) or + abs(other.start - self.end) <= datetime.timedelta(minutes=30) + ) and self.location_pk == other.location_pk: + self.adjacent.add(other) + + +class Session(object): + """ + This TimeSlot class is analogous to the Session class in the models, + i.e. it represents a single session to be scheduled. It also pulls + in data about constraints, group parents, etc. + """ + def __init__(self, meeting, session_db, verbosity): + """ + Initialise this object from a Session model instance. + This includes collecting all constraints from the database, + and calculating an initial complexity. + """ + self.verbosity = verbosity + self.session_pk = session_db.pk + self.group = session_db.group.acronym + self.parent = session_db.group.parent.acronym if session_db.group.parent else None + self.ad = session_db.group.ad_role().pk if session_db.group.ad_role() else None + self.is_area_meeting = any([ + session_db.group.type_id == 'area', + session_db.group.type_id == 'ag', + self.group in ['dispatch', 'gendispatch', 'intarea', 'opsarea/opsawg', 'rtgarea', + 'rtgwg', 'saag', 'secdispatch', 'tsvarea', 'irtfopen'] + # meeting_seen_as_area TODO + ]) + self.is_bof = session_db.group.state_id == 'bof' + self.is_prg = session_db.group.type_id == 'rg' and session_db.group.state_id == 'proposed' + + self.attendees = session_db.attendees + if not self.attendees: + if self.verbosity >= 1: + print('WARNING: session {} (pk {}) has no attendees set, assuming any room fits' + .format(self.group, self.session_pk)) + self.attendees = 0 + self.requested_duration = session_db.requested_duration + + constraints_db = models.Constraint.objects.filter( + Q(source=session_db.group) | Q(source__in=session_db.joint_with_groups.all()), + meeting=meeting, + ) + + self.conflict_groups = defaultdict(int) + self.conflict_people = set() + self.conflict_people_penalty = 0 + self.time_relation = None + self.time_relation_penalty = 0 + self.wg_adjacent = None + self.wg_adjacent_penalty = 0 + self.wg_adjacent = None + self.timeranges_unavailable = set() + self.timeranges_unavailable_penalty = 0 + + self.last_cost = None + + for constraint_db in constraints_db: + if constraint_db.name.slug in ['conflict', 'conflic2', 'conflic3']: + self.conflict_groups[constraint_db.target.acronym] += constraint_db.name.penalty + elif constraint_db.name.slug == 'bethere': + self.conflict_people.add(constraint_db.person.pk) + self.conflict_people_penalty = constraint_db.name.penalty + elif constraint_db.name.slug == 'time_relation': + self.time_relation = constraint_db.time_relation + self.time_relation_penalty = constraint_db.name.penalty + elif constraint_db.name.slug == 'wg_adjacent': + self.wg_adjacent = constraint_db.target.acronym + self.wg_adjacent_penalty = constraint_db.name.penalty + elif constraint_db.name.slug == 'timerange': + self.timeranges_unavailable.update({t.slug for t in constraint_db.timeranges.all()}) + self.timeranges_unavailable_penalty = constraint_db.name.penalty + else: + f = 'Unknown constraint type {} for {}' + raise ValueError(f.format(constraint_db.name.slug, self.group)) + + self.complexity = sum([ + self.attendees, + sum(self.conflict_groups.values()), + (self.conflict_people_penalty * len(self.conflict_people)), + self.time_relation_penalty, + self.wg_adjacent_penalty * 1000, + self.timeranges_unavailable_penalty * len(self.timeranges_unavailable), + self.requested_duration.seconds * 100, + ]) + + def update_complexity(self, other_sessions): + """ + Update the complexity of this session, based on all other sessions. + This should be called after all Session objects are created, and + updates the complexity of this session based on how many conflicts + other sessions may have with this session + """ + for other_session in other_sessions: + self.complexity += sum([ + sum([cost for group, cost in other_session.conflict_groups.items() if + group == self.group]), + self.conflict_people_penalty * len( + self.conflict_people.intersection(other_session.conflict_people)) + ]) + + def fits_in_timeslot(self, timeslot): + # return self.attendees <= timeslot.capacity and self.requested_duration <= timeslot.duration + return self.attendees <= timeslot.capacity and timeslot.time_group not in self.timeranges_unavailable and self.requested_duration <= timeslot.duration + + + def calculate_cost(self, schedule, my_timeslot, overlapping_sessions, my_sessions): + """ + Calculate the cost of this session, in the provided schedule, with this session + being in my_timeslot, and a given set of overlapping sessions and the set of + all sessions of this group. + The functionality is split into a few methods, to optimise caching. + + overlapping_sessions is a list of Session objects + my_sessions is an iterable of tuples, each tuple containing a TimeSlot and a Session + + The return value is a tuple of violations (list of strings) and a cost (integer). + """ + violations, cost = [], 0 + overlapping_sessions = tuple(overlapping_sessions) + + if my_timeslot.time_group in self.timeranges_unavailable: + violations.append('{}: scheduled in unavailable timerange {}' + .format(self.group, my_timeslot.time_group)) + cost += self.timeranges_unavailable_penalty + + v, c = self._calculate_cost_overlapping_groups(overlapping_sessions) + violations += v + cost += c + + v, c = self._calculate_cost_business_logic(overlapping_sessions) + violations += v + cost += c + + v, c = self._calculate_cost_my_other_sessions(tuple(my_sessions)) + violations += v + cost += c + + if self.wg_adjacent: + adjacent_groups = tuple([schedule[t].group for t in my_timeslot.adjacent if t in schedule]) + if self.wg_adjacent not in adjacent_groups: + violations.append('{}: missing adjacency with {}, adjacents are: {}' + .format(self.group, self.wg_adjacent, ', '.join(adjacent_groups))) + cost += self.wg_adjacent_penalty + + self.last_cost = cost + return violations, cost + + @functools.lru_cache(maxsize=10000) + def _calculate_cost_overlapping_groups(self, overlapping_sessions): + violations, cost = [], 0 + for other in overlapping_sessions: + if not other: + continue + if other.group == self.group: + violations.append('{}: scheduled twice in overlapping slots'.format(self.group)) + cost += math.inf + if other.group in self.conflict_groups: + violations.append('{}: group conflict with {}'.format(self.group, other.group)) + cost += self.conflict_groups[other.group] + + conflict_people = self.conflict_people.intersection(other.conflict_people) + for person in conflict_people: + violations.append('{}: conflict w/ key person {}, also in {}' + .format(self.group, person, other.group)) + cost += len(conflict_people) * self.conflict_people_penalty + return violations, cost + + @functools.lru_cache(maxsize=10000) + def _calculate_cost_business_logic(self, overlapping_sessions): + violations, cost = [], 0 + for other in overlapping_sessions: + if not other: + continue + # BoFs cannot conflict with PRGs + if self.is_bof and other.is_prg: + violations.append('{}: BoF overlaps with PRG: {}' + .format(self.group, other.group)) + cost += 100000 # TODO + # BoFs cannot conflict with any other BoFs + if self.is_bof and other.is_bof: + violations.append('{}: BoF overlaps with other BoF: {}' + .format(self.group, other.group)) + cost += 100000 # TODO + # BoFs cannot conflict with any other WGs in their area + if self.is_bof and self.parent == other.parent: + violations.append('{}: BoF overlaps with other session from same area: {}' + .format(self.group, other.group)) + cost += 100000 # TODO + # BoFs cannot conflict with any area-wide meetings (of any area) + if self.is_bof and other.is_area_meeting: + violations.append('{}: BoF overlaps with area meeting {}' + .format(self.group, other.group)) + cost += 100000 # TODO + # Area meetings cannot conflict with anything else in their area + if self.is_area_meeting and other.parent == self.group: + violations.append('{}: area meeting overlaps with session from same area: {}' + .format(self.group, other.group)) + cost += 100000 # TODO + # Area meetings cannot conflict with other area meetings + if self.is_area_meeting and other.is_area_meeting: + violations.append('{}: area meeting overlaps with other area meeting: {}' + .format(self.group, other.group)) + cost += 10000 # TODO + # WGs overseen by the same Area Director should not conflict + if self.ad and self.ad == other.ad: + violations.append('{}: has same AD as {}'.format(self.group, other.group)) + cost += 1000 # TODO + return violations, cost + + # TODO: Python 2 compatibility + @functools.lru_cache(maxsize=10000) + def _calculate_cost_my_other_sessions(self, my_sessions): + violations, cost = [], 0 + my_sessions = list(my_sessions) + if len(my_sessions) >= 2: + if my_sessions != sorted(my_sessions, key=lambda i: i[1].session_pk): + session_order = [s.session_pk for t, s in my_sessions] + violations.append('{}: sessions out of order: {}'.format(self.group, session_order)) + cost += 10000 # TODO + + if self.time_relation and len(my_sessions) >= 2: + group_days = [t.start.date() for t, s in my_sessions] + difference_days = abs((group_days[1] - group_days[0]).days) + if self.time_relation == 'subsequent-days' and difference_days != 1: + violations.append('{}: has time relation subsequent-days but difference is {}' + .format(self.group, difference_days)) + cost += self.time_relation_penalty + elif self.time_relation == 'one-day-seperation' and difference_days == 1: + violations.append('{}: has time relation one-day-seperation but difference is {}' + .format(self.group, difference_days)) + cost += self.time_relation_penalty + return violations, cost diff --git a/ietf/meeting/test_schedule_generator.py b/ietf/meeting/test_schedule_generator.py new file mode 100644 index 000000000..2f251d24a --- /dev/null +++ b/ietf/meeting/test_schedule_generator.py @@ -0,0 +1,124 @@ +# Copyright The IETF Trust 2020, All Rights Reserved +import calendar +import datetime + +from ietf.utils.test_utils import TestCase +from ietf.group.factories import GroupFactory, RoleFactory +from ietf.person.factories import PersonFactory +from ietf.meeting.models import Session, Constraint, TimerangeName +from ietf.meeting.factories import MeetingFactory, RoomFactory, TimeSlotFactory, SessionFactory +from ietf.meeting.management.commands.schedule_generator import ScheduleHandler + + +class ScheduleGeneratorTest(TestCase): + def setUp(self): + # Create a meeting of 2 days, 4 sessions per day, in 2 rooms. + # Two rooms is a fairly low level of simultaneous schedules, this is needed + # because the schedule in these tests is much more complex than a real schedule. + self.meeting = MeetingFactory(type_id='ietf', days=2) + self.rooms = [ + RoomFactory(meeting=self.meeting, capacity=100), + RoomFactory(meeting=self.meeting, capacity=10) + ] + + self.timeslots = [] + for room in self.rooms: + for day in range(0, 2): + for hour in range(12, 16): + t = TimeSlotFactory( + meeting=self.meeting, + location=room, + time=datetime.datetime.combine( + self.meeting.date + datetime.timedelta(days=day), + datetime.time(hour, 0), + ), + duration=datetime.timedelta(minutes=60), + ) + self.timeslots.append(t) + + self.first_meeting_day = calendar.day_name[self.meeting.date.weekday()].lower() + + self.area1 = GroupFactory(acronym='area1', type_id='area') + self.area2 = GroupFactory(acronym='area2', type_id='area') + self.wg1 = GroupFactory(acronym='wg1', parent=self.area1) + self.wg2 = GroupFactory(acronym='wg2', ) + self.wg3 = GroupFactory(acronym='wg3', ) + self.bof1 = GroupFactory(acronym='bof1', parent=self.area1, state_id='bof') + self.bof2 = GroupFactory(acronym='bof2', parent=self.area2, state_id='bof') + self.prg1 = GroupFactory(acronym='prg1', parent=self.area2, type_id='rg', state_id='proposed') + + self.ad_role = RoleFactory(group=self.wg1, name_id='ad') + RoleFactory(group=self.bof1, name_id='ad', person=self.ad_role.person) + + self.person1 = PersonFactory() + + def test_normal_schedule(self): + self._create_basic_sessions() + generator = ScheduleHandler(self.meeting.number, verbosity=0) + violations, cost = generator.run() + self.assertEqual(violations, self.fixed_violations) + self.assertEqual(cost, self.fixed_cost) + + def test_unresolvable_schedule(self): + self._create_basic_sessions() + self.wg2.parent = self.area1 + self.wg2.save() + self.bof2.parent = self.area1 + self.bof2.save() + + generator = ScheduleHandler(self.meeting.number, verbosity=0) + violations, cost = generator.run() + self.assertNotEqual(violations, []) + self.assertEqual(cost, 100000 + self.fixed_cost) + + def test_random_initial_schedule(self): + self._create_basic_sessions() + generator = ScheduleHandler(self.meeting.number, verbosity=0) + generator.schedule.initial_random = True + violations, cost = generator.run() + self.assertEqual(violations, self.fixed_violations) + self.assertEqual(cost, self.fixed_cost) + + def test_too_many_sessions(self): + self._create_basic_sessions() + self._create_basic_sessions() + with self.assertRaises(ValueError): + generator = ScheduleHandler(self.meeting.number, verbosity=0) + generator.run() + + def _create_basic_sessions(self): + for group in [self.area1, self.area2, self.wg1, self.wg2, self.wg3, self.bof1, + self.bof2, self.prg1]: + SessionFactory(meeting=self.meeting, group=group, add_to_schedule=False, attendees=5, + requested_duration=datetime.timedelta(hours=1)) + for group in self.bof1, self.bof2, self.wg2, self.prg1: + SessionFactory(meeting=self.meeting, group=group, add_to_schedule=False, attendees=55, + requested_duration=datetime.timedelta(hours=1)) + SessionFactory(meeting=self.meeting, group=self.wg2, add_to_schedule=False, attendees=500, + requested_duration=datetime.timedelta(hours=2)) + + joint_session = SessionFactory(meeting=self.meeting, group=self.wg2, add_to_schedule=False) + joint_session.joint_with_groups.add(self.wg3) + + Constraint.objects.create(meeting=self.meeting, source=self.wg1, + name_id='wg_adjacent', target=self.area1) + Constraint.objects.create(meeting=self.meeting, source=self.wg2, + name_id='conflict', target=self.bof1) + Constraint.objects.create(meeting=self.meeting, source=self.bof1, + name_id='bethere', person=self.person1) + Constraint.objects.create(meeting=self.meeting, source=self.wg2, + name_id='bethere', person=self.person1) + Constraint.objects.create(meeting=self.meeting, source=self.bof1, + name_id='time_relation', time_relation='subsequent-days') + Constraint.objects.create(meeting=self.meeting, source=self.bof2, + name_id='time_relation', time_relation='one-day-separation') + + timerange_c1 = Constraint.objects.create(meeting=self.meeting, source=self.wg2, + name_id='timerange') + timerange_c1.timeranges.set(TimerangeName.objects.filter(slug__startswith=self.first_meeting_day)) + + self.fixed_violations = ['No timeslot with sufficient duration available for wg2, ' + 'requested 2:00:00, trimmed to 1:00:00', + 'No timeslot with sufficient capacity available for wg2, ' + 'requested 500, trimmed to 100'] + self.fixed_cost = 10000 + 10000 From cbcb5a2bd218eec8f8608f0a7176a099d29fa50a Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Thu, 28 May 2020 19:47:30 +0000 Subject: [PATCH 02/17] Add schedule saving to automatic scheduler, various cleanups - Legacy-Id: 17893 --- .../management/commands/schedule_generator.py | 123 +++++++++++------- ietf/meeting/test_schedule_generator.py | 48 ++++--- 2 files changed, 107 insertions(+), 64 deletions(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index 12f902a83..da5694016 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -5,11 +5,14 @@ import calendar import datetime import math import random +import string import functools from collections import defaultdict from django.core.management.base import BaseCommand, CommandError from django.db.models import Q + +from ietf.person.models import Person from ietf.meeting import models OPTIMISER_MAX_CYCLES = 100 @@ -23,37 +26,54 @@ class Command(BaseCommand): help='Number of the meeting to generate schedule for') def handle(self, meeting, verbosity, *args, **kwargs): - ScheduleHandler(meeting, verbosity).run() + ScheduleHandler(self.stdout, meeting, verbosity).run() class ScheduleHandler(object): - def __init__(self, meeting_number, verbosity): + def __init__(self, stdout, meeting_number, verbosity): + self.stdout = stdout self.verbosity = verbosity - self.meeting = models.Meeting.objects.get(number=meeting_number) - self.load_meeting() + try: + self.meeting = models.Meeting.objects.get(number=meeting_number) + except models.Meeting.DoesNotExist: + raise CommandError('Unknown meeting number {}'.format(meeting_number)) + self._load_meeting() def run(self): """Schedule all sessions""" self.schedule.fill_initial_schedule() violations, cost = self.schedule.total_schedule_cost() if self.verbosity >= 1: - print('Initial schedule completed with {} violations, total cost {}' - .format(len(violations), cost)) + self.stdout.write('Initial schedule completed with {} violations, total cost {}' + .format(len(violations), cost)) self.schedule.optimise_schedule() violations, cost = self.schedule.total_schedule_cost() if self.verbosity >= 1: - print('Optimisation completed with {} violations, total cost {}' - .format(len(violations), cost)) + self.stdout.write('Optimisation completed with {} violations, total cost {}' + .format(len(violations), cost)) if self.verbosity >= 1 and violations: - print('Remaining violations:') + self.stdout.write('Remaining violations:') for v in violations: - print(v) + self.stdout.write(v) - # TODO: actually save the schedule + self._save_schedule(cost) return violations, cost - - def load_meeting(self): + + def _save_schedule(self, cost): + name = 'Auto-' + ''.join(random.choice(string.ascii_uppercase) for i in range(10)) + schedule_db = models.Schedule.objects.create( + meeting=self.meeting, + name=name, + owner=Person.objects.get(name='(System)'), + public=True, + visible=True, + badness=cost, + ) + self.schedule.save_assignments(schedule_db) + self.stdout.write('Scheduled saved as {}'.format(name)) + + def _load_meeting(self): """Load all timeslots and sessions into in-memory objects.""" # TODO: ensure these filters are correct timeslots_db = models.TimeSlot.objects.filter( @@ -71,13 +91,13 @@ class ScheduleHandler(object): schedulingevent__status_id='schedw', ).select_related('group') - sessions = {Session(self.meeting, s, self.verbosity) for s in sessions_db} + sessions = {Session(self.stdout, self.meeting, s, self.verbosity) for s in sessions_db} for session in sessions: # The complexity of a session also depends on how many # sessions have declared a conflict towards this session. session.update_complexity(sessions) - self.schedule = Schedule(timeslots, sessions, self.verbosity) + self.schedule = Schedule(self.stdout, timeslots, sessions, self.verbosity) self.schedule.adjust_for_timeslot_availability() @@ -87,17 +107,26 @@ class Schedule(object): The schedule is internally represented as a dict, timeslots being keys, sessions being values. Note that "timeslot" means the combination of a timeframe and a location. """ - def __init__(self, timeslots, sessions, verbosity): + def __init__(self, stdout, timeslots, sessions, verbosity): + self.stdout = stdout self.timeslots = timeslots self.sessions = sessions self.verbosity = verbosity self.schedule = dict() self.best_cost = math.inf self.best_schedule = None - self.initial_random = False self.fixed_cost = 0 self.fixed_violations = [] + def save_assignments(self, schedule_db): + for timeslot, session in self.schedule.items(): + models.SchedTimeSessAssignment.objects.create( + timeslot_id=timeslot.timeslot_pk, + session_id=session.session_pk, + schedule=schedule_db, + badness=session.last_cost, + ) + def adjust_for_timeslot_availability(self): """ Check the number of sessions, their required capacity and duration against availability. @@ -105,8 +134,8 @@ class Schedule(object): If sessions can't fit, they are trimmed, and a fixed cost is applied. """ if len(self.sessions) > len(self.timeslots): - raise ValueError('More sessions ({}) than timeslots ({})' - .format(len(self.sessions), len(self.timeslots))) + raise CommandError('More sessions ({}) than timeslots ({})' + .format(len(self.sessions), len(self.timeslots))) def make_capacity_adjustments(t_attr, s_attr): availables = [getattr(timeslot, t_attr) for timeslot in self.timeslots] @@ -180,13 +209,10 @@ class Schedule(object): - Second: shortest duration that still fits - Third: smallest room that still fits If there are multiple options with equal value, a random one is picked. - - If self.initial_random is set, each session is assigned to a completely random - timeslot, that still fits. This is used in testing. """ if self.verbosity >= 2: - print('== Initial scheduler starting, scheduling {} sessions in {} timeslots =='. - format(len(self.sessions), len(self.timeslots))) + self.stdout.write('== Initial scheduler starting, scheduling {} sessions in {} timeslots ==' + .format(len(self.sessions), len(self.timeslots))) sessions = sorted(self.sessions, key=lambda s: s.complexity, reverse=True) for session in sessions: @@ -204,12 +230,11 @@ class Schedule(object): return self.calculate_dynamic_cost(proposed_schedule)[1], t.duration, t.capacity possible_slots.sort(key=timeslot_preference) - if self.initial_random: - random.shuffle(possible_slots) self._schedule_session(session, possible_slots[0]) if self.verbosity >= 3: - print('Scheduled {} at {} in location {}' - .format(session.group, possible_slots[0].start, possible_slots[0].location_pk)) + self.stdout.write('Scheduled {} at {} in location {}' + .format(session.group, possible_slots[0].start, + possible_slots[0].location_pk)) def optimise_schedule(self): """ @@ -237,9 +262,10 @@ class Schedule(object): random.shuffle(items) if self.verbosity >= 2: - print('== Optimiser starting run {}, dynamic cost after last optimiser run {} ==' - .format(run_count, last_run_cost)) - print('Dynamic violations in last optimiser run: {}'.format(last_run_violations)) + self.stdout.write('== Optimiser starting run {}, dynamic cost after last run {} ==' + .format(run_count, last_run_cost)) + self.stdout.write('Dynamic violations in last optimiser run: {}' + .format(last_run_violations)) if shuffle_next_run: shuffle_next_run = False last_run_cost = None # After a shuffle, attempt at least two regular runs @@ -249,7 +275,7 @@ class Schedule(object): best_cost = self.calculate_dynamic_cost()[1] if best_cost == 0: if self.verbosity >= 2: - print('Optimiser found an optimal schedule') + self.stdout.write('Optimiser found an optimal schedule') return best_timeslot = None @@ -263,8 +289,8 @@ class Schedule(object): switched_with = self._switch_sessions(original_timeslot, best_timeslot) switched_with = switched_with.group if switched_with else '' if self.verbosity >= 3: - print('Found cost reduction to {} by switching {} with {}' - .format(best_cost, session.group, switched_with)) + self.stdout.write('Found cost reduction to {} by switching {} with {}' + .format(best_cost, session.group, switched_with)) if last_run_cost == best_cost: shuffle_next_run = True @@ -272,8 +298,8 @@ class Schedule(object): self._save_schedule() if self.verbosity >= 2: - print('Optimiser did not find an optimal schedule, using best schedule at dynamic cost {}' - .format(self.best_cost)) + self.stdout.write('Optimiser did not find perfect schedule, using best schedule at dynamic cost {}' + .format(self.best_cost)) self.schedule = self.best_schedule def _shuffle_conflicted_sessions(self, items): @@ -286,8 +312,8 @@ class Schedule(object): to_reschedule = [(t, s) for t, s in items if s.last_cost] random.shuffle(to_reschedule) if self.verbosity >= 2: - print('Optimiser has no more improvements, shuffling sessions {}' - .format(', '.join([s.group for t, s in to_reschedule]))) + self.stdout.write('Optimiser has no more improvements, shuffling sessions {}' + .format(', '.join([s.group for t, s in to_reschedule]))) for original_timeslot, rescheduling_session in to_reschedule: possible_new_slots = list(self.timeslots) @@ -299,8 +325,8 @@ class Schedule(object): if switched_with is not False: switched_group = switched_with.group if switched_with else '' if self.verbosity >= 3: - print('Shuffled {} to random new slot, previously in slot was {}' - .format(rescheduling_session.group, switched_group)) + self.stdout.write('Moved {} to random new slot, previously in slot was {}' + .format(rescheduling_session.group, switched_group)) break def _schedule_session(self, session, timeslot): @@ -378,9 +404,9 @@ class TimeSlot(object): if self.start.time() < datetime.time(12, 30): self.time_of_day = 'morning' elif self.start.time() < datetime.time(15, 30): - self.time_of_day = 'early-afternoon' + self.time_of_day = 'afternoon-early' else: - self.time_of_day = 'late-afternoon' + self.time_of_day = 'afternoon-late' self.time_group = self.day + '-' + self.time_of_day self.overlaps = set() self.adjacent = set() @@ -411,12 +437,13 @@ class Session(object): i.e. it represents a single session to be scheduled. It also pulls in data about constraints, group parents, etc. """ - def __init__(self, meeting, session_db, verbosity): + def __init__(self, stdout, meeting, session_db, verbosity): """ Initialise this object from a Session model instance. This includes collecting all constraints from the database, and calculating an initial complexity. """ + self.stdout = stdout self.verbosity = verbosity self.session_pk = session_db.pk self.group = session_db.group.acronym @@ -435,8 +462,8 @@ class Session(object): self.attendees = session_db.attendees if not self.attendees: if self.verbosity >= 1: - print('WARNING: session {} (pk {}) has no attendees set, assuming any room fits' - .format(self.group, self.session_pk)) + self.stdout.write('WARNING: session {} (pk {}) has no attendees set, assuming any room fits' + .format(self.group, self.session_pk)) self.attendees = 0 self.requested_duration = session_db.requested_duration @@ -475,7 +502,7 @@ class Session(object): self.timeranges_unavailable_penalty = constraint_db.name.penalty else: f = 'Unknown constraint type {} for {}' - raise ValueError(f.format(constraint_db.name.slug, self.group)) + raise CommandError(f.format(constraint_db.name.slug, self.group)) self.complexity = sum([ self.attendees, @@ -503,9 +530,7 @@ class Session(object): ]) def fits_in_timeslot(self, timeslot): - # return self.attendees <= timeslot.capacity and self.requested_duration <= timeslot.duration - return self.attendees <= timeslot.capacity and timeslot.time_group not in self.timeranges_unavailable and self.requested_duration <= timeslot.duration - + return self.attendees <= timeslot.capacity and self.requested_duration <= timeslot.duration def calculate_cost(self, schedule, my_timeslot, overlapping_sessions, my_sessions): """ diff --git a/ietf/meeting/test_schedule_generator.py b/ietf/meeting/test_schedule_generator.py index 2f251d24a..9315cedb4 100644 --- a/ietf/meeting/test_schedule_generator.py +++ b/ietf/meeting/test_schedule_generator.py @@ -1,6 +1,9 @@ # Copyright The IETF Trust 2020, All Rights Reserved import calendar import datetime +from io import StringIO + +from django.core.management.base import CommandError from ietf.utils.test_utils import TestCase from ietf.group.factories import GroupFactory, RoleFactory @@ -53,45 +56,60 @@ class ScheduleGeneratorTest(TestCase): self.person1 = PersonFactory() def test_normal_schedule(self): + stdout = StringIO() self._create_basic_sessions() - generator = ScheduleHandler(self.meeting.number, verbosity=0) + generator = ScheduleHandler(stdout, self.meeting.number, verbosity=3) violations, cost = generator.run() self.assertEqual(violations, self.fixed_violations) self.assertEqual(cost, self.fixed_cost) + stdout.seek(0) + output = stdout.read() + self.assertIn('WARNING: session wg2 (pk 13) has no attendees set', output) + self.assertIn('scheduling 13 sessions in 16 timeslots', output) + self.assertIn('Optimiser starting run 0', output) + self.assertIn('Optimiser found an optimal schedule', output) + + schedule = self.meeting.schedule_set.get(name__startswith='Auto-') + self.assertEqual(schedule.assignments.count(), 13) + def test_unresolvable_schedule(self): + stdout = StringIO() self._create_basic_sessions() self.wg2.parent = self.area1 self.wg2.save() self.bof2.parent = self.area1 self.bof2.save() - generator = ScheduleHandler(self.meeting.number, verbosity=0) + generator = ScheduleHandler(stdout, self.meeting.number, verbosity=2) violations, cost = generator.run() self.assertNotEqual(violations, []) - self.assertEqual(cost, 100000 + self.fixed_cost) + self.assertGreater(cost, self.fixed_cost) + + stdout.seek(0) + output = stdout.read() + self.assertIn('Optimiser did not find perfect schedule', output) - def test_random_initial_schedule(self): - self._create_basic_sessions() - generator = ScheduleHandler(self.meeting.number, verbosity=0) - generator.schedule.initial_random = True - violations, cost = generator.run() - self.assertEqual(violations, self.fixed_violations) - self.assertEqual(cost, self.fixed_cost) - def test_too_many_sessions(self): + stdout = StringIO() self._create_basic_sessions() self._create_basic_sessions() - with self.assertRaises(ValueError): - generator = ScheduleHandler(self.meeting.number, verbosity=0) + with self.assertRaises(CommandError): + generator = ScheduleHandler(stdout, self.meeting.number, verbosity=0) generator.run() - + + def test_invalid_meeting_number(self): + stdout = StringIO() + with self.assertRaises(CommandError): + generator = ScheduleHandler(stdout, 'not-valid-meeting-number-aaaa', verbosity=0) + generator.run() + def _create_basic_sessions(self): for group in [self.area1, self.area2, self.wg1, self.wg2, self.wg3, self.bof1, self.bof2, self.prg1]: SessionFactory(meeting=self.meeting, group=group, add_to_schedule=False, attendees=5, requested_duration=datetime.timedelta(hours=1)) - for group in self.bof1, self.bof2, self.wg2, self.prg1: + for group in self.bof1, self.bof2, self.wg2: SessionFactory(meeting=self.meeting, group=group, add_to_schedule=False, attendees=55, requested_duration=datetime.timedelta(hours=1)) SessionFactory(meeting=self.meeting, group=self.wg2, add_to_schedule=False, attendees=500, From c8e0a83b47cf737ce5febccd2ada780432e5ec22 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Fri, 29 May 2020 12:03:50 +0000 Subject: [PATCH 03/17] Add new model for business logic meeting constraint costs, update existing constraint costs, and small improvements in tests - Legacy-Id: 17894 --- .../management/commands/schedule_generator.py | 35 +++-- .../migrations/0028_businessconstraint.py | 93 ++++++++++++ ietf/meeting/models.py | 15 ++ ietf/meeting/test_schedule_generator.py | 27 ++-- ietf/name/fixtures/names.json | 138 +++++++++++++++++- 5 files changed, 279 insertions(+), 29 deletions(-) create mode 100644 ietf/meeting/migrations/0028_businessconstraint.py diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index da5694016..52bbf19e6 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -75,6 +75,11 @@ class ScheduleHandler(object): def _load_meeting(self): """Load all timeslots and sessions into in-memory objects.""" + business_constraint_costs = { + bc.slug: bc.penalty + for bc in models.BusinessConstraint.objects.all() + } + # TODO: ensure these filters are correct timeslots_db = models.TimeSlot.objects.filter( meeting=self.meeting, @@ -91,13 +96,15 @@ class ScheduleHandler(object): schedulingevent__status_id='schedw', ).select_related('group') - sessions = {Session(self.stdout, self.meeting, s, self.verbosity) for s in sessions_db} + sessions = {Session(self.stdout, self.meeting, s, business_constraint_costs, self.verbosity) + for s in sessions_db} for session in sessions: # The complexity of a session also depends on how many # sessions have declared a conflict towards this session. session.update_complexity(sessions) - self.schedule = Schedule(self.stdout, timeslots, sessions, self.verbosity) + self.schedule = Schedule( + self.stdout, timeslots, sessions, business_constraint_costs, self.verbosity) self.schedule.adjust_for_timeslot_availability() @@ -107,10 +114,11 @@ class Schedule(object): The schedule is internally represented as a dict, timeslots being keys, sessions being values. Note that "timeslot" means the combination of a timeframe and a location. """ - def __init__(self, stdout, timeslots, sessions, verbosity): + def __init__(self, stdout, timeslots, sessions, business_constraint_costs, verbosity): self.stdout = stdout self.timeslots = timeslots self.sessions = sessions + self.business_constraint_costs = business_constraint_costs self.verbosity = verbosity self.schedule = dict() self.best_cost = math.inf @@ -154,7 +162,7 @@ class Schedule(object): msg = f.format(t_attr, session.group, getattr(session, s_attr), largest_available) setattr(session, s_attr, largest_available) availables.pop(-1) - self.fixed_cost += 10000 # TODO + self.fixed_cost += self.business_constraint_costs['session_requires_trim'] self.fixed_violations.append(msg) make_capacity_adjustments('duration', 'requested_duration') @@ -437,7 +445,7 @@ class Session(object): i.e. it represents a single session to be scheduled. It also pulls in data about constraints, group parents, etc. """ - def __init__(self, stdout, meeting, session_db, verbosity): + def __init__(self, stdout, meeting, session_db, business_constraint_costs, verbosity): """ Initialise this object from a Session model instance. This includes collecting all constraints from the database, @@ -445,6 +453,7 @@ class Session(object): """ self.stdout = stdout self.verbosity = verbosity + self.business_constraint_costs = business_constraint_costs self.session_pk = session_db.pk self.group = session_db.group.acronym self.parent = session_db.group.parent.acronym if session_db.group.parent else None @@ -604,36 +613,36 @@ class Session(object): if self.is_bof and other.is_prg: violations.append('{}: BoF overlaps with PRG: {}' .format(self.group, other.group)) - cost += 100000 # TODO + cost += self.business_constraint_costs['bof_overlapping_prg'] # BoFs cannot conflict with any other BoFs if self.is_bof and other.is_bof: violations.append('{}: BoF overlaps with other BoF: {}' .format(self.group, other.group)) - cost += 100000 # TODO + cost += self.business_constraint_costs['bof_overlapping_bof'] # BoFs cannot conflict with any other WGs in their area if self.is_bof and self.parent == other.parent: violations.append('{}: BoF overlaps with other session from same area: {}' .format(self.group, other.group)) - cost += 100000 # TODO + cost += self.business_constraint_costs['bof_overlapping_area_wg'] # BoFs cannot conflict with any area-wide meetings (of any area) if self.is_bof and other.is_area_meeting: violations.append('{}: BoF overlaps with area meeting {}' .format(self.group, other.group)) - cost += 100000 # TODO + cost += self.business_constraint_costs['bof_overlapping_area_meeting'] # Area meetings cannot conflict with anything else in their area if self.is_area_meeting and other.parent == self.group: violations.append('{}: area meeting overlaps with session from same area: {}' .format(self.group, other.group)) - cost += 100000 # TODO + cost += self.business_constraint_costs['area_overlapping_in_area'] # Area meetings cannot conflict with other area meetings if self.is_area_meeting and other.is_area_meeting: violations.append('{}: area meeting overlaps with other area meeting: {}' .format(self.group, other.group)) - cost += 10000 # TODO + cost += self.business_constraint_costs['area_overlapping_other_area'] # WGs overseen by the same Area Director should not conflict if self.ad and self.ad == other.ad: violations.append('{}: has same AD as {}'.format(self.group, other.group)) - cost += 1000 # TODO + cost += self.business_constraint_costs['session_overlap_ad'] return violations, cost # TODO: Python 2 compatibility @@ -645,7 +654,7 @@ class Session(object): if my_sessions != sorted(my_sessions, key=lambda i: i[1].session_pk): session_order = [s.session_pk for t, s in my_sessions] violations.append('{}: sessions out of order: {}'.format(self.group, session_order)) - cost += 10000 # TODO + cost += self.business_constraint_costs['sessions_out_of_order'] if self.time_relation and len(my_sessions) >= 2: group_days = [t.start.date() for t, s in my_sessions] diff --git a/ietf/meeting/migrations/0028_businessconstraint.py b/ietf/meeting/migrations/0028_businessconstraint.py new file mode 100644 index 000000000..c5ccb354f --- /dev/null +++ b/ietf/meeting/migrations/0028_businessconstraint.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-05-29 02:52 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def forward(apps, schema_editor): + BusinessConstraint = apps.get_model("meeting", "BusinessConstraint") + BusinessConstraint.objects.create( + slug="bof_overlapping_prg", + name="BoFs cannot conflict with PRGs", + penalty=100000, + ) + BusinessConstraint.objects.create( + slug="bof_overlapping_bof", + name="BoFs cannot conflict with any other BoFs", + penalty=100000, + ) + BusinessConstraint.objects.create( + slug="bof_overlapping_area_wg", + name="BoFs cannot conflict with any other WGs in their area", + penalty=100000, + ) + BusinessConstraint.objects.create( + slug="bof_overlapping_area_meeting", + name="BoFs cannot conflict with any area-wide meetings (of any area)", + penalty=10000, + ) + BusinessConstraint.objects.create( + slug="area_overlapping_in_area", + name="Area meetings cannot conflict with anything else in their area", + penalty=10000, + ) + BusinessConstraint.objects.create( + slug="area_overlapping_other_area", + name="Area meetings cannot conflict with other area meetings", + penalty=1000, + ) + BusinessConstraint.objects.create( + slug="session_overlap_ad", + name="WGs overseen by the same Area Director should not conflict", + penalty=100, + ) + BusinessConstraint.objects.create( + slug="sessions_out_of_order", + name="Sessions should be scheduled in requested order", + penalty=100000, + ) + BusinessConstraint.objects.create( + slug="session_requires_trim", + name="Sessions should be scheduled according to requested duration and attendees", + penalty=100000, + ) + + ConstraintName = apps.get_model("name", "ConstraintName") + ConstraintName.objects.filter(slug='conflict').update(penalty=100000) + ConstraintName.objects.filter(slug='conflic2').update(penalty=10000) + ConstraintName.objects.filter(slug='conflic3').update(penalty=100000) + ConstraintName.objects.filter(slug='bethere').update(penalty=10000) + ConstraintName.objects.filter(slug='timerange').update(penalty=10000) + ConstraintName.objects.filter(slug='time_relation').update(penalty=1000) + ConstraintName.objects.filter(slug='wg_adjacent').update(penalty=1000) + + +def reverse(apps, schema_editor): + ConstraintName = apps.get_model("name", "ConstraintName") + ConstraintName.objects.filter(slug='conflict').update(penalty=100000) + ConstraintName.objects.filter(slug='conflic2').update(penalty=10000) + ConstraintName.objects.filter(slug='conflic3').update(penalty=1000) + ConstraintName.objects.filter(slug='bethere').update(penalty=200000) + ConstraintName.objects.filter(slug='timerange').update(penalty=100000) + ConstraintName.objects.filter(slug='time_relation').update(penalty=100000) + ConstraintName.objects.filter(slug='wg_adjacent').update(penalty=100000) + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0027_add_constraint_options_and_joint_groups'), + ] + + operations = [ + migrations.CreateModel( + name='BusinessConstraint', + fields=[ + ('slug', models.CharField(max_length=32, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255)), + ('penalty', models.IntegerField(default=0, help_text='The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)')), + ], + ), + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index e7ebbe324..6322792ef 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -816,9 +816,23 @@ class SchedTimeSessAssignment(models.Model): return "-".join(components).lower() + +class BusinessConstraint(models.Model): + """ + Constraints on the scheduling that apply across all qualifying + sessions in all meetings. Used by the ScheduleGenerator, which + expects a single BusinessConstraint to exist in the database. + """ + slug = models.CharField(max_length=32, primary_key=True) + name = models.CharField(max_length=255) + penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)") + + class Constraint(models.Model): """ Specifies a constraint on the scheduling. + These constraints apply to a specific group during a specific meeting. + Available types are: - conflict/conflic2/conflic3: a conflict between source and target WG/session, with varying priority. The first is used for a chair conflict, the second for @@ -828,6 +842,7 @@ class Constraint(models.Model): - time_relation: preference for a time difference between sessions - wg_adjacent: request for source WG to be adjacent (directly before or after, no breaks, same room) the target WG + """ TIME_RELATION_CHOICES = ( ('subsequent-days', 'Schedule the sessions on subsequent days'), diff --git a/ietf/meeting/test_schedule_generator.py b/ietf/meeting/test_schedule_generator.py index 9315cedb4..7f7a358d5 100644 --- a/ietf/meeting/test_schedule_generator.py +++ b/ietf/meeting/test_schedule_generator.py @@ -8,14 +8,14 @@ from django.core.management.base import CommandError from ietf.utils.test_utils import TestCase from ietf.group.factories import GroupFactory, RoleFactory from ietf.person.factories import PersonFactory -from ietf.meeting.models import Session, Constraint, TimerangeName +from ietf.meeting.models import Session, Constraint, TimerangeName, BusinessConstraint from ietf.meeting.factories import MeetingFactory, RoomFactory, TimeSlotFactory, SessionFactory from ietf.meeting.management.commands.schedule_generator import ScheduleHandler class ScheduleGeneratorTest(TestCase): def setUp(self): - # Create a meeting of 2 days, 4 sessions per day, in 2 rooms. + # Create a meeting of 2 days, 5 sessions per day, in 2 rooms. # Two rooms is a fairly low level of simultaneous schedules, this is needed # because the schedule in these tests is much more complex than a real schedule. self.meeting = MeetingFactory(type_id='ietf', days=2) @@ -27,7 +27,7 @@ class ScheduleGeneratorTest(TestCase): self.timeslots = [] for room in self.rooms: for day in range(0, 2): - for hour in range(12, 16): + for hour in range(12, 17): t = TimeSlotFactory( meeting=self.meeting, location=room, @@ -49,6 +49,8 @@ class ScheduleGeneratorTest(TestCase): self.bof1 = GroupFactory(acronym='bof1', parent=self.area1, state_id='bof') self.bof2 = GroupFactory(acronym='bof2', parent=self.area2, state_id='bof') self.prg1 = GroupFactory(acronym='prg1', parent=self.area2, type_id='rg', state_id='proposed') + self.all_groups = [self.area1, self.area2, self.wg1, self.wg2, self.wg3, self.bof1, + self.bof2, self.prg1] self.ad_role = RoleFactory(group=self.wg1, name_id='ad') RoleFactory(group=self.bof1, name_id='ad', person=self.ad_role.person) @@ -66,7 +68,7 @@ class ScheduleGeneratorTest(TestCase): stdout.seek(0) output = stdout.read() self.assertIn('WARNING: session wg2 (pk 13) has no attendees set', output) - self.assertIn('scheduling 13 sessions in 16 timeslots', output) + self.assertIn('scheduling 13 sessions in 20 timeslots', output) self.assertIn('Optimiser starting run 0', output) self.assertIn('Optimiser found an optimal schedule', output) @@ -76,10 +78,14 @@ class ScheduleGeneratorTest(TestCase): def test_unresolvable_schedule(self): stdout = StringIO() self._create_basic_sessions() - self.wg2.parent = self.area1 - self.wg2.save() - self.bof2.parent = self.area1 - self.bof2.save() + for group in self.all_groups: + group.parent = self.area1 + group.ad = self.ad_role + group.save() + c = Constraint.objects.create(meeting=self.meeting, source=group, name_id='timerange') + c.timeranges.set(TimerangeName.objects.filter(slug__startswith=self.first_meeting_day)) + Constraint.objects.create(meeting=self.meeting, source=group, + name_id='bethere', person=self.person1) generator = ScheduleHandler(stdout, self.meeting.number, verbosity=2) violations, cost = generator.run() @@ -105,8 +111,7 @@ class ScheduleGeneratorTest(TestCase): generator.run() def _create_basic_sessions(self): - for group in [self.area1, self.area2, self.wg1, self.wg2, self.wg3, self.bof1, - self.bof2, self.prg1]: + for group in self.all_groups: SessionFactory(meeting=self.meeting, group=group, add_to_schedule=False, attendees=5, requested_duration=datetime.timedelta(hours=1)) for group in self.bof1, self.bof2, self.wg2: @@ -139,4 +144,4 @@ class ScheduleGeneratorTest(TestCase): 'requested 2:00:00, trimmed to 1:00:00', 'No timeslot with sufficient capacity available for wg2, ' 'requested 500, trimmed to 100'] - self.fixed_cost = 10000 + 10000 + self.fixed_cost = BusinessConstraint.objects.get(slug='session_requires_trim').penalty * 2 diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index d5aa737f6..c04bf3391 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -5570,7 +5570,7 @@ "desc": "", "name": "Person must be present", "order": 0, - "penalty": 200000, + "penalty": 10000, "used": true }, "model": "name.constraintname", @@ -5592,7 +5592,7 @@ "desc": "", "name": "Conflicts with (tertiary)", "order": 0, - "penalty": 1000, + "penalty": 100000, "used": true }, "model": "name.constraintname", @@ -5614,7 +5614,7 @@ "desc": "", "name": "Preference for time between sessions", "order": 0, - "penalty": 100000, + "penalty": 1000, "used": true }, "model": "name.constraintname", @@ -5625,7 +5625,7 @@ "desc": "", "name": "Can't meet within timerange", "order": 0, - "penalty": 100000, + "penalty": 10000, "used": true }, "model": "name.constraintname", @@ -5636,7 +5636,7 @@ "desc": "", "name": "Request for adjacent scheduling with another WG", "order": 0, - "penalty": 100000, + "penalty": 1000, "used": true }, "model": "name.constraintname", @@ -14471,5 +14471,133 @@ }, "model": "utils.versioninfo", "pk": 4 + }, +{ + "model": "meeting.businessconstraint", + "pk": "area_overlapping_in_area", + "fields": { + "name": "Area meetings cannot conflict with anything else in their area", + "penalty": 1000 } +}, +{ + "model": "meeting.businessconstraint", + "pk": "area_overlapping_other_area", + "fields": { + "name": "Area meetings cannot conflict with other area meetings", + "penalty": 500 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "bof_overlapping_area_meeting", + "fields": { + "name": "BoFs cannot conflict with any area-wide meetings (of any area)", + "penalty": 1000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "bof_overlapping_area_wg", + "fields": { + "name": "BoFs cannot conflict with any other WGs in their area", + "penalty": 10000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "bof_overlapping_bof", + "fields": { + "name": "BoFs cannot conflict with any other BoFs", + "penalty": 10000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "bof_overlapping_prg", + "fields": { + "name": "BoFs cannot conflict with PRGs", + "penalty": 10000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "session_overlap_ad", + "fields": { + "name": "WGs overseen by the same Area Director should not conflict", + "penalty": 100 + } +}, + { + "model": "meeting.businessconstraint", + "pk": "area_overlapping_in_area", + "fields": { + "name": "Area meetings cannot conflict with anything else in their area", + "penalty": 10000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "area_overlapping_other_area", + "fields": { + "name": "Area meetings cannot conflict with other area meetings", + "penalty": 1000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "bof_overlapping_area_meeting", + "fields": { + "name": "BoFs cannot conflict with any area-wide meetings (of any area)", + "penalty": 10000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "bof_overlapping_area_wg", + "fields": { + "name": "BoFs cannot conflict with any other WGs in their area", + "penalty": 100000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "bof_overlapping_bof", + "fields": { + "name": "BoFs cannot conflict with any other BoFs", + "penalty": 100000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "bof_overlapping_prg", + "fields": { + "name": "BoFs cannot conflict with PRGs", + "penalty": 100000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "sessions_out_of_order", + "fields": { + "name": "Sessions should be scheduled in requested order", + "penalty": 100000 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "session_overlap_ad", + "fields": { + "name": "WGs overseen by the same Area Director should not conflict", + "penalty": 100 + } +}, +{ + "model": "meeting.businessconstraint", + "pk": "session_requires_trim", + "fields": { + "name": "Sessions should be scheduled according to requested duration and attendees", + "penalty": 100000 + } +} ] From 1aad1bee489473b8a3b8b51b505a200ef5b9e756 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Fri, 29 May 2020 12:14:51 +0000 Subject: [PATCH 04/17] Add Python 2 compatibility for schedule builder with functools32 - Legacy-Id: 17895 --- .../management/commands/schedule_generator.py | 13 ++++++++----- requirements.txt | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index 52bbf19e6..e76612a10 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -6,9 +6,13 @@ import datetime import math import random import string -import functools from collections import defaultdict +try: + from functools import lru_cache +except ImportError: + from functools32 import lru_cache + from django.core.management.base import BaseCommand, CommandError from django.db.models import Q @@ -583,7 +587,7 @@ class Session(object): self.last_cost = cost return violations, cost - @functools.lru_cache(maxsize=10000) + @lru_cache(maxsize=10000) def _calculate_cost_overlapping_groups(self, overlapping_sessions): violations, cost = [], 0 for other in overlapping_sessions: @@ -603,7 +607,7 @@ class Session(object): cost += len(conflict_people) * self.conflict_people_penalty return violations, cost - @functools.lru_cache(maxsize=10000) + @lru_cache(maxsize=10000) def _calculate_cost_business_logic(self, overlapping_sessions): violations, cost = [], 0 for other in overlapping_sessions: @@ -645,8 +649,7 @@ class Session(object): cost += self.business_constraint_costs['session_overlap_ad'] return violations, cost - # TODO: Python 2 compatibility - @functools.lru_cache(maxsize=10000) + @lru_cache(maxsize=10000) def _calculate_cost_my_other_sessions(self, my_sessions): violations, cost = [], 0 my_sessions = list(my_sessions) diff --git a/requirements.txt b/requirements.txt index 336e60196..aadc204bd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -70,4 +70,4 @@ Unidecode>=0.4.18 xml2rfc>=2.35.0 xym>=0.4.4,!=0.4.7,<1.0 #zxcvbn-python>=4.4.14 # Not needed until we do back-end password entropy validation - +functools32==3.2.3-2; python_version < '3' \ No newline at end of file From 2f8dfe8c78ac7d6e1c79218eae3dae76dcacf161 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Fri, 29 May 2020 12:49:51 +0000 Subject: [PATCH 05/17] Removed Python 2 compatibility for schedule builder as it's obsolete - Legacy-Id: 17896 --- ietf/meeting/management/commands/schedule_generator.py | 6 +----- requirements.txt | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index e76612a10..f43dd62dc 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -7,11 +7,7 @@ import math import random import string from collections import defaultdict - -try: - from functools import lru_cache -except ImportError: - from functools32 import lru_cache +from functools import lru_cache from django.core.management.base import BaseCommand, CommandError from django.db.models import Q diff --git a/requirements.txt b/requirements.txt index aadc204bd..ee70870df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -70,4 +70,3 @@ Unidecode>=0.4.18 xml2rfc>=2.35.0 xym>=0.4.4,!=0.4.7,<1.0 #zxcvbn-python>=4.4.14 # Not needed until we do back-end password entropy validation -functools32==3.2.3-2; python_version < '3' \ No newline at end of file From 38aaa38a0b6c55eebbd2644eb14feb214c1cfe14 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 1 Jun 2020 09:19:18 +0000 Subject: [PATCH 06/17] Don't schedule sessions on Sunday in schedule builder. - Legacy-Id: 17899 --- ietf/meeting/management/commands/schedule_generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index f43dd62dc..f26a10f85 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -87,6 +87,7 @@ class ScheduleHandler(object): ).exclude(location__capacity=None).select_related('location') timeslots = {TimeSlot(t, self.verbosity) for t in timeslots_db} + timeslots = {t for t in timeslots if t.day != 'sunday'} for timeslot in timeslots: timeslot.store_relations(timeslots) @@ -522,7 +523,7 @@ class Session(object): self.timeranges_unavailable_penalty * len(self.timeranges_unavailable), self.requested_duration.seconds * 100, ]) - + def update_complexity(self, other_sessions): """ Update the complexity of this session, based on all other sessions. From 468afdcbe3bde78f07c1fdeadb21bfc1793a82a1 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 1 Jun 2020 09:22:37 +0000 Subject: [PATCH 07/17] Fix tests to account for meetings starting on a Sunday - Legacy-Id: 17900 --- ietf/meeting/test_schedule_generator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ietf/meeting/test_schedule_generator.py b/ietf/meeting/test_schedule_generator.py index 7f7a358d5..d281fb9fc 100644 --- a/ietf/meeting/test_schedule_generator.py +++ b/ietf/meeting/test_schedule_generator.py @@ -15,10 +15,11 @@ from ietf.meeting.management.commands.schedule_generator import ScheduleHandler class ScheduleGeneratorTest(TestCase): def setUp(self): - # Create a meeting of 2 days, 5 sessions per day, in 2 rooms. + # Create a meeting of 2 days, 5 sessions per day, in 2 rooms. There are 3 days + # actually created, but sundays are ignored. # Two rooms is a fairly low level of simultaneous schedules, this is needed # because the schedule in these tests is much more complex than a real schedule. - self.meeting = MeetingFactory(type_id='ietf', days=2) + self.meeting = MeetingFactory(type_id='ietf', days=2, date=datetime.date(2020, 5, 31)) self.rooms = [ RoomFactory(meeting=self.meeting, capacity=100), RoomFactory(meeting=self.meeting, capacity=10) @@ -26,7 +27,7 @@ class ScheduleGeneratorTest(TestCase): self.timeslots = [] for room in self.rooms: - for day in range(0, 2): + for day in range(0, 3): for hour in range(12, 17): t = TimeSlotFactory( meeting=self.meeting, From 57d8ffb27aa943915c02c157718a5be1c7919626 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 1 Jun 2020 09:25:51 +0000 Subject: [PATCH 08/17] Re-introduce Group.meeting_seen_as_area and use it in automatic scheduler - Legacy-Id: 17901 --- .../0024_add_meeting_seen_as_area.py | 37 +++++++++++++++++++ ietf/group/models.py | 3 +- .../management/commands/schedule_generator.py | 4 +- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 ietf/group/migrations/0024_add_meeting_seen_as_area.py diff --git a/ietf/group/migrations/0024_add_meeting_seen_as_area.py b/ietf/group/migrations/0024_add_meeting_seen_as_area.py new file mode 100644 index 000000000..8c2bcbef3 --- /dev/null +++ b/ietf/group/migrations/0024_add_meeting_seen_as_area.py @@ -0,0 +1,37 @@ +# Copyright The IETF Trust 2020', 'All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.27 on 2020-02-12 07:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def forward(apps, schema_editor): + Group = apps.get_model('group', 'Group') + initial_area_groups = ['dispatch', 'gendispatch', 'intarea', 'opsarea', 'opsawg', 'rtgarea', 'rtgwg', 'saag', 'secdispatch', 'tsvarea', 'irtfopen'] + Group.objects.filter(acronym__in=initial_area_groups).update(meeting_seen_as_area=True) + + +def reverse(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('group', '0023_use_milestone_dates_default_to_true'), + ] + + operations = [ + migrations.AddField( + model_name='group', + name='meeting_seen_as_area', + field=models.BooleanField(default=False, help_text='For meeting scheduling, should be considered an area meeting, even if the type is WG'), + ), + migrations.AddField( + model_name='grouphistory', + name='meeting_seen_as_area', + field=models.BooleanField(default=False, help_text='For meeting scheduling, should be considered an area meeting, even if the type is WG'), + ), + migrations.RunPython(forward, reverse), + ] diff --git a/ietf/group/models.py b/ietf/group/models.py index 80258f2f2..7a8a00cad 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -39,7 +39,8 @@ class GroupInfo(models.Model): list_subscribe = models.CharField(max_length=255, blank=True) list_archive = models.CharField(max_length=255, blank=True) comments = models.TextField(blank=True) - + meeting_seen_as_area = models.BooleanField(default=False, help_text='For meeting scheduling, should be considered an area meeting, even if the type is WG') + unused_states = models.ManyToManyField('doc.State', help_text="Document states that have been disabled for the group.", blank=True) unused_tags = models.ManyToManyField(DocTagName, help_text="Document tags that have been disabled for the group.", blank=True) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index f26a10f85..b61ded419 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -462,9 +462,7 @@ class Session(object): self.is_area_meeting = any([ session_db.group.type_id == 'area', session_db.group.type_id == 'ag', - self.group in ['dispatch', 'gendispatch', 'intarea', 'opsarea/opsawg', 'rtgarea', - 'rtgwg', 'saag', 'secdispatch', 'tsvarea', 'irtfopen'] - # meeting_seen_as_area TODO + session_db.group.meeting_seen_as_area, ]) self.is_bof = session_db.group.state_id == 'bof' self.is_prg = session_db.group.type_id == 'rg' and session_db.group.state_id == 'proposed' From 69a5752a0060c914bca811b6d8ac421621be764b Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 1 Jun 2020 09:27:25 +0000 Subject: [PATCH 09/17] Fix incorrect timerange name in create_dummy_meeting command - Legacy-Id: 17902 --- ietf/meeting/management/commands/create_dummy_meeting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/meeting/management/commands/create_dummy_meeting.py b/ietf/meeting/management/commands/create_dummy_meeting.py index bab20b847..8ffb57cd6 100644 --- a/ietf/meeting/management/commands/create_dummy_meeting.py +++ b/ietf/meeting/management/commands/create_dummy_meeting.py @@ -1565,7 +1565,7 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=115214, ) # Benjamin Kaduk c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=105815, ) # Roman Danyliw c = Constraint.objects.create(meeting=m, source=s.group, name_id='timerange') - c.timeranges.set(TimerangeName.objects.exclude(slug__startswith='thursday-early-afternoon')) + c.timeranges.set(TimerangeName.objects.exclude(slug__startswith='thursday-afternoon-early')) ## session for mpls ## s = Session.objects.create( From 2c49e7b2dd4d7e87ac70705895cb9e523fab07c3 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 1 Jun 2020 10:09:27 +0000 Subject: [PATCH 10/17] Add missing BusinessConstraint resource/admin - Legacy-Id: 17903 --- ietf/meeting/admin.py | 14 +++++++++++++- ietf/meeting/resources.py | 18 +++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/ietf/meeting/admin.py b/ietf/meeting/admin.py index f8477ce2f..5c70515d9 100644 --- a/ietf/meeting/admin.py +++ b/ietf/meeting/admin.py @@ -6,7 +6,7 @@ from django.contrib import admin from ietf.meeting.models import (Meeting, Room, Session, TimeSlot, Constraint, Schedule, SchedTimeSessAssignment, ResourceAssociation, FloorPlan, UrlResource, - SessionPresentation, ImportantDate, SlideSubmission, SchedulingEvent) + SessionPresentation, ImportantDate, SlideSubmission, SchedulingEvent, BusinessConstraint) class UrlResourceAdmin(admin.ModelAdmin): @@ -65,6 +65,18 @@ class TimeSlotAdmin(admin.ModelAdmin): admin.site.register(TimeSlot, TimeSlotAdmin) +class BusinessConstraintAdmin(admin.ModelAdmin): + list_display = ["slug", "name", "penalty"] + search_fields = ["slug", "name"] + + def name_lower(self, instance): + return instance.name.name.lower() + + name_lower.short_description = "businessconstraint" # type: ignore # https://github.com/python/mypy/issues/2087 + +admin.site.register(BusinessConstraint, BusinessConstraintAdmin) + + class ConstraintAdmin(admin.ModelAdmin): list_display = ["meeting", "source", "name_lower", "target"] raw_id_fields = ["meeting", "source", "target"] diff --git a/ietf/meeting/resources.py b/ietf/meeting/resources.py index a2b3779d2..03c7e902c 100644 --- a/ietf/meeting/resources.py +++ b/ietf/meeting/resources.py @@ -13,7 +13,8 @@ from ietf import api from ietf.meeting.models import ( Meeting, ResourceAssociation, Constraint, Room, Schedule, Session, TimeSlot, SchedTimeSessAssignment, SessionPresentation, FloorPlan, - UrlResource, ImportantDate, SlideSubmission, SchedulingEvent ) + UrlResource, ImportantDate, SlideSubmission, SchedulingEvent, + BusinessConstraint) from ietf.name.resources import MeetingTypeNameResource class MeetingResource(ModelResource): @@ -358,3 +359,18 @@ class SlideSubmissionResource(ModelResource): "submitter": ALL_WITH_RELATIONS, } api.meeting.register(SlideSubmissionResource()) + + +class BusinessConstraintResource(ModelResource): + class Meta: + queryset = BusinessConstraint.objects.all() + serializer = api.Serializer() + cache = SimpleCache() + #resource_name = 'businessconstraint' + ordering = ['slug', ] + filtering = { + "slug": ALL, + "name": ALL, + "penalty": ALL, + } +api.meeting.register(BusinessConstraintResource()) From fc2693370ecaaf5fcfa0823c861db7f1411c6dd9 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 1 Jun 2020 12:26:21 +0000 Subject: [PATCH 11/17] Improved initial session scheduling for tight schedules - Legacy-Id: 17904 --- .../management/commands/schedule_generator.py | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index b61ded419..2c6624fc4 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -140,7 +140,11 @@ class Schedule(object): """ Check the number of sessions, their required capacity and duration against availability. If there are too many sessions, the generator exits. - If sessions can't fit, they are trimmed, and a fixed cost is applied. + If sessions can't fit, they are trimmed, and a fixed cost is applied. + + Note that the trim is only applied on the in-memory object. The purpose + of trimming in advance is to prevent the optimiser from trying to resolve + a constraint that can never be resolved. """ if len(self.sessions) > len(self.timeslots): raise CommandError('More sessions ({}) than timeslots ({})' @@ -218,6 +222,9 @@ class Schedule(object): - Second: shortest duration that still fits - Third: smallest room that still fits If there are multiple options with equal value, a random one is picked. + + For initial scheduling, it is not a hard requirement that the timeslot is long + or large enough, though that will be preferred due to the lower cost. """ if self.verbosity >= 2: self.stdout.write('== Initial scheduler starting, scheduling {} sessions in {} timeslots ==' @@ -225,14 +232,9 @@ class Schedule(object): sessions = sorted(self.sessions, key=lambda s: s.complexity, reverse=True) for session in sessions: - possible_slots = [t for t in self.timeslots if - t not in self.schedule.keys() and session.fits_in_timeslot(t)] + possible_slots = [t for t in self.timeslots if t not in self.schedule.keys()] random.shuffle(possible_slots) - if not len(possible_slots): - # TODO: this needs better handling - raise Exception('No timeslot was left for {} ({})' - .format(session.group, session.session_pk)) - + def timeslot_preference(t): proposed_schedule = self.schedule.copy() proposed_schedule[t] = session @@ -339,8 +341,6 @@ class Schedule(object): break def _schedule_session(self, session, timeslot): - if not session.fits_in_timeslot(timeslot): - raise ValueError self.schedule[timeslot] = session def _cost_for_switch(self, timeslot1, timeslot2): @@ -555,6 +555,14 @@ class Session(object): violations, cost = [], 0 overlapping_sessions = tuple(overlapping_sessions) + if self.attendees > my_timeslot.capacity: + violations.append('{}: scheduled scheduled in too small room'.format(self.group)) + cost += self.business_constraint_costs['session_requires_trim'] + + if self.requested_duration > my_timeslot.duration: + violations.append('{}: scheduled scheduled in too short timeslot'.format(self.group)) + cost += self.business_constraint_costs['session_requires_trim'] + if my_timeslot.time_group in self.timeranges_unavailable: violations.append('{}: scheduled in unavailable timerange {}' .format(self.group, my_timeslot.time_group)) From a0b6c6dcfc0aa0d0a179c234b693deb3f4a6ffb1 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 1 Jun 2020 14:12:39 +0000 Subject: [PATCH 12/17] Add capacity optimiser to automatic scheduler. - Legacy-Id: 17905 --- .../management/commands/schedule_generator.py | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index 2c6624fc4..bf17065af 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -56,6 +56,8 @@ class ScheduleHandler(object): self.stdout.write('Remaining violations:') for v in violations: self.stdout.write(v) + + self.schedule.optimise_timeslot_capacity() self._save_schedule(cost) return violations, cost @@ -339,6 +341,34 @@ class Schedule(object): self.stdout.write('Moved {} to random new slot, previously in slot was {}' .format(rescheduling_session.group, switched_group)) break + + def optimise_timeslot_capacity(self): + """ + Optimise the schedule for room capacity usage. + + For each fully overlapping timeslot, the sessions are re-ordered so + that smaller sessions are in smaller rooms, and larger sessions in + larger rooms. This does not change which sessions overlap, so it + has no impact on the schedule cost. + """ + optimised_timeslots = set() + for timeslot in list(self.schedule.keys()): + if timeslot in optimised_timeslots: + continue + timeslot_overlaps = sorted(timeslot.full_overlaps, key=lambda t: t.capacity, reverse=True) + sessions_overlaps = [self.schedule.get(t) for t in timeslot_overlaps] + sessions_overlaps.sort(key=lambda s: s.attendees if s else 0, reverse=True) + assert len(timeslot_overlaps) == len(sessions_overlaps) + + for new_timeslot in timeslot_overlaps: + new_session = sessions_overlaps.pop(0) + if not new_session and new_timeslot in self.schedule: + del self.schedule[new_timeslot] + elif new_session: + self.schedule[new_timeslot] = new_session + + optimised_timeslots.add(timeslot) + optimised_timeslots.update(timeslot_overlaps) def _schedule_session(self, session, timeslot): self.schedule[timeslot] = session @@ -418,13 +448,17 @@ class TimeSlot(object): self.time_of_day = 'afternoon-late' self.time_group = self.day + '-' + self.time_of_day self.overlaps = set() + self.full_overlaps = set() self.adjacent = set() def store_relations(self, other_timeslots): """ Store relations to all other timeslots. This should be called after all TimeSlot objects have been created. This allows fast - lookups of which TimeSlot objects overlap or are adjacent. + lookups of which TimeSlot objects overlap or are adjacent. + Note that there is a distinction between an overlap, meaning + at least part of the timeslots occur during the same time, + and a full overlap, meaning the start and end time are identical. """ for other in other_timeslots: if any([ @@ -433,6 +467,8 @@ class TimeSlot(object): self.start >= other.start and self.end <= other.end, ]) and other != self: self.overlaps.add(other) + if self.start == other.start and self.end == other.end and other != self: + self.full_overlaps.add(other) if ( abs(self.start - other.end) <= datetime.timedelta(minutes=30) or abs(other.start - self.end) <= datetime.timedelta(minutes=30) From 69fbd46b277be6f578fdbbd31b4e71261698e23a Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 1 Jun 2020 15:03:46 +0000 Subject: [PATCH 13/17] Save automatic schedules as public=False visible=True - Legacy-Id: 17906 --- ietf/meeting/management/commands/schedule_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index bf17065af..08b7d138a 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -68,7 +68,7 @@ class ScheduleHandler(object): meeting=self.meeting, name=name, owner=Person.objects.get(name='(System)'), - public=True, + public=False, visible=True, badness=cost, ) From d0386b8524cc9e62adddbde2cb2957bbfe2a9545 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Tue, 2 Jun 2020 17:39:25 +0000 Subject: [PATCH 14/17] Add link to docs in automatic schedule builder. - Legacy-Id: 17907 --- ietf/meeting/management/commands/schedule_generator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index 08b7d138a..d6ae243a4 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -1,4 +1,6 @@ # Copyright The IETF Trust 2020, All Rights Reserved +# For an overview of this process and context, see: +# https://trac.tools.ietf.org/tools/ietfdb/wiki/MeetingConstraints from __future__ import absolute_import, print_function, unicode_literals import calendar @@ -82,7 +84,6 @@ class ScheduleHandler(object): for bc in models.BusinessConstraint.objects.all() } - # TODO: ensure these filters are correct timeslots_db = models.TimeSlot.objects.filter( meeting=self.meeting, type_id='regular', From ec1ee89e89f3d816a27439ed0924b31940d68159 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 15 Jun 2020 09:16:48 +0000 Subject: [PATCH 15/17] - Increase penalty of timerange and area meeting overlap - Fix incidental bug in schedule optimiser - Fix unused import - Legacy-Id: 17988 --- ietf/meeting/management/commands/schedule_generator.py | 1 + ietf/meeting/migrations/0028_businessconstraint.py | 4 ++-- ietf/meeting/test_schedule_generator.py | 2 +- ietf/name/fixtures/names.json | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ietf/meeting/management/commands/schedule_generator.py b/ietf/meeting/management/commands/schedule_generator.py index d6ae243a4..d930001b2 100644 --- a/ietf/meeting/management/commands/schedule_generator.py +++ b/ietf/meeting/management/commands/schedule_generator.py @@ -323,6 +323,7 @@ class Schedule(object): an entirely random timeslot, in which they fit. Parameter is an iterable of (timeslot, session) tuples. """ + self.calculate_dynamic_cost() # update all costs to_reschedule = [(t, s) for t, s in items if s.last_cost] random.shuffle(to_reschedule) if self.verbosity >= 2: diff --git a/ietf/meeting/migrations/0028_businessconstraint.py b/ietf/meeting/migrations/0028_businessconstraint.py index c5ccb354f..1ca943776 100644 --- a/ietf/meeting/migrations/0028_businessconstraint.py +++ b/ietf/meeting/migrations/0028_businessconstraint.py @@ -35,7 +35,7 @@ def forward(apps, schema_editor): BusinessConstraint.objects.create( slug="area_overlapping_other_area", name="Area meetings cannot conflict with other area meetings", - penalty=1000, + penalty=100000, ) BusinessConstraint.objects.create( slug="session_overlap_ad", @@ -58,7 +58,7 @@ def forward(apps, schema_editor): ConstraintName.objects.filter(slug='conflic2').update(penalty=10000) ConstraintName.objects.filter(slug='conflic3').update(penalty=100000) ConstraintName.objects.filter(slug='bethere').update(penalty=10000) - ConstraintName.objects.filter(slug='timerange').update(penalty=10000) + ConstraintName.objects.filter(slug='timerange').update(penalty=1000000) ConstraintName.objects.filter(slug='time_relation').update(penalty=1000) ConstraintName.objects.filter(slug='wg_adjacent').update(penalty=1000) diff --git a/ietf/meeting/test_schedule_generator.py b/ietf/meeting/test_schedule_generator.py index d281fb9fc..217c67f3b 100644 --- a/ietf/meeting/test_schedule_generator.py +++ b/ietf/meeting/test_schedule_generator.py @@ -8,7 +8,7 @@ from django.core.management.base import CommandError from ietf.utils.test_utils import TestCase from ietf.group.factories import GroupFactory, RoleFactory from ietf.person.factories import PersonFactory -from ietf.meeting.models import Session, Constraint, TimerangeName, BusinessConstraint +from ietf.meeting.models import Constraint, TimerangeName, BusinessConstraint from ietf.meeting.factories import MeetingFactory, RoomFactory, TimeSlotFactory, SessionFactory from ietf.meeting.management.commands.schedule_generator import ScheduleHandler diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index c04bf3391..3146794e3 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -5625,7 +5625,7 @@ "desc": "", "name": "Can't meet within timerange", "order": 0, - "penalty": 10000, + "penalty": 1000000, "used": true }, "model": "name.constraintname", @@ -14541,7 +14541,7 @@ "pk": "area_overlapping_other_area", "fields": { "name": "Area meetings cannot conflict with other area meetings", - "penalty": 1000 + "penalty": 100000 } }, { From 9dd0035a191095fac280f5dbd65a24bb6f503de8 Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 15 Jun 2020 20:51:46 +0000 Subject: [PATCH 16/17] Fix migrations after rebase. - Legacy-Id: 17994 --- ...meeting_seen_as_area.py => 0031_add_meeting_seen_as_area.py} | 2 +- .../{0028_businessconstraint.py => 0029_businessconstraint.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename ietf/group/migrations/{0024_add_meeting_seen_as_area.py => 0031_add_meeting_seen_as_area.py} (95%) rename ietf/meeting/migrations/{0028_businessconstraint.py => 0029_businessconstraint.py} (98%) diff --git a/ietf/group/migrations/0024_add_meeting_seen_as_area.py b/ietf/group/migrations/0031_add_meeting_seen_as_area.py similarity index 95% rename from ietf/group/migrations/0024_add_meeting_seen_as_area.py rename to ietf/group/migrations/0031_add_meeting_seen_as_area.py index 8c2bcbef3..80c7921bf 100644 --- a/ietf/group/migrations/0024_add_meeting_seen_as_area.py +++ b/ietf/group/migrations/0031_add_meeting_seen_as_area.py @@ -19,7 +19,7 @@ def reverse(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('group', '0023_use_milestone_dates_default_to_true'), + ('group', '0030_populate_default_used_roles'), ] operations = [ diff --git a/ietf/meeting/migrations/0028_businessconstraint.py b/ietf/meeting/migrations/0029_businessconstraint.py similarity index 98% rename from ietf/meeting/migrations/0028_businessconstraint.py rename to ietf/meeting/migrations/0029_businessconstraint.py index 1ca943776..ba8ae8939 100644 --- a/ietf/meeting/migrations/0028_businessconstraint.py +++ b/ietf/meeting/migrations/0029_businessconstraint.py @@ -77,7 +77,7 @@ def reverse(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('meeting', '0027_add_constraint_options_and_joint_groups'), + ('meeting', '0028_auto_20200501_0139'), ] operations = [ From fe23d2f799d82de11501717bab3afbdffcecb13f Mon Sep 17 00:00:00 2001 From: Sasha Romijn Date: Mon, 15 Jun 2020 20:53:00 +0000 Subject: [PATCH 17/17] Modify dummy meeting script to match IETF 106 better. - Legacy-Id: 17995 --- .../commands/create_dummy_meeting.py | 313 ++++-------------- 1 file changed, 71 insertions(+), 242 deletions(-) diff --git a/ietf/meeting/management/commands/create_dummy_meeting.py b/ietf/meeting/management/commands/create_dummy_meeting.py index 8ffb57cd6..00e8f3d31 100644 --- a/ietf/meeting/management/commands/create_dummy_meeting.py +++ b/ietf/meeting/management/commands/create_dummy_meeting.py @@ -216,6 +216,51 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=115244, ) # Tim Wicinski c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=111656, ) # Warren Kumari + ## session for dnssd ## + s = Session.objects.create( + meeting=m, + type_id="regular", + group_id=1895, # dnssd + attendees=75, + agenda_note="Joint with HOMENET", + requested_duration=datetime.timedelta(seconds=7200), # 2:00:00 + comments="""dnssd and homenet would like to do a single joint 2 hour meeting. We'll figure out how to divide the time.""", + remote_instructions="", + ) + ## session for dnssd ## + s = Session.objects.create( + meeting=m, + type_id="regular", + group_id=1895, # dnssd + attendees=75, + agenda_note="", + requested_duration=datetime.timedelta(seconds=3600), # 1:00:00 + comments="""dnssd and homenet would like to do a single joint 2 hour meeting. We'll figure out how to divide the time.""", + remote_instructions="", + ) + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1452, ) # dnsop + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=2150, ) # babel + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1326, ) # tls + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=2161, ) # quic + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1578, ) # v6ops + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1803, ) # homenet + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1723, ) # 6man + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1958, ) # dprive + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1718, ) # httpbis + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=2208, ) # doh + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflict', target_id=1665, ) # intarea + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=1789, ) # core + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2220, ) # mls + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=1956, ) # anima + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic3', target_id=2231, ) # rats + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic3', target_id=1903, ) # 6tisch + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic3', target_id=2249, ) # lake + c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic3', target_id=1730, ) # roll + c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=119562, ) # David Schinazi + c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=114464, ) # Barbara Stark + c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=105099, ) # Éric Vyncke + c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=19177, ) # Stephen Farrell + ## session for lsvr ## s = Session.objects.create( meeting=m, @@ -425,18 +470,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='timerange') c.timeranges.set(TimerangeName.objects.exclude(slug='monday-morning')) - ## session for tram ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1926, # tram - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for v6ops ## s = Session.objects.create( meeting=m, @@ -619,18 +652,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='timerange') c.timeranges.set(TimerangeName.objects.exclude(slug__startswith='friday').exclude(slug__startswith='thursday')) - ## session for mtgvenue ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=2147, # mtgvenue - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for tcpm ## s = Session.objects.create( meeting=m, @@ -1031,18 +1052,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=108279, ) # Martin Vigoureux c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=105786, ) # Matthew Bocci - ## session for cellar ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=2022, # cellar - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for homenet and dnssd ## s = Session.objects.create( meeting=m, @@ -1078,18 +1087,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=105099, ) # Éric Vyncke c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=19177, ) # Stephen Farrell - ## session for curdle ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=2143, # curdle - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for acme ## s = Session.objects.create( meeting=m, @@ -1510,18 +1507,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=105620, ) # Peter Van der Stok c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=102254, ) # Michael Richardson - ## session for bfcpbis ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1832, # bfcpbis - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for saag ## s = Session.objects.create( meeting=m, @@ -1949,42 +1934,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=101923, ) # Jonathan Lennox c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=21684, ) # Barry Leiba - ## session for kitten ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1634, # kitten - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - - ## session for clue ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1816, # clue - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - - ## session for payload ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1814, # payload - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for bfd ## s = Session.objects.create( meeting=m, @@ -2259,18 +2208,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=121666, ) # Jérôme François c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=108591, ) # Laurent Ciavaglia - ## session for softwire ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1678, # softwire - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for intarea ## s = Session.objects.create( meeting=m, @@ -2389,18 +2326,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=109802, ) # Alvaro Retana c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=112405, ) # Jeff Tantsura - ## session for uta ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1918, # uta - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for spring ## s = Session.objects.create( meeting=m, @@ -2432,18 +2357,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=107172, ) # Bruno Decraene c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=116387, ) # Rob Shakir - ## session for nfsv4 ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1152, # nfsv4 - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for dhc ## s = Session.objects.create( meeting=m, @@ -2790,18 +2703,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=102154, ) # Alexey Melnikov c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=21684, ) # Barry Leiba - ## session for sipcore ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1762, # sipcore - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for manet ## s = Session.objects.create( meeting=m, @@ -2896,18 +2797,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='timerange') c.timeranges.set(TimerangeName.objects.filter(slug__startswith='friday')) - ## session for mmusic ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1138, # mmusic - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for ntp ## s = Session.objects.create( meeting=m, @@ -2933,18 +2822,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=106412, ) # Suresh Krishnan c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=4857, ) # Karen O'Donoghue - ## session for tictoc ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1709, # tictoc - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for oauth ## s = Session.objects.create( meeting=m, @@ -3201,18 +3078,6 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=11843, ) # Carsten Bormann c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=108990, ) # Ari Keränen - ## session for i2nsf ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=1965, # i2nsf - attendees=None, - agenda_note="", - requested_duration=datetime.timedelta(0), # 0:00:00 - comments="""""", - remote_instructions="", - ) - ## session for ace ## s = Session.objects.create( meeting=m, @@ -3412,31 +3277,7 @@ class Command(BaseCommand): c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=1996, ) # dots c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=1831, ) # mile c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=105815, ) # Roman Danyliw - - ## session for rseme ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=2259, # rseme - attendees=150, - agenda_note="", - requested_duration=datetime.timedelta(seconds=5400), # 1:30:00 - comments="""Please avoid other BoFs, and minimize conflicts for attendees.""", - remote_instructions="", - ) - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2233, ) # git - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2147, ) # mtgvenue - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2221, ) # iasa2 - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2252, ) # gendispatch - c = Constraint.objects.create(meeting=m, source=s.group, name_id='bethere', person_id=113431, ) # Heather Flanagan - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2253, ) # abcd - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2256, ) # raw - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2254, ) # wpack - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2258, ) # mathmesh - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2257, ) # txauth - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2255, ) # tmrid - c = Constraint.objects.create(meeting=m, source=s.group, name_id='conflic2', target_id=2260, ) # webtrans - + ## session for gendispatch ## s = Session.objects.create( meeting=m, @@ -3548,18 +3389,6 @@ class Command(BaseCommand): 'tuesday-afternoon-early', 'tuesday-afternoon-late', 'wednesday-morning', 'wednesday-afternoon-early', 'wednesday-afternoon-late', 'thursday-morning'])) - ## session for hotrfc ## - s = Session.objects.create( - meeting=m, - type_id="regular", - group_id=2225, # hotrfc - attendees=200, - agenda_note="", - requested_duration=datetime.timedelta(seconds=7200), # 2:00:00 - comments="""""", - remote_instructions="", - ) - ## session for nvo3 ## s = Session.objects.create( meeting=m, @@ -4297,55 +4126,55 @@ class Command(BaseCommand): ## timeslot 2019-11-19 08:30:00 length 1:15:00 in None ## TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=None, show_location=False) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Orchard size: 50 ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Orchard"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Orchard"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in VIP A size: 100 ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="VIP A"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="VIP A"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Hullet size: 100 ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Hullet"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Hullet"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Olivia size: 150 ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Olivia"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Olivia"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Sophia size: 200 ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Sophia"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Sophia"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Collyer size: 250 ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Collyer"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Collyer"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Padang size: 300 ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Padang"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Padang"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Canning size: 250 ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Canning"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Canning"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Canning/Padang size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Canning/Padang"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Canning/Padang"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Stamford & Fairmont Ballroom Foyers size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Stamford & Fairmont Ballroom Foyers"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Stamford & Fairmont Ballroom Foyers"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Convention Foyer size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Convention Foyer"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Convention Foyer"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Fairmont Ballroom Foyer size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Fairmont Ballroom Foyer"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Fairmont Ballroom Foyer"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Moor/Morrison size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Moor/Morrison"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Moor/Morrison"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Moor/Morrison size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Moor/Morrison"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Moor/Morrison"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in VIP B size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="VIP B"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="VIP B"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Clark size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Clark"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Clark"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Mercury/Enterprise size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Mercury/Enterprise"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Mercury/Enterprise"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Minto size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Minto"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Minto"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Fullerton size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Fullerton"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Fullerton"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Bonham size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Bonham"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Bonham"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Bailey size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Bailey"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Bailey"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Ord/Blundell size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Ord/Blundell"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Ord/Blundell"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Indiana size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Indiana"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Indiana"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Bras Basah size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Bras Basah"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Bras Basah"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 1:15:00 in Butterworth size: None ## - TimeSlot.objects.create(meeting=m, type_id="regular", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Butterworth"), show_location=True) + TimeSlot.objects.create(meeting=m, type_id="other", name="Side Meetings / Open Time", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=4500), location=Room.objects.get(meeting=m, name="Butterworth"), show_location=True) ## timeslot 2019-11-19 08:30:00 length 10:00:00 in Convention Foyer size: None ## TimeSlot.objects.create(meeting=m, type_id="reg", name="IETF Registration", time=datetime.datetime(2019, 11, 19, 8, 30), duration=datetime.timedelta(seconds=36000), location=Room.objects.get(meeting=m, name="Convention Foyer"), show_location=True) ## timeslot 2019-11-19 08:00:00 length 1:00:00 in Stamford & Fairmont Ballroom Foyers size: None ##