Skip to content

Commit 27384a1

Browse files
committed
Make constraint hints more obvious. Show constraints in the session
information panel. - Legacy-Id: 17971
1 parent d357723 commit 27384a1

8 files changed

Lines changed: 236 additions & 96 deletions

File tree

ietf/meeting/tests_js.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def test_edit_meeting_schedule(self):
189189
s1_element = self.driver.find_element_by_css_selector('#session{}'.format(s1.pk))
190190
s1_element.click()
191191

192-
constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].selected-hint".format(s1.pk))
192+
constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].would-violate-hint".format(s1.pk))
193193
self.assertTrue(constraint_element.is_displayed())
194194

195195
# current constraint violations

ietf/meeting/utils.py

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,25 @@
33

44

55
import datetime
6+
import itertools
7+
import re
68
import requests
79

10+
from collections import defaultdict
811
from urllib.error import HTTPError
12+
913
from django.conf import settings
1014
from django.template.loader import render_to_string
1115
from django.db.models.expressions import Subquery, OuterRef
16+
from django.utils.html import format_html
1217

1318
import debug # pyflakes:ignore
1419

1520
from ietf.dbtemplate.models import DBTemplate
16-
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot
21+
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot, Constraint
1722
from ietf.group.models import Group, Role
1823
from ietf.group.utils import can_manage_materials
19-
from ietf.name.models import SessionStatusName
24+
from ietf.name.models import SessionStatusName, ConstraintName
2025
from ietf.nomcom.utils import DISQUALIFYING_ROLE_QUERY_EXPRESSION
2126
from ietf.person.models import Email
2227
from ietf.secr.proceedings.proc_utils import import_audio_files
@@ -303,3 +308,91 @@ def data_for_meetings_overview(meetings, interim_status=None):
303308
m.interim_meeting_cancelled = m.type_id == 'interim' and all(s.current_status == 'canceled' for s in m.sessions)
304309

305310
return meetings
311+
312+
def format_constraint_editor_label(label, inner_fmt="{}"):
313+
m = re.match(r'(.*)\(person\)(.*)', label)
314+
if m:
315+
return format_html("{}<i class=\"fa fa-user-o\"></i>{}", format_html(inner_fmt, m.groups()[0]), m.groups()[1])
316+
317+
m = re.match(r"\(([^()]+)\)", label)
318+
if m:
319+
return format_html("<span class=\"encircled\">{}</span>", format_html(inner_fmt, m.groups()[0]))
320+
321+
return format_html(inner_fmt, label)
322+
323+
def preprocess_constraints_for_meeting_schedule_editor(meeting, sessions):
324+
constraints = Constraint.objects.filter(meeting=meeting).prefetch_related('target', 'person', 'timeranges')
325+
326+
# process constraint names
327+
constraint_names = {n.pk: n for n in ConstraintName.objects.all()}
328+
329+
for n in list(constraint_names.values()):
330+
# add reversed version of the name
331+
reverse_n = ConstraintName(
332+
slug=n.slug + "-reversed",
333+
name="{} - reversed".format(n.name),
334+
)
335+
reverse_n.formatted_editor_label = format_constraint_editor_label(n.editor_label, inner_fmt="-{}")
336+
constraint_names[reverse_n.slug] = reverse_n
337+
338+
n.formatted_editor_label = format_constraint_editor_label(n.editor_label)
339+
n.countless_formatted_editor_label = format_html(n.formatted_editor_label, count="") if "{count}" in n.formatted_editor_label else n.formatted_editor_label
340+
341+
# convert human-readable rules in the database to constraints on actual sessions
342+
constraints_for_sessions = defaultdict(list)
343+
344+
person_needed_for_groups = defaultdict(set)
345+
for c in constraints:
346+
if c.name_id == 'bethere' and c.person_id is not None:
347+
person_needed_for_groups[c.person_id].add(c.source_id)
348+
349+
sessions_for_group = defaultdict(list)
350+
for s in sessions:
351+
if s.group_id is not None:
352+
sessions_for_group[s.group_id].append(s.pk)
353+
354+
def add_group_constraints(g1_pk, g2_pk, name_id, person_id):
355+
if g1_pk != g2_pk:
356+
for s1_pk in sessions_for_group.get(g1_pk, []):
357+
for s2_pk in sessions_for_group.get(g2_pk, []):
358+
if s1_pk != s2_pk:
359+
constraints_for_sessions[s1_pk].append((name_id, s2_pk, person_id))
360+
361+
reverse_constraints = []
362+
seen_forward_constraints_for_groups = set()
363+
364+
for c in constraints:
365+
if c.target_id:
366+
add_group_constraints(c.source_id, c.target_id, c.name_id, c.person_id)
367+
seen_forward_constraints_for_groups.add((c.source_id, c.target_id, c.name_id))
368+
reverse_constraints.append(c)
369+
370+
elif c.person_id:
371+
for g in person_needed_for_groups.get(c.person_id):
372+
add_group_constraints(c.source_id, g, c.name_id, c.person_id)
373+
374+
for c in reverse_constraints:
375+
# suppress reverse constraints in case we have a forward one already
376+
if (c.target_id, c.source_id, c.name_id) not in seen_forward_constraints_for_groups:
377+
add_group_constraints(c.target_id, c.source_id, c.name_id + "-reversed", c.person_id)
378+
379+
# formatted constraints
380+
def format_constraint(c):
381+
if c.name_id == "time_relation":
382+
return c.get_time_relation_display()
383+
elif c.name_id == "timerange":
384+
return ", ".join(t.desc for t in c.timeranges.all())
385+
elif c.person:
386+
return c.person.plain_name()
387+
elif c.target:
388+
return c.target.acronym
389+
else:
390+
return "UNKNOWN"
391+
392+
formatted_constraints_for_sessions = defaultdict(dict)
393+
for (group_pk, cn_pk), cs in itertools.groupby(sorted(constraints, key=lambda c: (c.source_id, constraint_names[c.name_id].order, c.name_id, c.pk)), key=lambda c: (c.source_id, c.name_id)):
394+
cs = list(cs)
395+
for s_pk in sessions_for_group.get(group_pk, []):
396+
formatted_constraints_for_sessions[s_pk][constraint_names[cn_pk]] = [format_constraint(c) for c in cs]
397+
398+
return constraints_for_sessions, formatted_constraints_for_sessions, constraint_names

