|
16 | 16 | import tempfile |
17 | 17 | import markdown2 |
18 | 18 |
|
19 | | - |
20 | 19 | from calendar import timegm |
21 | 20 | from collections import OrderedDict, Counter, deque, defaultdict |
22 | 21 | from urllib.parse import unquote |
@@ -495,8 +494,6 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): |
495 | 494 | for a in assignments: |
496 | 495 | assignments_by_session[a.session_id].append(a) |
497 | 496 |
|
498 | | - rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity") |
499 | | - |
500 | 497 | tombstone_states = ['canceled', 'canceledpa', 'resched'] |
501 | 498 |
|
502 | 499 | sessions = add_event_info_to_session_qs( |
@@ -581,6 +578,145 @@ def prepare_sessions_for_display(sessions): |
581 | 578 |
|
582 | 579 | s.readonly = s.current_status in tombstone_states or any(a.schedule_id != schedule.pk for a in assignments_by_session.get(s.pk, [])) |
583 | 580 |
|
| 581 | + def prepare_timeslots_for_display(timeslots, rooms): |
| 582 | + """Prepare timeslot data for template |
| 583 | +
|
| 584 | + Prepares timeslots for display by sorting into groups in a structure |
| 585 | + that can be rendered by the template and by adding some data to the timeslot |
| 586 | + instances. Currently adds a 'layout_width' property to each timeslot instance. |
| 587 | + The layout_width is the width, in em, that should be used to style the timeslot's |
| 588 | + width. |
| 589 | +
|
| 590 | + Rooms are partitioned into groups that have identical sets of timeslots |
| 591 | + for the entire meeting. |
| 592 | +
|
| 593 | + The result of this method is an OrderedDict, days, keyed by the Date |
| 594 | + of each day that has at least one timeslot. The value of days[day] is a |
| 595 | + list with one entry for each group of rooms. Each entry is a list of |
| 596 | + dicts with keys 'room' and 'timeslots'. The 'room' value is the room |
| 597 | + instance and 'timeslots' is a list of timeslot instances for that room. |
| 598 | +
|
| 599 | + The format is more easily illustrated than explained: |
| 600 | +
|
| 601 | + days = OrderedDict( |
| 602 | + Date(2021, 5, 27): [ |
| 603 | + [ # room group 1 |
| 604 | + {'room': <room1>, 'timeslots': [<room1 timeslot1>, <room1 timeslot2>]}, |
| 605 | + {'room': <room2>, 'timeslots': [<room2 timeslot1>, <room2 timeslot2>]}, |
| 606 | + {'room': <room3>, 'timeslots': [<room3 timeslot1>, <room3 timeslot2>]}, |
| 607 | + ], |
| 608 | + [ # room group 2 |
| 609 | + {'room': <room4>, 'timeslots': [<room4 timeslot1>]}, |
| 610 | + ], |
| 611 | + ], |
| 612 | + Date(2021, 5, 28): [ |
| 613 | + [ # room group 1 |
| 614 | + {'room': <room1>, 'timeslots': [<room1 timeslot3>]}, |
| 615 | + {'room': <room2>, 'timeslots': [<room2 timeslot3>]}, |
| 616 | + {'room': <room3>, 'timeslots': [<room3 timeslot3>]}, |
| 617 | + ], |
| 618 | + [ # room group 2 |
| 619 | + {'room': <room4>, 'timeslots': []}, |
| 620 | + ], |
| 621 | + ], |
| 622 | + ) |
| 623 | + """ |
| 624 | + |
| 625 | + # Populate room_data. This collects the timeslots for each room binned by |
| 626 | + # day, plus data needed for sorting the rooms for display. |
| 627 | + room_data = dict() |
| 628 | + all_days = set() |
| 629 | + # timeslots_qs is already sorted by location, name, and time |
| 630 | + for t in timeslots: |
| 631 | + if t.location not in rooms: |
| 632 | + continue |
| 633 | + |
| 634 | + t.layout_width = timedelta_to_css_ems(t.duration) |
| 635 | + if t.location_id not in room_data: |
| 636 | + room_data[t.location_id] = dict( |
| 637 | + timeslots_by_day=dict(), |
| 638 | + timeslot_count=0, |
| 639 | + start_and_duration=[], |
| 640 | + first_timeslot = t, |
| 641 | + ) |
| 642 | + rd = room_data[t.location_id] |
| 643 | + rd['timeslot_count'] += 1 |
| 644 | + rd['start_and_duration'].append((t.time, t.duration)) |
| 645 | + ttd = t.time.date() |
| 646 | + all_days.add(ttd) |
| 647 | + if ttd not in rd['timeslots_by_day']: |
| 648 | + rd['timeslots_by_day'][ttd] = [] |
| 649 | + rd['timeslots_by_day'][ttd].append(t) |
| 650 | + |
| 651 | + all_days = sorted(all_days) # changes set to a list |
| 652 | + # Note the maximum timeslot count for any room |
| 653 | + max_timeslots = max(rd['timeslot_count'] for rd in room_data.values()) |
| 654 | + |
| 655 | + # Partition rooms into groups with identical timeslot arrangements. |
| 656 | + # Start by discarding any roos that have no timeslots. |
| 657 | + rooms_with_timeslots = [r for r in rooms if r.pk in room_data] |
| 658 | + # Then sort the remaining rooms. |
| 659 | + sorted_rooms = sorted( |
| 660 | + rooms_with_timeslots, |
| 661 | + key=lambda room: ( |
| 662 | + # First, sort regular session rooms ahead of others - these will usually |
| 663 | + # have more timeslots than other room types. |
| 664 | + 0 if room_data[room.pk]['timeslot_count'] == max_timeslots else 1, |
| 665 | + # Sort rooms with earlier timeslots ahead of later |
| 666 | + room_data[room.pk]['first_timeslot'].time, |
| 667 | + # Sort rooms with more sessions ahead of rooms with fewer |
| 668 | + 0 - room_data[room.pk]['timeslot_count'], |
| 669 | + # Sort by list of starting time and duration so that groups with identical |
| 670 | + # timeslot structure will be neighbors. The grouping algorithm relies on this! |
| 671 | + room_data[room.pk]['start_and_duration'], |
| 672 | + # Within each group, sort higher capacity rooms first. |
| 673 | + room.capacity, |
| 674 | + # Finally, sort alphabetically by name |
| 675 | + room.name |
| 676 | + ) |
| 677 | + ) |
| 678 | + |
| 679 | + # Rooms are now ordered so rooms with identical timeslot arrangements are neighbors. |
| 680 | + # Walk the list, splitting these into groups. |
| 681 | + room_groups = [] |
| 682 | + last_start_and_duration = None # Used to watch for changes in start_and_duration |
| 683 | + for room in sorted_rooms: |
| 684 | + if last_start_and_duration != room_data[room.pk]['start_and_duration']: |
| 685 | + room_groups.append([]) # start a new room_group |
| 686 | + last_start_and_duration = room_data[room.pk]['start_and_duration'] |
| 687 | + room_groups[-1].append(room) |
| 688 | + |
| 689 | + # Next, build the structure that will hold the data for the view. This makes it |
| 690 | + # easier to arrange that every room has an entry for every day, even if there is |
| 691 | + # no timeslot for that day. This makes the HTML template much easier to write. |
| 692 | + # Use OrderedDicts instead of lists so that we can easily put timeslot data in the |
| 693 | + # right place. |
| 694 | + days = OrderedDict( |
| 695 | + ( |
| 696 | + day, # key in the Ordered Dict |
| 697 | + [ |
| 698 | + # each value is an OrderedDict of room group data |
| 699 | + OrderedDict( |
| 700 | + (room.pk, dict(room=room, timeslots=[])) |
| 701 | + for room in rg |
| 702 | + ) for rg in room_groups |
| 703 | + ] |
| 704 | + ) for day in all_days |
| 705 | + ) |
| 706 | + |
| 707 | + # With the structure's skeleton built, now fill in the data. The loops must |
| 708 | + # preserve the order of room groups and rooms within each group. |
| 709 | + for rg_num, rgroup in enumerate(room_groups): |
| 710 | + for room in rgroup: |
| 711 | + for day, ts_for_day in room_data[room.pk]['timeslots_by_day'].items(): |
| 712 | + days[day][rg_num][room.pk]['timeslots'] = ts_for_day |
| 713 | + |
| 714 | + # Now convert the OrderedDict entries into lists since we don't need to |
| 715 | + # do lookup by pk any more. |
| 716 | + for day in days.keys(): |
| 717 | + days[day] = [list(rg.values()) for rg in days[day]] |
| 718 | + |
| 719 | + return days |
584 | 720 |
|
585 | 721 | if request.method == 'POST': |
586 | 722 | if not can_edit: |
@@ -660,34 +796,11 @@ def prepare_sessions_for_display(sessions): |
660 | 796 |
|
661 | 797 | return HttpResponse("Invalid parameters", status=400) |
662 | 798 |
|
663 | | - # prepare timeslot layout |
664 | | - |
665 | | - timeslots_by_room_and_day = defaultdict(list) |
666 | | - room_has_timeslots = set() |
667 | | - for t in timeslots_qs: |
668 | | - room_has_timeslots.add(t.location_id) |
669 | | - timeslots_by_room_and_day[(t.location_id, t.time.date())].append(t) |
670 | | - |
671 | | - days = [] |
672 | | - for day in sorted(set(t.time.date() for t in timeslots_qs)): |
673 | | - room_timeslots = [] |
674 | | - for r in rooms: |
675 | | - if r.pk not in room_has_timeslots: |
676 | | - continue |
677 | | - |
678 | | - timeslots = [] |
679 | | - for t in timeslots_by_room_and_day.get((r.pk, day), []): |
680 | | - t.layout_width = timedelta_to_css_ems(t.end_time() - t.time) |
681 | | - timeslots.append(t) |
682 | | - |
683 | | - room_timeslots.append((r, timeslots)) |
684 | | - |
685 | | - days.append({ |
686 | | - 'day': day, |
687 | | - 'room_timeslots': room_timeslots, |
688 | | - }) |
| 799 | + # Show only rooms that have regular sessions |
| 800 | + rooms = meeting.room_set.filter(session_types__slug='regular') |
689 | 801 |
|
690 | | - room_labels = [[r for r in rooms if r.pk in room_has_timeslots] for i in range(len(days))] |
| 802 | + # Construct timeslot data for the template to render |
| 803 | + days = prepare_timeslots_for_display(timeslots_qs, rooms) |
691 | 804 |
|
692 | 805 | # possible timeslot start/ends |
693 | 806 | timeslot_groups = defaultdict(set) |
@@ -761,7 +874,6 @@ def cubehelix(i, total, hue=1.2, start_angle=0.5): |
761 | 874 | 'can_edit_properties': can_edit or secretariat, |
762 | 875 | 'secretariat': secretariat, |
763 | 876 | 'days': days, |
764 | | - 'room_labels': room_labels, |
765 | 877 | 'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()), |
766 | 878 | 'unassigned_sessions': unassigned_sessions, |
767 | 879 | 'session_parents': session_parents, |
|
0 commit comments