Merged in /branch/iola/meeting-improvement-r17835@18048. This provides a new snapshot of the new schedule editor work, with improved edit page layout and details.
- Legacy-Id: 18358
This commit is contained in:
commit
07d60de46e
|
@ -88,14 +88,24 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
type_id='regular',
|
||||
location=room2,
|
||||
duration=datetime.timedelta(hours=2),
|
||||
time=slot1.time - datetime.timedelta(seconds=10 * 60),
|
||||
time=slot1.time - datetime.timedelta(minutes=10),
|
||||
)
|
||||
|
||||
slot3 = TimeSlot.objects.create(
|
||||
meeting=meeting,
|
||||
type_id='regular',
|
||||
location=room2,
|
||||
duration=datetime.timedelta(hours=2),
|
||||
time=max(slot1.end_time(), slot2.end_time()) + datetime.timedelta(minutes=10),
|
||||
)
|
||||
|
||||
s1, s2 = Session.objects.filter(meeting=meeting, type='regular')
|
||||
s2.requested_duration = slot2.duration + datetime.timedelta(minutes=10)
|
||||
s2.save()
|
||||
SchedTimeSessAssignment.objects.filter(session=s1).delete()
|
||||
|
||||
s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular')
|
||||
|
||||
Constraint.objects.create(
|
||||
meeting=meeting,
|
||||
source=s1.group,
|
||||
|
@ -108,19 +118,20 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
self.driver.get(url)
|
||||
|
||||
q = PyQuery(self.driver.page_source)
|
||||
self.assertEqual(len(q('.session')), 2)
|
||||
self.assertEqual(len(q('.session')), 3)
|
||||
|
||||
# select - show session info
|
||||
s2_element = self.driver.find_element_by_css_selector('#session{}'.format(s2.pk))
|
||||
s2_element.click()
|
||||
|
||||
session_info_element = self.driver.find_element_by_css_selector('.session-info-container label')
|
||||
self.assertIn(s2.group.acronym, session_info_element.text)
|
||||
session_info_container = self.driver.find_element_by_css_selector('.session-info-container')
|
||||
self.assertIn(s2.group.acronym, session_info_container.find_element_by_css_selector(".title").text)
|
||||
self.assertEqual(session_info_container.find_element_by_css_selector(".other-session .time").text, "not yet scheduled")
|
||||
|
||||
# deselect
|
||||
self.driver.find_element_by_css_selector('.session-info-container').click()
|
||||
self.driver.find_element_by_css_selector('.scheduling-panel').click()
|
||||
|
||||
self.assertEqual(self.driver.find_elements_by_css_selector('.session-info-container label'), [])
|
||||
self.assertEqual(session_info_container.find_elements_by_css_selector(".title"), [])
|
||||
|
||||
# unschedule
|
||||
|
||||
|
@ -139,39 +150,48 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
|
||||
self.driver.execute_script('!function(s){s.fn.simulateDragDrop=function(t){return this.each(function(){new s.simulateDragDrop(this,t)})},s.simulateDragDrop=function(t,a){this.options=a,this.simulateEvent(t,a)},s.extend(s.simulateDragDrop.prototype,{simulateEvent:function(t,a){var e="dragstart",n=this.createEvent(e);this.dispatchEvent(t,e,n),e="drop";var r=this.createEvent(e,{});r.dataTransfer=n.dataTransfer,this.dispatchEvent(s(a.dropTarget)[0],e,r),e="dragend";var i=this.createEvent(e,{});i.dataTransfer=n.dataTransfer,this.dispatchEvent(t,e,i)},createEvent:function(t){var a=document.createEvent("CustomEvent");return a.initCustomEvent(t,!0,!0,null),a.dataTransfer={data:{},setData:function(t,a){this.data[t]=a},getData:function(t){return this.data[t]}},a},dispatchEvent:function(t,a,e){t.dispatchEvent?t.dispatchEvent(e):t.fireEvent&&t.fireEvent("on"+a,e)}})}(jQuery);')
|
||||
|
||||
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '.unassigned-sessions'}});".format(s2.pk))
|
||||
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '.unassigned-sessions .drop-target'}});".format(s2.pk))
|
||||
|
||||
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.unassigned-sessions #session{}'.format(s2.pk))))
|
||||
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(session=s2, schedule=schedule)), [])
|
||||
|
||||
# sorting unassigned
|
||||
sorted_pks = [s.pk for s in sorted([s1, s2], key=lambda s: s.group.acronym)]
|
||||
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.acronym, s.requested_duration, s.pk))]
|
||||
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=name]').click()
|
||||
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions #session{} + #session{}'.format(*sorted_pks)))
|
||||
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{} + #session{}'.format(*sorted_pks)))
|
||||
|
||||
sorted_pks = [s.pk for s in sorted([s1, s2], key=lambda s: (s.group.parent.acronym, s.group.acronym))]
|
||||
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
|
||||
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=parent]').click()
|
||||
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions #session{} + #session{}'.format(*sorted_pks)))
|
||||
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
|
||||
|
||||
sorted_pks = [s.pk for s in sorted([s1, s2], key=lambda s: (s.requested_duration, s.group.parent.acronym, s.group.acronym))]
|
||||
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (s.requested_duration, s.group.parent.acronym, s.group.acronym, s.pk))]
|
||||
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=duration]').click()
|
||||
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions #session{} + #session{}'.format(*sorted_pks)))
|
||||
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
|
||||
|
||||
sorted_pks = [s.pk for s in sorted([s1, s2], key=lambda s: (bool(s.comments), s.group.parent.acronym, s.group.acronym))]
|
||||
sorted_pks = [s.pk for s in sorted([s1, s2, s2b], key=lambda s: (int(bool(s.comments)), s.group.parent.acronym, s.group.acronym, s.requested_duration, s.pk))]
|
||||
self.driver.find_element_by_css_selector('[name=sort_unassigned] option[value=comments]').click()
|
||||
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions #session{} + #session{}'.format(*sorted_pks)))
|
||||
self.assertTrue(self.driver.find_element_by_css_selector('.unassigned-sessions .drop-target #session{} + #session{}'.format(*sorted_pks)))
|
||||
|
||||
# schedule
|
||||
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{}'}});".format(s2.pk, slot1.pk))
|
||||
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot1.pk))
|
||||
|
||||
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1.pk, s2.pk))))
|
||||
|
||||
assignment = SchedTimeSessAssignment.objects.get(session=s2, schedule=schedule)
|
||||
self.assertEqual(assignment.timeslot, slot1)
|
||||
|
||||
# timeslot constraint hints when selected
|
||||
s1_element = self.driver.find_element_by_css_selector('#session{}'.format(s1.pk))
|
||||
s1_element.click()
|
||||
|
||||
# violated due to constraints
|
||||
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.would-violate-hint'.format(slot1.pk)))
|
||||
# violated due to missing capacity
|
||||
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.would-violate-hint'.format(slot3.pk)))
|
||||
|
||||
# reschedule
|
||||
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{}'}});".format(s2.pk, slot2.pk))
|
||||
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s2.pk, slot2.pk))
|
||||
|
||||
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot2.pk, s2.pk))))
|
||||
|
||||
|
@ -185,14 +205,12 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{}.overfull'.format(slot2.pk)))
|
||||
|
||||
# constraint hints
|
||||
s1_element = self.driver.find_element_by_css_selector('#session{}'.format(s1.pk))
|
||||
s1_element.click()
|
||||
|
||||
constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].selected-hint".format(s1.pk))
|
||||
constraint_element = s2_element.find_element_by_css_selector(".constraints span[data-sessions=\"{}\"].would-violate-hint".format(s1.pk))
|
||||
self.assertTrue(constraint_element.is_displayed())
|
||||
|
||||
# current constraint violations
|
||||
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{}'}});".format(s1.pk, slot1.pk))
|
||||
self.driver.execute_script("jQuery('#session{}').simulateDragDrop({{dropTarget: '#timeslot{} .drop-target'}});".format(s1.pk, slot1.pk))
|
||||
|
||||
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '#timeslot{} #session{}'.format(slot1.pk, s1.pk))))
|
||||
|
||||
|
@ -203,6 +221,16 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
self.assertTrue(s1_element.is_displayed())
|
||||
self.driver.find_element_by_css_selector(".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
|
||||
self.assertTrue(not s1_element.is_displayed())
|
||||
self.driver.find_element_by_css_selector(".session-parent-toggles [value=\"{}\"]".format(s1.group.parent.acronym)).click()
|
||||
self.assertTrue(s1_element.is_displayed())
|
||||
|
||||
# hide timeslots
|
||||
self.driver.find_element_by_css_selector(".timeslot-group-toggles button").click()
|
||||
self.assertTrue(self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
|
||||
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [value=\"{}\"]".format("ts-group-{}-{}".format(slot2.time.strftime("%Y%m%d-%H%M"), int(slot2.duration.total_seconds() / 60)))).click()
|
||||
self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal [data-dismiss=\"modal\"]").click()
|
||||
self.assertTrue(not self.driver.find_element_by_css_selector("#timeslot-group-toggles-modal").is_displayed())
|
||||
|
||||
|
||||
@skipIf(skip_selenium, skip_message)
|
||||
@skipIf(django.VERSION[0]==2, "Skipping test with race conditions under Django 2")
|
||||
|
|
|
@ -1057,7 +1057,7 @@ class EditTests(TestCase):
|
|||
name=ConstraintName.objects.get(slug="conflict"),
|
||||
)
|
||||
|
||||
p = Person.objects.all().first()
|
||||
p = Person.objects.order_by('pk')[1]
|
||||
|
||||
Constraint.objects.create(
|
||||
meeting=meeting,
|
||||
|
@ -1090,7 +1090,7 @@ class EditTests(TestCase):
|
|||
for s in [s1, s2]:
|
||||
e = q("#session{}".format(s.pk))
|
||||
|
||||
# info in the movable entity
|
||||
# info in the item representing the session that can be moved around
|
||||
self.assertIn(s.group.acronym, e.find(".session-label").text())
|
||||
if s.comments:
|
||||
self.assertTrue(e.find(".comments"))
|
||||
|
@ -1098,8 +1098,18 @@ class EditTests(TestCase):
|
|||
self.assertIn(str(s.attendees), e.find(".attendees").text())
|
||||
self.assertTrue(e.hasClass("parent-{}".format(s.group.parent.acronym)))
|
||||
|
||||
constraints = e.find(".constraints > span")
|
||||
s_other = s2 if s == s1 else s1
|
||||
self.assertEqual(len(constraints), 3)
|
||||
self.assertEqual(constraints.eq(0).attr("data-sessions"), str(s_other.pk))
|
||||
self.assertEqual(constraints.eq(0).find(".fa-user-o").parent().text(), "1") # 1 person in the constraint
|
||||
self.assertEqual(constraints.eq(1).attr("data-sessions"), str(s_other.pk))
|
||||
self.assertEqual(constraints.eq(1).find(".encircled").text(), "1" if s_other == s2 else "-1")
|
||||
self.assertEqual(constraints.eq(2).attr("data-sessions"), str(s_other.pk))
|
||||
self.assertEqual(constraints.eq(2).find(".encircled").text(), "AD")
|
||||
|
||||
# session info for the panel
|
||||
self.assertIn(str(s.requested_duration.total_seconds() / 60.0 / 60), e.find(".session-info label").text())
|
||||
self.assertIn(str(round(s.requested_duration.total_seconds() / 60.0 / 60, 1)), e.find(".session-info .title").text())
|
||||
|
||||
event = SchedulingEvent.objects.filter(session=s).order_by("id").first()
|
||||
if event:
|
||||
|
@ -1108,14 +1118,12 @@ class EditTests(TestCase):
|
|||
if s.comments:
|
||||
self.assertIn(s.comments, e.find(".comments").text())
|
||||
|
||||
# constraints
|
||||
constraints = e.find(".constraints > span")
|
||||
s_other = s2 if s == s1 else s1
|
||||
self.assertEqual(len(constraints), 2)
|
||||
self.assertEqual(constraints.eq(0).attr("data-sessions"), str(s_other.pk))
|
||||
self.assertEqual(constraints.eq(1).attr("data-sessions"), str(s_other.pk))
|
||||
self.assertEqual(constraints.find(".encircled").text(), "1")
|
||||
self.assertEqual(constraints.find(".fa-user-o").parent().text(), "1") # 1 person in the constraint
|
||||
formatted_constraints = e.find(".session-info .formatted-constraints > *")
|
||||
if s == s1:
|
||||
self.assertIn(s_other.group.acronym, formatted_constraints.eq(0).html())
|
||||
self.assertIn(p.name, formatted_constraints.eq(1).html())
|
||||
elif s == s2:
|
||||
self.assertIn(p.name, formatted_constraints.eq(0).html())
|
||||
|
||||
self.assertTrue(q("em:contains(\"You can't edit this schedule\")"))
|
||||
|
||||
|
|
|
@ -3,22 +3,28 @@
|
|||
|
||||
|
||||
import datetime
|
||||
import itertools
|
||||
import re
|
||||
import requests
|
||||
|
||||
from collections import defaultdict
|
||||
from urllib.error import HTTPError
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django.db.models.expressions import Subquery, OuterRef
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
from ietf.dbtemplate.models import DBTemplate
|
||||
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot
|
||||
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot, Constraint
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.group.utils import can_manage_materials
|
||||
from ietf.name.models import SessionStatusName
|
||||
from ietf.name.models import SessionStatusName, ConstraintName
|
||||
from ietf.nomcom.utils import DISQUALIFYING_ROLE_QUERY_EXPRESSION
|
||||
from ietf.person.models import Email
|
||||
from ietf.person.models import Person, Email
|
||||
from ietf.secr.proceedings.proc_utils import import_audio_files
|
||||
|
||||
def session_time_for_sorting(session, use_meeting_date):
|
||||
|
@ -309,3 +315,130 @@ def data_for_meetings_overview(meetings, interim_status=None):
|
|||
m.interim_meeting_cancelled = m.type_id == 'interim' and all(s.current_status == 'canceled' for s in m.sessions)
|
||||
|
||||
return meetings
|
||||
|
||||
def reverse_editor_label(label):
|
||||
reverse_sign = "-"
|
||||
|
||||
m = re.match(r"(<[^>]+>)([^<].*)", label)
|
||||
if m:
|
||||
return m.groups()[0] + reverse_sign + m.groups()[1]
|
||||
else:
|
||||
return reverse_sign + label
|
||||
|
||||
def preprocess_constraints_for_meeting_schedule_editor(meeting, sessions):
|
||||
# process constraint names - we synthesize extra names to be able
|
||||
# to treat the concepts in the same manner as the modelled ones
|
||||
constraint_names = {n.pk: n for n in ConstraintName.objects.all()}
|
||||
|
||||
joint_with_groups_constraint_name = ConstraintName(
|
||||
slug='joint_with_groups',
|
||||
name="Joint session with",
|
||||
editor_label="<i class=\"fa fa-clone\"></i>",
|
||||
order=8,
|
||||
)
|
||||
constraint_names[joint_with_groups_constraint_name.slug] = joint_with_groups_constraint_name
|
||||
|
||||
ad_constraint_name = ConstraintName(
|
||||
slug='responsible_ad',
|
||||
name="Responsible AD",
|
||||
editor_label="<span class=\"encircled\">AD</span>",
|
||||
order=9,
|
||||
)
|
||||
constraint_names[ad_constraint_name.slug] = ad_constraint_name
|
||||
|
||||
for n in list(constraint_names.values()):
|
||||
# add reversed version of the name
|
||||
reverse_n = ConstraintName(
|
||||
slug=n.slug + "-reversed",
|
||||
name="{} - reversed".format(n.name),
|
||||
)
|
||||
reverse_n.formatted_editor_label = mark_safe(reverse_editor_label(n.editor_label))
|
||||
constraint_names[reverse_n.slug] = reverse_n
|
||||
|
||||
n.formatted_editor_label = mark_safe(n.editor_label)
|
||||
n.countless_formatted_editor_label = format_html(n.formatted_editor_label, count="") if "{count}" in n.formatted_editor_label else n.formatted_editor_label
|
||||
|
||||
# convert human-readable rules in the database to constraints on actual sessions
|
||||
constraints = list(Constraint.objects.filter(meeting=meeting).prefetch_related('target', 'person', 'timeranges'))
|
||||
|
||||
# synthesize AD constraints - we can treat them as a special kind of 'bethere'
|
||||
responsible_ad_for_group = {}
|
||||
session_groups = set(s.group for s in sessions if s.group and s.group.parent and s.group.parent.type_id == 'area')
|
||||
meeting_time = datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0))
|
||||
|
||||
# dig up historic AD names
|
||||
for group_id, history_time, pk in Person.objects.filter(rolehistory__name='ad', rolehistory__group__group__in=session_groups, rolehistory__group__time__lte=meeting_time).values_list('rolehistory__group__group', 'rolehistory__group__time', 'pk').order_by('rolehistory__group__time'):
|
||||
responsible_ad_for_group[group_id] = pk
|
||||
for group_id, pk in Person.objects.filter(role__name='ad', role__group__in=session_groups, role__group__time__lte=meeting_time).values_list('role__group', 'pk'):
|
||||
responsible_ad_for_group[group_id] = pk
|
||||
|
||||
ad_person_lookup = {p.pk: p for p in Person.objects.filter(pk__in=set(responsible_ad_for_group.values()))}
|
||||
for group in session_groups:
|
||||
ad = ad_person_lookup.get(responsible_ad_for_group.get(group.pk))
|
||||
if ad is not None:
|
||||
constraints.append(Constraint(meeting=meeting, source=group, person=ad, name=ad_constraint_name))
|
||||
|
||||
# process must not be scheduled at the same time constraints
|
||||
constraints_for_sessions = defaultdict(list)
|
||||
|
||||
person_needed_for_groups = {cn.slug: defaultdict(set) for cn in constraint_names.values()}
|
||||
for c in constraints:
|
||||
if c.person_id is not None:
|
||||
person_needed_for_groups[c.name_id][c.person_id].add(c.source_id)
|
||||
|
||||
sessions_for_group = defaultdict(list)
|
||||
for s in sessions:
|
||||
if s.group_id is not None:
|
||||
sessions_for_group[s.group_id].append(s.pk)
|
||||
|
||||
def add_group_constraints(g1_pk, g2_pk, name_id, person_id):
|
||||
if g1_pk != g2_pk:
|
||||
for s1_pk in sessions_for_group.get(g1_pk, []):
|
||||
for s2_pk in sessions_for_group.get(g2_pk, []):
|
||||
if s1_pk != s2_pk:
|
||||
constraints_for_sessions[s1_pk].append((name_id, s2_pk, person_id))
|
||||
|
||||
reverse_constraints = []
|
||||
seen_forward_constraints_for_groups = set()
|
||||
|
||||
for c in constraints:
|
||||
if c.target_id and c.name_id != 'wg_adjacent':
|
||||
add_group_constraints(c.source_id, c.target_id, c.name_id, c.person_id)
|
||||
seen_forward_constraints_for_groups.add((c.source_id, c.target_id, c.name_id))
|
||||
reverse_constraints.append(c)
|
||||
|
||||
elif c.person_id:
|
||||
for g in person_needed_for_groups[c.name_id].get(c.person_id):
|
||||
add_group_constraints(c.source_id, g, c.name_id, c.person_id)
|
||||
|
||||
for c in reverse_constraints:
|
||||
# suppress reverse constraints in case we have a forward one already
|
||||
if (c.target_id, c.source_id, c.name_id) not in seen_forward_constraints_for_groups:
|
||||
add_group_constraints(c.target_id, c.source_id, c.name_id + "-reversed", c.person_id)
|
||||
|
||||
# formatted constraints
|
||||
def format_constraint(c):
|
||||
if c.name_id == "time_relation":
|
||||
return c.get_time_relation_display()
|
||||
elif c.name_id == "timerange":
|
||||
return ", ".join(t.desc for t in c.timeranges.all())
|
||||
elif c.person:
|
||||
return c.person.plain_name()
|
||||
elif c.target:
|
||||
return c.target.acronym
|
||||
else:
|
||||
return "UNKNOWN"
|
||||
|
||||
formatted_constraints_for_sessions = defaultdict(dict)
|
||||
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)):
|
||||
cs = list(cs)
|
||||
for s_pk in sessions_for_group.get(group_pk, []):
|
||||
formatted_constraints_for_sessions[s_pk][constraint_names[cn_pk]] = [format_constraint(c) for c in cs]
|
||||
|
||||
# synthesize joint_with_groups constraints
|
||||
for s in sessions:
|
||||
joint_groups = s.joint_with_groups.all()
|
||||
if joint_groups:
|
||||
formatted_constraints_for_sessions[s.pk][joint_with_groups_constraint_name] = [g.acronym for g in joint_groups]
|
||||
|
||||
return constraints_for_sessions, formatted_constraints_for_sessions, constraint_names
|
||||
|
|
|
@ -51,11 +51,10 @@ from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent, DocA
|
|||
from ietf.group.models import Group
|
||||
from ietf.group.utils import can_manage_session_materials, can_manage_some_groups, can_manage_group
|
||||
from ietf.person.models import Person
|
||||
from ietf.person.name import plain_name
|
||||
from ietf.ietfauth.utils import role_required, has_role
|
||||
from ietf.mailtrigger.utils import gather_address_lists
|
||||
from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission
|
||||
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment, Constraint, ConstraintName
|
||||
from ietf.meeting.models import SessionStatusName, SchedulingEvent, SchedTimeSessAssignment
|
||||
from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name
|
||||
from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list
|
||||
from ietf.meeting.helpers import get_all_assignments_from_schedule
|
||||
|
@ -78,6 +77,7 @@ from ietf.meeting.utils import session_time_for_sorting
|
|||
from ietf.meeting.utils import session_requested_by
|
||||
from ietf.meeting.utils import current_session_status
|
||||
from ietf.meeting.utils import data_for_meetings_overview
|
||||
from ietf.meeting.utils import preprocess_constraints_for_meeting_schedule_editor
|
||||
from ietf.message.utils import infer_message
|
||||
from ietf.secr.proceedings.utils import handle_upload_file
|
||||
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
|
||||
|
@ -483,11 +483,11 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
meeting=meeting,
|
||||
# Restrict graphical scheduling to regular meeting requests (Sessions) for now
|
||||
type='regular',
|
||||
),
|
||||
).order_by('pk'),
|
||||
requested_time=True,
|
||||
requested_by=True,
|
||||
).exclude(current_status__in=['notmeet', 'disappr', 'deleted', 'apprw']).prefetch_related(
|
||||
'resources', 'group', 'group__parent', 'group__type',
|
||||
'resources', 'group', 'group__parent', 'group__type', 'joint_with_groups',
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
|
@ -524,21 +524,23 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
for a in assignments:
|
||||
assignments_by_session[a.session_id].append(a)
|
||||
|
||||
# Prepare timeslot layout, making a timeline per day scaled in
|
||||
# browser em units to ensure that everything lines up even if the
|
||||
# timeslots are not the same in the different rooms
|
||||
# prepare timeslot layout
|
||||
|
||||
min_duration = min(t.duration for t in timeslots_qs)
|
||||
max_duration = max(t.duration for t in timeslots_qs)
|
||||
|
||||
def timedelta_to_css_ems(timedelta):
|
||||
css_ems_per_hour = 5
|
||||
return timedelta.seconds / 60.0 / 60.0 * css_ems_per_hour
|
||||
# we scale the session and slots a bit according to their
|
||||
# length for an added visual clue
|
||||
capped_min_d = max(min_duration, datetime.timedelta(minutes=30))
|
||||
capped_max_d = min(max_duration, datetime.timedelta(hours=4))
|
||||
capped_timedelta = min(max(capped_min_d, timedelta), capped_max_d)
|
||||
|
||||
timeslots_by_day = defaultdict(list)
|
||||
for t in timeslots_qs:
|
||||
timeslots_by_day[t.time.date()].append(t)
|
||||
|
||||
day_min_max = []
|
||||
for day, timeslots in sorted(timeslots_by_day.items()):
|
||||
day_min_max.append((day, min(t.time for t in timeslots), max(t.end_time() for t in timeslots)))
|
||||
min_d_css_rems = 8
|
||||
max_d_css_rems = 10
|
||||
# interpolate
|
||||
scale = (capped_timedelta - capped_min_d) / (capped_max_d - capped_min_d) if capped_min_d != capped_max_d else 1
|
||||
return min_d_css_rems + (max_d_css_rems - min_d_css_rems) * scale
|
||||
|
||||
timeslots_by_room_and_day = defaultdict(list)
|
||||
room_has_timeslots = set()
|
||||
|
@ -547,34 +549,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
timeslots_by_room_and_day[(t.location_id, t.time.date())].append(t)
|
||||
|
||||
days = []
|
||||
for day, day_min_time, day_max_time in day_min_max:
|
||||
day_labels = []
|
||||
day_width = timedelta_to_css_ems(day_max_time - day_min_time)
|
||||
|
||||
label_width = 4 # em
|
||||
|
||||
hourly_delta = 2
|
||||
|
||||
first_hour = int(math.ceil((day_min_time.hour + day_min_time.minute / 60.0) / hourly_delta) * hourly_delta)
|
||||
t = day_min_time.replace(hour=first_hour, minute=0, second=0, microsecond=0)
|
||||
|
||||
last_hour = int(math.floor((day_max_time.hour + day_max_time.minute / 60.0) / hourly_delta) * hourly_delta)
|
||||
end = day_max_time.replace(hour=last_hour, minute=0, second=0, microsecond=0)
|
||||
|
||||
while t <= end:
|
||||
left_offset = timedelta_to_css_ems(t - day_min_time)
|
||||
right_offset = day_width - left_offset
|
||||
if right_offset > label_width:
|
||||
# there's room for the label
|
||||
day_labels.append((t, 'left', left_offset))
|
||||
else:
|
||||
day_labels.append((t, 'right', right_offset))
|
||||
|
||||
t += datetime.timedelta(seconds=hourly_delta * 60 * 60)
|
||||
|
||||
if not day_labels:
|
||||
day_labels.append((day_min_time, 'left', 0))
|
||||
|
||||
for day in sorted(set(t.time.date() for t in timeslots_qs)):
|
||||
room_timeslots = []
|
||||
for r in rooms:
|
||||
if r.pk not in room_has_timeslots:
|
||||
|
@ -582,23 +557,24 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
|
||||
timeslots = []
|
||||
for t in timeslots_by_room_and_day.get((r.pk, day), []):
|
||||
timeslots.append({
|
||||
'timeslot': t,
|
||||
'offset': timedelta_to_css_ems(t.time - day_min_time),
|
||||
'width': timedelta_to_css_ems(t.end_time() - t.time),
|
||||
})
|
||||
t.layout_width = timedelta_to_css_ems(t.end_time() - t.time)
|
||||
timeslots.append(t)
|
||||
|
||||
room_timeslots.append((r, timeslots))
|
||||
|
||||
days.append({
|
||||
'day': day,
|
||||
'width': day_width,
|
||||
'time_labels': day_labels,
|
||||
'room_timeslots': room_timeslots,
|
||||
})
|
||||
|
||||
room_labels = [[r for r in rooms if r.pk in room_has_timeslots] for i in range(len(days))]
|
||||
|
||||
# possible timeslot start/ends
|
||||
timeslot_groups = defaultdict(set)
|
||||
for ts in timeslots_qs:
|
||||
ts.start_end_group = "ts-group-{}-{}".format(ts.time.strftime("%Y%m%d-%H%M"), int(ts.duration.total_seconds() / 60))
|
||||
timeslot_groups[ts.time.date()].add((ts.time, ts.end_time(), ts.start_end_group))
|
||||
|
||||
# prepare sessions
|
||||
for ts in timeslots_qs:
|
||||
ts.session_assignments = []
|
||||
|
@ -624,86 +600,18 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
), key=lambda p: p.acronym)
|
||||
for i, p in enumerate(session_parents):
|
||||
rgb_color = cubehelix(i, len(session_parents))
|
||||
p.scheduling_color = "#" + "".join( hex(int(round(x * 255)))[2:] for x in rgb_color)
|
||||
|
||||
# dig out historic AD names
|
||||
ad_names = {}
|
||||
session_groups = set(s.group for s in sessions if s.group and s.group.parent and s.group.parent.type_id == 'area')
|
||||
meeting_time = datetime.datetime.combine(meeting.date, datetime.time(0, 0, 0))
|
||||
|
||||
for group_id, history_time, name in Person.objects.filter(rolehistory__name='ad', rolehistory__group__group__in=session_groups, rolehistory__group__time__lte=meeting_time).values_list('rolehistory__group__group', 'rolehistory__group__time', 'name').order_by('rolehistory__group__time'):
|
||||
ad_names[group_id] = plain_name(name)
|
||||
|
||||
for group_id, name in Person.objects.filter(role__name='ad', role__group__in=session_groups, role__group__time__lte=meeting_time).values_list('role__group', 'name'):
|
||||
ad_names[group_id] = plain_name(name)
|
||||
p.scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round(x * 255)) for x in rgb_color))
|
||||
p.light_scheduling_color = "rgb({}, {}, {})".format(*tuple(int(round((0.9 + 0.1 * x) * 255)) for x in rgb_color))
|
||||
|
||||
# requesters
|
||||
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))}
|
||||
|
||||
# constraints - convert the human-readable rules in the database
|
||||
# to constraints on the actual sessions, compress them and output
|
||||
# them, so that the JS simply has to detect violations and show
|
||||
# the relevant preprocessed label
|
||||
constraints = Constraint.objects.filter(meeting=meeting)
|
||||
person_needed_for_groups = defaultdict(set)
|
||||
for c in constraints:
|
||||
if c.name_id == 'bethere' and c.person_id is not None:
|
||||
person_needed_for_groups[c.person_id].add(c.source_id)
|
||||
# constraints
|
||||
constraints_for_sessions, formatted_constraints_for_sessions, constraint_names = preprocess_constraints_for_meeting_schedule_editor(meeting, sessions)
|
||||
|
||||
sessions_for_group = defaultdict(list)
|
||||
for s in sessions:
|
||||
if s.group_id is not None:
|
||||
sessions_for_group[s.group_id].append(s.pk)
|
||||
|
||||
constraint_names = {n.pk: n for n in ConstraintName.objects.all()}
|
||||
constraint_names_with_count = set()
|
||||
constraint_label_replacements = [
|
||||
(re.compile(r"\(person\)"), lambda match_groups: format_html("<i class=\"fa fa-user-o\"></i>")),
|
||||
(re.compile(r"\(([^()])\)"), lambda match_groups: format_html("<span class=\"encircled\">{}</span>", match_groups[0])),
|
||||
]
|
||||
for n in list(constraint_names.values()):
|
||||
# spiff up the labels a bit
|
||||
for pattern, replacer in constraint_label_replacements:
|
||||
m = pattern.match(n.editor_label)
|
||||
if m:
|
||||
n.editor_label = replacer(m.groups())
|
||||
|
||||
# add reversed version of the name
|
||||
reverse_n = ConstraintName(
|
||||
slug=n.slug + "-reversed",
|
||||
name="{} - reversed".format(n.name),
|
||||
)
|
||||
reverse_n.editor_label = format_html("<i>{}</i>", n.editor_label)
|
||||
constraint_names[reverse_n.slug] = reverse_n
|
||||
|
||||
constraints_for_sessions = defaultdict(list)
|
||||
|
||||
def add_group_constraints(g1_pk, g2_pk, name_id, person_id):
|
||||
if g1_pk != g2_pk:
|
||||
for s1_pk in sessions_for_group.get(g1_pk, []):
|
||||
for s2_pk in sessions_for_group.get(g2_pk, []):
|
||||
if s1_pk != s2_pk:
|
||||
constraints_for_sessions[s1_pk].append((name_id, s2_pk, person_id))
|
||||
|
||||
reverse_constraints = []
|
||||
seen_forward_constraints_for_groups = set()
|
||||
|
||||
for c in constraints:
|
||||
if c.target_id:
|
||||
add_group_constraints(c.source_id, c.target_id, c.name_id, c.person_id)
|
||||
seen_forward_constraints_for_groups.add((c.source_id, c.target_id, c.name_id))
|
||||
reverse_constraints.append(c)
|
||||
|
||||
elif c.person_id:
|
||||
constraint_names_with_count.add(c.name_id)
|
||||
|
||||
for g in person_needed_for_groups.get(c.person_id):
|
||||
add_group_constraints(c.source_id, g, c.name_id, c.person_id)
|
||||
|
||||
for c in reverse_constraints:
|
||||
# suppress reverse constraints in case we have a forward one already
|
||||
if (c.target_id, c.source_id, c.name_id) not in seen_forward_constraints_for_groups:
|
||||
add_group_constraints(c.target_id, c.source_id, c.name_id + "-reversed", c.person_id)
|
||||
sessions_for_group[s.group_id].append(s)
|
||||
|
||||
unassigned_sessions = []
|
||||
for s in sessions:
|
||||
|
@ -715,29 +623,33 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
elif s.name:
|
||||
s.scheduling_label = s.name
|
||||
|
||||
s.requested_duration_in_hours = s.requested_duration.seconds / 60.0 / 60.0
|
||||
s.requested_duration_in_hours = round(s.requested_duration.seconds / 60.0 / 60.0, 1)
|
||||
|
||||
session_layout_margin = 0.2
|
||||
s.layout_width = timedelta_to_css_ems(s.requested_duration) - 2 * session_layout_margin
|
||||
s.parent_acronym = s.group.parent.acronym if s.group and s.group.parent else ""
|
||||
s.historic_group_ad_name = ad_names.get(s.group_id)
|
||||
|
||||
# compress the constraints, so similar constraint explanations
|
||||
# are shared between the conflicting sessions they cover
|
||||
constrained_sessions_grouped_by_explanation = defaultdict(set)
|
||||
# compress the constraints, so similar constraint labels are
|
||||
# shared between the conflicting sessions they cover - the JS
|
||||
# then simply has to detect violations and show the
|
||||
# preprocessed labels
|
||||
constrained_sessions_grouped_by_label = defaultdict(set)
|
||||
for name_id, ts in itertools.groupby(sorted(constraints_for_sessions.get(s.pk, [])), key=lambda t: t[0]):
|
||||
ts = list(ts)
|
||||
session_pks = (t[1] for t in ts)
|
||||
constraint_name = constraint_names[name_id]
|
||||
if name_id in constraint_names_with_count:
|
||||
if "{count}" in constraint_name.formatted_editor_label:
|
||||
for session_pk, grouped_session_pks in itertools.groupby(session_pks):
|
||||
count = sum(1 for i in grouped_session_pks)
|
||||
constrained_sessions_grouped_by_explanation[format_html("{}{}", constraint_name.editor_label, count)].add(session_pk)
|
||||
constrained_sessions_grouped_by_label[format_html(constraint_name.formatted_editor_label, count=count)].add(session_pk)
|
||||
|
||||
else:
|
||||
constrained_sessions_grouped_by_explanation[constraint_name.editor_label].update(session_pks)
|
||||
constrained_sessions_grouped_by_label[constraint_name.formatted_editor_label].update(session_pks)
|
||||
|
||||
s.constrained_sessions = list(constrained_sessions_grouped_by_explanation.items())
|
||||
s.constrained_sessions = list(constrained_sessions_grouped_by_label.items())
|
||||
s.formatted_constraints = formatted_constraints_for_sessions.get(s.pk, {})
|
||||
|
||||
s.other_sessions = [s_other for s_other in sessions_for_group.get(s.group_id) if s != s_other]
|
||||
|
||||
assigned = False
|
||||
for a in assignments_by_session.get(s.pk, []):
|
||||
|
@ -763,6 +675,7 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None):
|
|||
'js_data': json.dumps(js_data, indent=2),
|
||||
'days': days,
|
||||
'room_labels': room_labels,
|
||||
'timeslot_groups': sorted((d, list(sorted(t_groups))) for d, t_groups in timeslot_groups.items()),
|
||||
'unassigned_sessions': unassigned_sessions,
|
||||
'session_parents': session_parents,
|
||||
'hide_menu': True,
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,46 @@
|
|||
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('name', '0016_add_research_area_groups'),
|
||||
]
|
||||
|
||||
def update_editor_labels(apps, schema_editor):
|
||||
ConstraintName = apps.get_model('name', 'ConstraintName')
|
||||
for cn in ConstraintName.objects.all():
|
||||
cn.editor_label = {
|
||||
'bethere': "<i class=\"fa fa-user-o\"></i>{count}",
|
||||
'wg_adjacent': "<i class=\"fa fa-step-forward\"></i>",
|
||||
'conflict': "<span class=\"encircled\">1</span>",
|
||||
'conflic2': "<span class=\"encircled\">2</span>",
|
||||
'conflic3': "<span class=\"encircled\">3</span>",
|
||||
'time_relation': "Δ",
|
||||
'timerange': "<i class=\"fa fa-calendar-o\"></i>",
|
||||
}.get(cn.slug, cn.editor_label)
|
||||
|
||||
cn.order = {
|
||||
'conflict': 1,
|
||||
'conflic2': 2,
|
||||
'conflic3': 3,
|
||||
'bethere': 4,
|
||||
'timerange': 5,
|
||||
'time_relation': 6,
|
||||
'wg_adjacent': 7,
|
||||
}.get(cn.slug, cn.order)
|
||||
|
||||
cn.save()
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='constraintname',
|
||||
name='editor_label',
|
||||
field=models.CharField(blank=True, help_text='Very short label for producing warnings inline in the sessions in the schedule editor.', max_length=64),
|
||||
),
|
||||
migrations.RunPython(update_editor_labels, noop, elidable=True),
|
||||
]
|
|
@ -69,7 +69,7 @@ class TimeSlotTypeName(NameModel):
|
|||
class ConstraintName(NameModel):
|
||||
"""conflict, conflic2, conflic3, bethere, timerange, time_relation, wg_adjacent"""
|
||||
penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)")
|
||||
editor_label = models.CharField(max_length=32, blank=True, help_text="Very short label for producing warnings inline in the sessions in the schedule editor.")
|
||||
editor_label = models.CharField(max_length=64, blank=True, help_text="Very short label for producing warnings inline in the sessions in the schedule editor.")
|
||||
class TimerangeName(NameModel):
|
||||
"""(monday|tuesday|wednesday|thursday|friday)-(morning|afternoon-early|afternoon-late)"""
|
||||
class LiaisonStatementPurposeName(NameModel):
|
||||
|
|
|
@ -1011,11 +1011,11 @@ DEV_MIDDLEWARE = ()
|
|||
# page loading. If you wish to use the sql_queries debug listing, put this in
|
||||
# your settings_local and make sure your client IP address is in INTERNAL_IPS:
|
||||
#
|
||||
# DEV_TEMPLATE_CONTEXT_PROCESSORS = (
|
||||
# DEV_TEMPLATE_CONTEXT_PROCESSORS = [
|
||||
# 'ietf.context_processors.sql_debug',
|
||||
# )
|
||||
# ]
|
||||
#
|
||||
DEV_TEMPLATE_CONTEXT_PROCESSORS = ()
|
||||
DEV_TEMPLATE_CONTEXT_PROCESSORS = [] # type: List[str]
|
||||
|
||||
# Domain which hosts draft and wg alias lists
|
||||
DRAFT_ALIAS_DOMAIN = IETF_DOMAIN
|
||||
|
|
|
@ -1022,80 +1022,82 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .day {
|
||||
margin-right: 2.5em;
|
||||
margin-left: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .room-label-column .day {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .day-label {
|
||||
height: 3em;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .day-flow {
|
||||
margin-left: 8em;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 120em) {
|
||||
/* if there's only room for two days, it looks a bit odd with space-between */
|
||||
.edit-meeting-schedule .edit-grid .day-flow {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .day-flow .day-label {
|
||||
border-bottom: 2px solid #eee;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeline {
|
||||
.edit-meeting-schedule .edit-grid .timeslots {
|
||||
position: relative;
|
||||
height: 1.6em;
|
||||
height: 4.5em;
|
||||
padding-bottom: 0.15em;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeline > div {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeline.timeslots {
|
||||
height: 3.3em;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeline .time-label {
|
||||
font-size: smaller;
|
||||
border-left: 2px solid #eee;
|
||||
border-right: 2px solid #eee;
|
||||
padding: 0 0.2em;
|
||||
height: 1.3em;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeline .time-label.text-left {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeline .time-label.text-right {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .timeslot {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #eee;
|
||||
.edit-meeting-schedule .edit-grid .timeslot {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
background-color: #f4f4f4;
|
||||
height: 100%;
|
||||
border-bottom: 0.15em solid #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .timeslot.dropping {
|
||||
.edit-meeting-schedule .edit-grid .timeslot .time-label {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeslot .drop-target {
|
||||
position: relative; /* this is merely to make sure we are positioned above the time labels */
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeslot.dropping {
|
||||
background-color: #ccc;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .timeslot.overfull {
|
||||
.edit-meeting-schedule .edit-grid .timeslot.overfull {
|
||||
border-right: 2px dashed #fff; /* cut-off illusion */
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .timeslot.would-violate-hint {
|
||||
background-color: #ffe0e0;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .constraints .encircled,
|
||||
.edit-meeting-schedule .formatted-constraints .encircled {
|
||||
border: 1px solid #000;
|
||||
border-radius: 1em;
|
||||
padding: 0 0.3em;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .formatted-constraints .encircled {
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
/* sessions */
|
||||
.edit-meeting-schedule .session {
|
||||
background-color: #fff;
|
||||
|
@ -1109,11 +1111,27 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
}
|
||||
|
||||
.edit-meeting-schedule .session.selected {
|
||||
background-color: #fcfcfc;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .read-only .session.selected {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session.selected .session-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session.highlight {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session.highlight .session-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session.dragging {
|
||||
opacity: 0.3;
|
||||
opacity: 0.1;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
|
||||
|
@ -1122,13 +1140,23 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
margin-right: 0;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid, .edit-meeting-schedule .session {
|
||||
font-family: arial, helvetica, sans-serif;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session .session-label {
|
||||
flex-grow: 1;
|
||||
margin-left: 0.1em;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session .session-label.bof-session {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session.too-many-attendees .attendees {
|
||||
color: #f33;
|
||||
font-weight: bold;
|
||||
color: #8432d4;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session .constraints {
|
||||
|
@ -1143,25 +1171,22 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
}
|
||||
|
||||
.edit-meeting-schedule .session .constraints > span .encircled {
|
||||
border: 1px solid #f99;
|
||||
border-radius: 1em;
|
||||
min-width: 1.3em;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
border: 1px solid #b35eff;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session .constraints > span.violated-hint {
|
||||
display: inline-block;
|
||||
color: #f55;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session .constraints > span.selected-hint {
|
||||
display: inline-block;
|
||||
color: #8432d4;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session .constraints > span.selected-hint .encircled {
|
||||
border: 1px solid #b35eff;
|
||||
.edit-meeting-schedule .session .constraints > span.would-violate-hint {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
color: #f55;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session .constraints > span.would-violate-hint .encircled {
|
||||
border: 1px solid #f99;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .unassigned-sessions .session .constraints > span {
|
||||
|
@ -1180,6 +1205,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
left: 0;
|
||||
width: 100%;
|
||||
border-top: 0.2em solid #ccc;
|
||||
margin-bottom: 2em;
|
||||
background-color: #fff;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
@ -1189,14 +1215,11 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
}
|
||||
|
||||
.edit-meeting-schedule .unassigned-sessions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
margin-top: 0.5em;
|
||||
min-height: 4em;
|
||||
max-height: 13em;
|
||||
overflow-y: auto;
|
||||
background-color: #eee;
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .unassigned-sessions.dropping {
|
||||
|
@ -1204,6 +1227,12 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .unassigned-sessions .drop-target {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .scheduling-panel .preferences {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
@ -1217,6 +1246,21 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body {
|
||||
/*column-count: 3;*/
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body > * {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule #timeslot-group-toggles-modal .modal-body label {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session-parent-toggles {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
@ -1231,8 +1275,9 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
|
||||
.edit-meeting-schedule .scheduling-panel .session-info-container {
|
||||
padding-left: 0.5em;
|
||||
flex: 0 0 20em;
|
||||
max-height: 15em;
|
||||
flex: 0 0 25em;
|
||||
height: 20em;
|
||||
font-size: 14px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
@ -1240,3 +1285,7 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
font-style: italic;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .scheduling-panel .session-info-container .other-session:hover {
|
||||
cursor: default;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ jQuery(document).ready(function () {
|
|||
|
||||
let sessions = content.find(".session");
|
||||
let timeslots = content.find(".timeslot");
|
||||
let days = content.find(".day-flow .day");
|
||||
|
||||
// hack to work around lack of position sticky support in old browsers, see https://caniuse.com/#feat=css-sticky
|
||||
if (content.find(".scheduling-panel").css("position") != "sticky") {
|
||||
|
@ -17,13 +18,48 @@ jQuery(document).ready(function () {
|
|||
content.css("padding-bottom", "14em");
|
||||
}
|
||||
|
||||
function findTimeslotsOverlapping(intervals) {
|
||||
let res = [];
|
||||
|
||||
timeslots.each(function () {
|
||||
var timeslot = jQuery(this);
|
||||
let start = timeslot.data("start");
|
||||
let end = timeslot.data("end");
|
||||
|
||||
for (let i = 0; i < intervals.length; ++i) {
|
||||
if (end >= intervals[i][0] && intervals[i][1] >= start) {
|
||||
res.push(timeslot);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// selecting
|
||||
function selectSessionElement(element) {
|
||||
if (element) {
|
||||
sessions.not(element).removeClass("selected");
|
||||
jQuery(element).addClass("selected");
|
||||
showConstraintHints(element.id.slice("session".length));
|
||||
content.find(".scheduling-panel .session-info-container").html(jQuery(element).find(".session-info").html());
|
||||
|
||||
showConstraintHints(element);
|
||||
|
||||
let sessionInfoContainer = content.find(".scheduling-panel .session-info-container");
|
||||
sessionInfoContainer.html(jQuery(element).find(".session-info").html());
|
||||
|
||||
sessionInfoContainer.find("[data-original-title]").tooltip();
|
||||
|
||||
sessionInfoContainer.find(".time").text(jQuery(element).closest(".timeslot").data('scheduledatlabel'));
|
||||
|
||||
sessionInfoContainer.find(".other-session").each(function () {
|
||||
let scheduledAt = sessions.filter("#session" + this.dataset.othersessionid).closest(".timeslot").data('scheduledatlabel');
|
||||
let timeElement = jQuery(this).find(".time");
|
||||
if (scheduledAt)
|
||||
timeElement.text(timeElement.data("scheduled").replace("{time}", scheduledAt));
|
||||
else
|
||||
timeElement.text(timeElement.data("notscheduled"));
|
||||
});
|
||||
}
|
||||
else {
|
||||
sessions.removeClass("selected");
|
||||
|
@ -32,20 +68,49 @@ jQuery(document).ready(function () {
|
|||
}
|
||||
}
|
||||
|
||||
function showConstraintHints(sessionIdStr) {
|
||||
function showConstraintHints(selectedSession) {
|
||||
let sessionId = selectedSession ? selectedSession.id.slice("session".length) : null;
|
||||
// hints on the sessions
|
||||
sessions.find(".constraints > span").each(function () {
|
||||
if (!sessionIdStr) {
|
||||
jQuery(this).removeClass("selected-hint");
|
||||
if (!sessionId) {
|
||||
jQuery(this).removeClass("would-violate-hint");
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionIds = this.dataset.sessions;
|
||||
if (sessionIds)
|
||||
jQuery(this).toggleClass("selected-hint", sessionIds.split(",").indexOf(sessionIdStr) != -1);
|
||||
if (!sessionIds)
|
||||
return;
|
||||
|
||||
let wouldViolate = sessionIds.split(",").indexOf(sessionId) != -1;
|
||||
jQuery(this).toggleClass("would-violate-hint", wouldViolate);
|
||||
});
|
||||
|
||||
// hints on timeslots
|
||||
timeslots.removeClass("would-violate-hint");
|
||||
if (selectedSession) {
|
||||
let intervals = [];
|
||||
timeslots.filter(":has(.session .constraints > span.would-violate-hint)").each(function () {
|
||||
intervals.push([this.dataset.start, this.dataset.end]);
|
||||
});
|
||||
|
||||
let overlappingTimeslots = findTimeslotsOverlapping(intervals);
|
||||
for (let i = 0; i < overlappingTimeslots.length; ++i)
|
||||
overlappingTimeslots[i].addClass("would-violate-hint");
|
||||
|
||||
// check room sizes
|
||||
let attendees = +selectedSession.dataset.attendees;
|
||||
if (attendees) {
|
||||
timeslots.not(".would-violate-hint").each(function () {
|
||||
if (attendees > +jQuery(this).closest(".timeslots").data("roomcapacity"))
|
||||
jQuery(this).addClass("would-violate-hint");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
content.on("click", function (event) {
|
||||
if (jQuery(event.target).is(".session-info-container") || jQuery(event.target).closest(".session-info-container").length > 0)
|
||||
return;
|
||||
selectSessionElement(null);
|
||||
});
|
||||
|
||||
|
@ -71,13 +136,13 @@ jQuery(document).ready(function () {
|
|||
sessions.prop('draggable', true);
|
||||
|
||||
// dropping
|
||||
let dropElements = content.find(".timeslot,.unassigned-sessions");
|
||||
let dropElements = content.find(".timeslot .drop-target,.unassigned-sessions .drop-target");
|
||||
dropElements.on('dragenter', function (event) {
|
||||
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
|
||||
return;
|
||||
|
||||
event.preventDefault(); // default action is signalling that this is not a valid target
|
||||
jQuery(this).addClass("dropping");
|
||||
jQuery(this).parent().addClass("dropping");
|
||||
});
|
||||
|
||||
dropElements.on('dragover', function (event) {
|
||||
|
@ -92,11 +157,11 @@ jQuery(document).ready(function () {
|
|||
if (event.originalEvent.currentTarget.contains(event.originalEvent.relatedTarget))
|
||||
return;
|
||||
|
||||
jQuery(this).removeClass("dropping");
|
||||
jQuery(this).parent().removeClass("dropping");
|
||||
});
|
||||
|
||||
dropElements.on('drop', function (event) {
|
||||
jQuery(this).removeClass("dropping");
|
||||
jQuery(this).parent().removeClass("dropping");
|
||||
|
||||
let sessionId = event.originalEvent.dataTransfer.getData("text/plain");
|
||||
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
|
||||
|
@ -112,6 +177,7 @@ jQuery(document).ready(function () {
|
|||
return;
|
||||
|
||||
let dropElement = jQuery(this);
|
||||
let dropParent = dropElement.parent();
|
||||
|
||||
function done(response) {
|
||||
if (response != "OK") {
|
||||
|
@ -121,11 +187,11 @@ jQuery(document).ready(function () {
|
|||
|
||||
dropElement.append(sessionElement); // move element
|
||||
updateCurrentSchedulingHints();
|
||||
if (dropElement.hasClass("unassigned-sessions"))
|
||||
if (dropParent.hasClass("unassigned-sessions"))
|
||||
sortUnassigned();
|
||||
}
|
||||
|
||||
if (dropElement.hasClass("unassigned-sessions")) {
|
||||
if (dropParent.hasClass("unassigned-sessions")) {
|
||||
jQuery.ajax({
|
||||
url: ietfData.urls.assign,
|
||||
method: "post",
|
||||
|
@ -143,7 +209,7 @@ jQuery(document).ready(function () {
|
|||
data: {
|
||||
action: "assign",
|
||||
session: sessionId.slice("session".length),
|
||||
timeslot: dropElement.attr("id").slice("timeslot".length)
|
||||
timeslot: dropParent.attr("id").slice("timeslot".length)
|
||||
},
|
||||
timeout: 5 * 1000
|
||||
}).fail(failHandler).done(done);
|
||||
|
@ -153,7 +219,7 @@ jQuery(document).ready(function () {
|
|||
|
||||
// hints for the current schedule
|
||||
|
||||
function updateCurrentSessionConstraintViolations() {
|
||||
function updateSessionConstraintViolations() {
|
||||
// do a sweep on sessions sorted by start time
|
||||
let scheduledSessions = [];
|
||||
|
||||
|
@ -226,14 +292,14 @@ jQuery(document).ready(function () {
|
|||
|
||||
function updateAttendeesViolations() {
|
||||
sessions.each(function () {
|
||||
let roomCapacity = jQuery(this).closest(".timeline").data("roomcapacity");
|
||||
let roomCapacity = jQuery(this).closest(".timeslots").data("roomcapacity");
|
||||
if (roomCapacity && this.dataset.attendees)
|
||||
jQuery(this).toggleClass("too-many-attendees", +this.dataset.attendees > +roomCapacity);
|
||||
});
|
||||
}
|
||||
|
||||
function updateCurrentSchedulingHints() {
|
||||
updateCurrentSessionConstraintViolations();
|
||||
updateSessionConstraintViolations();
|
||||
updateAttendeesViolations();
|
||||
updateTimeSlotDurationViolations();
|
||||
}
|
||||
|
@ -273,6 +339,10 @@ jQuery(document).ready(function () {
|
|||
function sortUnassigned() {
|
||||
let sortBy = content.find("select[name=sort_unassigned]").val();
|
||||
|
||||
function extractId(e) {
|
||||
return e.id.slice("session".length);
|
||||
}
|
||||
|
||||
function extractName(e) {
|
||||
return e.querySelector(".session-label").innerHTML;
|
||||
}
|
||||
|
@ -291,15 +361,15 @@ jQuery(document).ready(function () {
|
|||
|
||||
let keyFunctions = [];
|
||||
if (sortBy == "name")
|
||||
keyFunctions = [extractName, extractDuration];
|
||||
keyFunctions = [extractName, extractDuration, extractId];
|
||||
else if (sortBy == "parent")
|
||||
keyFunctions = [extractParent, extractName, extractDuration];
|
||||
keyFunctions = [extractParent, extractName, extractDuration, extractId];
|
||||
else if (sortBy == "duration")
|
||||
keyFunctions = [extractDuration, extractParent, extractName];
|
||||
keyFunctions = [extractDuration, extractParent, extractName, extractId];
|
||||
else if (sortBy == "comments")
|
||||
keyFunctions = [extractComments, extractParent, extractName, extractDuration];
|
||||
keyFunctions = [extractComments, extractParent, extractName, extractDuration, extractId];
|
||||
|
||||
let unassignedSessionsContainer = content.find(".unassigned-sessions");
|
||||
let unassignedSessionsContainer = content.find(".unassigned-sessions .drop-target");
|
||||
|
||||
let sortedSessions = sortArrayWithKeyFunctions(unassignedSessionsContainer.children(".session").toArray(), keyFunctions);
|
||||
for (let i = 0; i < sortedSessions.length; ++i)
|
||||
|
@ -312,7 +382,7 @@ jQuery(document).ready(function () {
|
|||
|
||||
sortUnassigned();
|
||||
|
||||
// toggling of sessions
|
||||
// toggling visible sessions by session parents
|
||||
let sessionParentInputs = content.find(".session-parent-toggles input");
|
||||
|
||||
function updateSessionParentToggling() {
|
||||
|
@ -326,7 +396,32 @@ jQuery(document).ready(function () {
|
|||
}
|
||||
|
||||
sessionParentInputs.on("click", updateSessionParentToggling);
|
||||
|
||||
updateSessionParentToggling();
|
||||
|
||||
// toggling visible timeslots
|
||||
let timeslotGroupInputs = content.find("#timeslot-group-toggles-modal .modal-body input");
|
||||
function updateTimeslotGroupToggling() {
|
||||
let checked = [];
|
||||
timeslotGroupInputs.filter(":checked").each(function () {
|
||||
checked.push("." + this.value);
|
||||
});
|
||||
|
||||
timeslots.filter(checked.join(",")).removeClass("hidden");
|
||||
timeslots.not(checked.join(",")).addClass("hidden");
|
||||
|
||||
days.each(function () {
|
||||
jQuery(this).toggle(jQuery(this).find(".timeslot:not(.hidden)").length > 0);
|
||||
});
|
||||
}
|
||||
|
||||
timeslotGroupInputs.on("click change", updateTimeslotGroupToggling);
|
||||
updateTimeslotGroupToggling();
|
||||
|
||||
// session info
|
||||
content.find(".session-info-container").on("mouseover", ".other-session", function (event) {
|
||||
sessions.filter("#session" + this.dataset.othersessionid).addClass("highlight");
|
||||
}).on("mouseleave", ".other-session", function (event) {
|
||||
sessions.filter("#session" + this.dataset.othersessionid).removeClass("highlight");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -88,10 +88,7 @@
|
|||
<div class="container-fluid">
|
||||
{% comment %} {% bootstrap_messages %} {% endcomment %}
|
||||
{% for message in messages %}
|
||||
<div class="alert{% if message.level_tag %} alert-{% if message.level_tag == 'error' %}danger{% else %}{{ message.level_tag }}{% endif %}{% endif %}{% if message.extra_tags %} {{message.extra_tags}}{% endif %} alert-dismissable">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="alert{% if message.level_tag %} alert-{% if message.level_tag == 'error' %}danger{% else %}{{ message.level_tag }}{% endif %}{% endif %}{% if message.extra_tags %} {{message.extra_tags}}{% endif %} alert-dismissable"><button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% if request.COOKIES.left_menu == "on" and not hide_menu %} {# ugly hack for the more or less unported meeting agenda edit pages #}
|
||||
<div class="row">
|
||||
|
|
|
@ -5,10 +5,10 @@
|
|||
{% load ietf_filters %}
|
||||
|
||||
{% block morecss %}
|
||||
{# set the group colors with CSS here #}
|
||||
{% for parent in session_parents %}
|
||||
.parent-{{ parent.acronym }} {
|
||||
background: linear-gradient(to right, {{ parent.scheduling_color }} 0.4em, transparent 0.5em);
|
||||
}
|
||||
.parent-{{ parent.acronym }} { background: linear-gradient(to right, {{ parent.scheduling_color }} 0.4em, transparent 0.5em); }
|
||||
.parent-{{ parent.acronym }}.selected { background-color: {{ parent.light_scheduling_color }}; }
|
||||
{% endfor %}
|
||||
{% endblock morecss %}
|
||||
|
||||
|
@ -47,7 +47,7 @@
|
|||
{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="edit-grid">
|
||||
<div class="edit-grid {% if not can_edit %}read-only{% endif %}">
|
||||
|
||||
{# using the same markup in both room labels and the actual days ensures they are aligned #}
|
||||
<div class="room-label-column">
|
||||
|
@ -58,10 +58,8 @@
|
|||
|
||||
</div>
|
||||
|
||||
<div class="timeline"></div>
|
||||
|
||||
{% for room in labels %}
|
||||
<div class="timeline timeslots">
|
||||
<div class="timeslots">
|
||||
<div class="room-name">
|
||||
<strong>{{ room.name }}</strong><br>
|
||||
{% if room.capacity %}{{ room.capacity }} <i class="fa fa-user-o"></i>{% endif %}
|
||||
|
@ -74,26 +72,26 @@
|
|||
|
||||
<div class="day-flow">
|
||||
{% for day in days %}
|
||||
<div class="day" style="width: {{ day.width }}em;">
|
||||
<div class="day">
|
||||
<div class="day-label">
|
||||
<strong>{{ day.day|date:"l" }}</strong><br>
|
||||
{{ day.day|date:"N j, Y" }}
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
{% for t, left_or_right, offset in day.time_labels %}
|
||||
<div class="time-label text-{{ left_or_right }}" style="{{ left_or_right }}: {{ offset }}em;">{{ t|date:"H:i" }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% for room, timeslots in day.room_timeslots %}
|
||||
<div class="timeline timeslots" data-roomcapacity="{{ room.capacity }}">
|
||||
<div class="timeslots" data-roomcapacity="{{ room.capacity }}">
|
||||
|
||||
{% for t in timeslots %}
|
||||
<div id="timeslot{{ t.timeslot.pk }}" class="timeslot" data-start="{{ t.timeslot.time.isoformat }}" data-end="{{ t.timeslot.end_time.isoformat }}" data-duration="{{ t.timeslot.duration.total_seconds }}" style="left: {{ t.offset }}em; width: {{ t.width }}em;">
|
||||
{% for assignment, session in t.timeslot.session_assignments %}
|
||||
<div id="timeslot{{ t.pk }}" class="timeslot {{ t.start_end_group }}" data-start="{{ t.time.isoformat }}" data-end="{{ t.end_time.isoformat }}" data-duration="{{ t.duration.total_seconds }}" data-scheduledatlabel="{{ t.time|date:"l G:i" }}-{{ t.end_time|date:"G:i" }}" style="width: {{ t.layout_width }}rem;">
|
||||
<div class="time-label">
|
||||
{{ t.time|date:"G:i" }} - {{ t.end_time|date:"G:i" }}
|
||||
</div>
|
||||
|
||||
<div class="drop-target">
|
||||
{% for assignment, session in t.session_assignments %}
|
||||
{% include "meeting/edit_meeting_schedule_session.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -106,9 +104,11 @@
|
|||
<div class="scheduling-panel">
|
||||
<div class="unassigned-container">
|
||||
<div class="unassigned-sessions">
|
||||
{% for session in unassigned_sessions %}
|
||||
{% include "meeting/edit_meeting_schedule_session.html" %}
|
||||
{% endfor %}
|
||||
<div class="drop-target">
|
||||
{% for session in unassigned_sessions %}
|
||||
{% include "meeting/edit_meeting_schedule_session.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="preferences">
|
||||
|
@ -128,11 +128,44 @@
|
|||
<label class="parent-{{ p.acronym }}"><input type="checkbox" checked value="{{ p.acronym }}"> {{ p.acronym }}</label>
|
||||
{% endfor %}
|
||||
</span>
|
||||
|
||||
<span class="timeslot-group-toggles">
|
||||
<button class="btn btn-default" data-toggle="modal" data-target="#timeslot-group-toggles-modal"><input type="checkbox" checked="checked" disabled> Timeslots</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="session-info-container"></div>
|
||||
</div>
|
||||
|
||||
<div id="timeslot-group-toggles-modal" class="modal" role="dialog" aria-labelledby="timeslot-group-toggles-modal-title">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 class="modal-title" id="timeslot-group-toggles-modal-title">Displayed timeslots</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
{% for day, t_groups in timeslot_groups %}
|
||||
<div>
|
||||
<div><strong>{{ day|date:"M. d" }}</strong></div>
|
||||
{% for start, end, key in t_groups %}
|
||||
<label><input type="checkbox" name="timeslot-group" value="{{ key }}" checked="checked"> {{ start|date:"H:i" }} - {{ end|date:"H:i" }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
<div id="session{{ session.pk }}" class="session {% if not session.group.parent.scheduling_color %}untoggleable{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %}" style="width:{{ session.layout_width }}em;" data-duration="{{ session.requested_duration.total_seconds }}" {% if session.attendees != None %}data-attendees="{{ session.attendees }}"{% endif %}>
|
||||
<div class="session-label">
|
||||
<div class="session-label {% if session.group and session.group.is_bof %}bof-session{% endif %}">
|
||||
{{ session.scheduling_label }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="requested-duration">{{ session.requested_duration_in_hours|floatformat }}h</span>
|
||||
|
||||
{% if session.attendees != None %}
|
||||
<span class="attendees">{{ session.attendees }}</span>
|
||||
<span class="attendees">· {{ session.attendees }}</span>
|
||||
{% endif %}
|
||||
|
||||
{% if session.comments %}
|
||||
|
@ -14,35 +16,41 @@
|
|||
|
||||
{% if session.constrained_sessions %}
|
||||
<span class="constraints">
|
||||
{% for explanation, sessions in session.constrained_sessions %}
|
||||
<span data-sessions="{{ sessions|join:"," }}">{{ explanation }}</span>
|
||||
{% for label, sessions in session.constrained_sessions %}
|
||||
<span data-sessions="{{ sessions|join:"," }}">{{ label }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# this is shown elsewhere on the page with JS - we just include it here for convenience #}
|
||||
{# the JS uses this to display session information in the bottom panel #}
|
||||
<div class="session-info">
|
||||
<label>
|
||||
{{ session.scheduling_label }}
|
||||
· {{ session.requested_duration_in_hours }} h
|
||||
{% if session.group %}· {% if session.group.is_bof %}BoF{% else %}{{ session.group.type.name }}{% endif %}{% endif %}
|
||||
{% if session.attendees != None %}· {{ session.attendees }} <i class="fa fa-user-o"></i>{% endif %}
|
||||
</label>
|
||||
<div class="title">
|
||||
<strong>
|
||||
<span class="time pull-right"></span>
|
||||
{{ session.scheduling_label }}
|
||||
· {{ session.requested_duration_in_hours }}h
|
||||
{% if session.group %}
|
||||
· {% if session.group.is_bof %}BoF{% else %}{{ session.group.type.name }}{% endif %}
|
||||
{% endif %}
|
||||
{% if session.attendees != None %}
|
||||
· {{ session.attendees }} <i class="fa fa-user-o"></i>
|
||||
{% endif %}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
{% if session.group %}
|
||||
<div>
|
||||
{{ session.group.name }}
|
||||
{% if session.group.parent %}
|
||||
· <span class="session-parent">{{ session.group.parent.acronym }}</span>
|
||||
{% if session.historic_group_ad_name %} ({{ session.historic_group_ad_name }}){% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.requested_by_person %}
|
||||
<div>
|
||||
<i class="fa fa-user-circle-o"></i> {{ session.requested_by_person.plain_name }} {% if session.requested_time %}({{ session.requested_time|date:"Y-m-d" }}){% endif %}
|
||||
<i title="Requested by" class="fa fa-user-circle-o"></i> {{ session.requested_by_person.plain_name }} {% if session.requested_time %}({{ session.requested_time|date:"Y-m-d" }}){% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
@ -60,5 +68,19 @@
|
|||
{{ session.comments|linebreaksbr }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if session.formatted_constraints %}
|
||||
<div class="formatted-constraints">
|
||||
{% for constraint_name, values in session.formatted_constraints.items %}
|
||||
<div>
|
||||
<span title="{{ constraint_name.name }}">{{ constraint_name.countless_formatted_editor_label }}</span>: {{ values|join:", " }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% for s in session.other_sessions %}
|
||||
<div class="other-session" data-othersessionid="{{ s.pk }}"><i class="fa fa-calendar"></i> Other session <span class="time" data-scheduled="scheduled: {time}" data-notscheduled="not yet scheduled"></span></div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -9,10 +9,7 @@ register = template.Library()
|
|||
def timesum(value):
|
||||
"""
|
||||
Sum the times in a list of dicts; used for sql query debugging info"""
|
||||
sum = 0.0
|
||||
for v in value:
|
||||
sum += float(v['time'])
|
||||
return sum
|
||||
return round(sum(float(v['time']) for v in value), 3)
|
||||
|
||||
|
||||
@register.filter()
|
||||
|
|
Loading…
Reference in a new issue