ietf/meeting/views.py

Lines changed: 14 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
from ietf.ietfauth.utils import role_required, has_role
5656
from ietf.mailtrigger.utils import gather_address_lists
5757
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
58-
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Constraint, ConstraintName
58+
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment
5959
from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
6060
from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
6161
from ietf.meeting.helpers import get_all_assignments_from_schedule
@@ -77,6 +77,7 @@
7777
from ietf.meeting.utils import session_requested_by
7878
from ietf.meeting.utils import current_session_status
7979
from ietf.meeting.utils import data_for_meetings_overview
80+
from ietf.meeting.utils import preprocess_constraints_for_meeting_schedule_editor
8081
from ietf.message.utils import infer_message
8182
from ietf.secr.proceedings.utils import handle_upload_file
8283
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
@@ -623,70 +624,8 @@ def cubehelix(i, total, hue=1.2, start_angle=0.5):
623624
# requesters
624625
requested_by_lookup = {p.pk: p for p in Person.objects.filter(pk__in=set(s.requested_by for s in sessions if s.requested_by))}
625626

626-
# constraints - convert the human-readable rules in the database
627-
# to constraints on the actual sessions, compress them and output
628-
# them, so that the JS simply has to detect violations and show
629-
# the relevant preprocessed label
630-
constraints = Constraint.objects.filter(meeting=meeting)
631-
person_needed_for_groups = defaultdict(set)
632-
for c in constraints:
633-
if c.name_id == 'bethere' and c.person_id is not None:
634-
person_needed_for_groups[c.person_id].add(c.source_id)
635-
636-
sessions_for_group = defaultdict(list)
637-
for s in sessions:
638-
if s.group_id is not None:
639-
sessions_for_group[s.group_id].append(s.pk)
640-
641-
constraint_names = {n.pk: n for n in ConstraintName.objects.all()}
642-
constraint_names_with_count = set()
643-
constraint_label_replacements = [
644-
(re.compile(r"\(person\)"), lambda match_groups: format_html("<i class=\"fa fa-user-o\"></i>")),
645-
(re.compile(r"\(([^()])\)"), lambda match_groups: format_html("<span class=\"encircled\">{}</span>", match_groups[0])),
646-
]
647-
for n in list(constraint_names.values()):
648-
# spiff up the labels a bit
649-
for pattern, replacer in constraint_label_replacements:
650-
m = pattern.match(n.editor_label)
651-
if m:
652-
n.editor_label = replacer(m.groups())
653-
654-
# add reversed version of the name
655-
reverse_n = ConstraintName(
656-
slug=n.slug + "-reversed",
657-
name="{} - reversed".format(n.name),
658-
)
659-
reverse_n.editor_label = format_html("<i>{}</i>", n.editor_label)
660-
constraint_names[reverse_n.slug] = reverse_n
661-
662-
constraints_for_sessions = defaultdict(list)
663-
664-
def add_group_constraints(g1_pk, g2_pk, name_id, person_id):
665-
if g1_pk != g2_pk:
666-
for s1_pk in sessions_for_group.get(g1_pk, []):
667-
for s2_pk in sessions_for_group.get(g2_pk, []):
668-
if s1_pk != s2_pk:
669-
constraints_for_sessions[s1_pk].append((name_id, s2_pk, person_id))
670-
671-
reverse_constraints = []
672-
seen_forward_constraints_for_groups = set()
673-
674-
for c in constraints:
675-
if c.target_id:
676-
add_group_constraints(c.source_id, c.target_id, c.name_id, c.person_id)
677-
seen_forward_constraints_for_groups.add((c.source_id, c.target_id, c.name_id))
678-
reverse_constraints.append(c)
679-
680-
elif c.person_id:
681-
constraint_names_with_count.add(c.name_id)
682-
683-
for g in person_needed_for_groups.get(c.person_id):
684-
add_group_constraints(c.source_id, g, c.name_id, c.person_id)
685-
686-
for c in reverse_constraints:
687-
# suppress reverse constraints in case we have a forward one already
688-
if (c.target_id, c.source_id, c.name_id) not in seen_forward_constraints_for_groups:
689-
add_group_constraints(c.target_id, c.source_id, c.name_id + "-reversed", c.person_id)
627+
# constraints
628+
constraints_for_sessions, formatted_constraints_for_sessions, constraint_names = preprocess_constraints_for_meeting_schedule_editor(meeting, sessions)
690629

691630
unassigned_sessions = []
692631
for s in sessions:
@@ -705,22 +644,25 @@ def add_group_constraints(g1_pk, g2_pk, name_id, person_id):
705644
s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else ""
706645
s.historic_group_ad_name = ad_names.get(s.group_id)
707646

708-
# compress the constraints, so similar constraint explanations
709-
# are shared between the conflicting sessions they cover
710-
constrained_sessions_grouped_by_explanation = defaultdict(set)
647+
# compress the constraints, so similar constraint labels are
648+
# shared between the conflicting sessions they cover - the JS
649+
# then simply has to detect violations and show the
650+
# preprocessed labels
651+
constrained_sessions_grouped_by_label = defaultdict(set)
711652
for name_id, ts in itertools.groupby(sorted(constraints_for_sessions.get(s.pk, [])), key=lambda t: t[0]):
712653
ts = list(ts)
713654
session_pks = (t[1] for t in ts)
714655
constraint_name = constraint_names[name_id]
715-
if name_id in constraint_names_with_count:
656+
if "{count}" in constraint_name.formatted_editor_label:
716657
for session_pk, grouped_session_pks in itertools.groupby(session_pks):
717658
count = sum(1 for i in grouped_session_pks)
718-
constrained_sessions_grouped_by_explanation[format_html("{}{}", constraint_name.editor_label, count)].add(session_pk)
659+
constrained_sessions_grouped_by_label[format_html(constraint_name.formatted_editor_label, count=count)].add(session_pk)
719660

720661
else:
721-
constrained_sessions_grouped_by_explanation[constraint_name.editor_label].update(session_pks)
662+
constrained_sessions_grouped_by_label[constraint_name.formatted_editor_label].update(session_pks)
722663

723-
s.constrained_sessions = list(constrained_sessions_grouped_by_explanation.items())
664+
s.constrained_sessions = list(constrained_sessions_grouped_by_label.items())
665+
s.formatted_constraints = formatted_constraints_for_sessions.get(s.pk, {})
724666

725667
assigned = False
726668
for a in assignments_by_session.get(s.pk, []):
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Copyright The IETF Trust 2020, All Rights Reserved
2+
3+
from django.db import migrations
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
('name', '0011_constraintname_editor_label'),
9+
]
10+
11+
def update_editor_labels(apps, schema_editor):
12+
ConstraintName = apps.get_model('name', 'ConstraintName')
13+
for cn in ConstraintName.objects.all():
14+
cn.editor_label = {
15+
'bethere': "(person){count}",
16+
}.get(cn.slug, cn.editor_label)
17+
18+
cn.order = {
19+
'conflict': 1,
20+
'conflic2': 2,
21+
'conflic3': 3,
22+
'bethere': 4,
23+
'timerange': 5,
24+
'time_relation': 6,
25+
'wg_adjacent': 7,
26+
}.get(cn.slug, cn.order)
27+
28+
cn.save()
29+
30+
def noop(apps, schema_editor):
31+
pass
32+
33+
operations = [
34+
migrations.RunPython(update_editor_labels, noop, elidable=True),
35+
]

ietf/static/ietf/css/ietf.css

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,6 +1096,24 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
10961096
border-right: 2px dashed #fff; /* cut-off illusion */
10971097
}
10981098

1099+
.edit-meeting-schedule .timeslot.would-violate-hint {
1100+
background-color: #ffe0e0;
1101+
}
1102+
1103+
.edit-meeting-schedule .constraints .encircled,
1104+
.edit-meeting-schedule .formatted-constraints .encircled {
1105+
border: 1px solid #000;
1106+
border-radius: 1em;
1107+
min-width: 1.3em;
1108+
text-align: center;
1109+
display: inline-block;
1110+
}
1111+
1112+
.edit-meeting-schedule .formatted-constraints .encircled {
1113+
font-size: smaller;
1114+
cursor: help;
1115+
}
1116+
10991117
/* sessions */
11001118
.edit-meeting-schedule .session {
11011119
background-color: #fff;
@@ -1108,6 +1126,14 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
11081126
cursor: pointer;
11091127
}
11101128

1129+
.edit-meeting-schedule .session.selected {
1130+
cursor: grabbing;
1131+
}
1132+
1133+
.edit-meeting-schedule .read-only .session.selected {
1134+
cursor: default;
1135+
}
1136+
11111137
.edit-meeting-schedule .session.selected .session-label {
11121138
font-weight: bold;
11131139
}
@@ -1132,7 +1158,8 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
11321158
}
11331159

11341160
.edit-meeting-schedule .session.too-many-attendees .attendees {
1135-
color: #f33;
1161+
font-weight: bold;
1162+
color: #8432d4;
11361163
}
11371164

11381165
.edit-meeting-schedule .session .constraints {
@@ -1147,25 +1174,22 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
11471174
}
11481175

11491176
.edit-meeting-schedule .session .constraints > span .encircled {
1150-
border: 1px solid #f99;
1151-
border-radius: 1em;
1152-
min-width: 1.3em;
1153-
text-align: center;
1154-
display: inline-block;
1177+
border: 1px solid #b35eff;
11551178
}
11561179

11571180
.edit-meeting-schedule .session .constraints > span.violated-hint {
11581181
display: inline-block;
1159-
color: #f55;
1182+
color: #8432d4;
11601183
}
11611184

1162-
.edit-meeting-schedule .session .constraints > span.selected-hint {
1185+
.edit-meeting-schedule .session .constraints > span.would-violate-hint {
11631186
display: inline-block;
1164-
color: #8432d4;
1187+
font-weight: bold;
1188+
color: #f55;
11651189
}
11661190

1167-
.edit-meeting-schedule .session .constraints > span.selected-hint .encircled {
1168-
border: 1px solid #b35eff;
1191+
.edit-meeting-schedule .session .constraints > span.would-violate-hint .encircled {
1192+
border: 1px solid #f99;
11691193
}
11701194

11711195
.edit-meeting-schedule .unassigned-sessions .session .constraints > span {

0 commit comments

Comments
 (0)