New branch from trunk @ r18382 for meeting improvements, with iola/meeting-improvement-r17835 merged in
- Legacy-Id: 18385
This commit is contained in:
commit
a39fa7f967
ietf
bin
meeting
admin.pyhelpers.py
management/commands
migrations
0031_session_tombstone_for.py0032_schedule_notes.py0033_add_session_origin.py0034_add_schedule_base.py
models.pytest_data.pytests_js.pytests_views.pyurls.pyutils.pyviews.pyname
secr
meetings
proceedings
static/secr/css
templates/meetings
utils
static/ietf
templates/meeting
agenda.htmlagenda.txtagenda_by_room.htmlagenda_by_type.htmldiff_schedules.htmledit_meeting_schedule.htmledit_meeting_schedule_session.htmledit_meeting_timeslots_and_misc_sessions.htmledit_timeslot_form.htmlinterim_announcement.txtinterim_request_details.htmllandscape_edit.htmlnew_meeting_schedule.htmlprivate_schedule.htmlroom-view.htmlschedule_list.html
|
@ -33,11 +33,11 @@ for meeting in Meeting.objects.filter(type="ietf").order_by("date"):
|
|||
for schedule in meeting.schedule_set.all():
|
||||
print " Checking for missing Break and Reg sessions in %s" % schedule
|
||||
for timeslot in meeting.timeslot_set.all():
|
||||
if timeslot.type_id == 'break':
|
||||
assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule)
|
||||
if timeslot.type_id == 'break' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=brk, schedule=schedule.base).exists()):
|
||||
assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=brk, schedule=schedule)
|
||||
if created:
|
||||
print " Added %s break assignment" % timeslot
|
||||
if timeslot.type_id == 'reg':
|
||||
assignment, created = ScheduleTimeslotSSessionAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule)
|
||||
if timeslot.type_id == 'reg' and not (schedule.base and SchedTimeSessAssignment.objects.filter(timeslot=timeslot, session=reg, schedule=schedule.base).exists()):
|
||||
assignment, created = SchedTimeSessAssignment.objects.get_or_create(timeslot=timeslot, session=reg, schedule=schedule)
|
||||
if created:
|
||||
print " Added %s registration assignment" % timeslot
|
||||
|
|
|
@ -138,8 +138,8 @@ admin.site.register(SchedulingEvent, SchedulingEventAdmin)
|
|||
|
||||
class ScheduleAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "meeting", "owner", "visible", "public", "badness"]
|
||||
list_filter = ["meeting", ]
|
||||
raw_id_fields = ["meeting", "owner", ]
|
||||
list_filter = ["meeting"]
|
||||
raw_id_fields = ["meeting", "owner", "origin", "base"]
|
||||
search_fields = ["meeting__number", "name", "owner__name"]
|
||||
ordering = ["-meeting", "name"]
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import re
|
|||
from tempfile import mkstemp
|
||||
|
||||
from django.http import HttpRequest, Http404
|
||||
from django.db.models import Max, Q, Prefetch
|
||||
from django.db.models import F, Max, Q, Prefetch
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.urls import reverse
|
||||
|
@ -150,13 +150,6 @@ def get_schedule(meeting, name=None):
|
|||
schedule = get_object_or_404(meeting.schedule_set, name=name)
|
||||
return schedule
|
||||
|
||||
def get_schedule_by_id(meeting, schedid):
|
||||
if schedid is None:
|
||||
schedule = meeting.schedule
|
||||
else:
|
||||
schedule = get_object_or_404(meeting.schedule_set, id=int(schedid))
|
||||
return schedule
|
||||
|
||||
# seems this belongs in ietf/person/utils.py?
|
||||
def get_person_by_email(email):
|
||||
# email == None may actually match people who haven't set an email!
|
||||
|
@ -171,13 +164,17 @@ def get_schedule_by_name(meeting, owner, name):
|
|||
return meeting.schedule_set.filter(name = name).first()
|
||||
|
||||
def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefetches=()):
|
||||
assignments_queryset = assignments_queryset.select_related(
|
||||
"timeslot", "timeslot__location", "timeslot__type",
|
||||
).prefetch_related(
|
||||
assignments_queryset = assignments_queryset.prefetch_related(
|
||||
'timeslot', 'timeslot__type', 'timeslot__meeting',
|
||||
'timeslot__location', 'timeslot__location__floorplan', 'timeslot__location__urlresource_set',
|
||||
Prefetch(
|
||||
"session",
|
||||
queryset=add_event_info_to_session_qs(Session.objects.all().prefetch_related(
|
||||
'group', 'group__charter', 'group__charter__group',
|
||||
Prefetch('materials',
|
||||
queryset=Document.objects.exclude(states__type=F("type"), states__slug='deleted').order_by('sessionpresentation__order').prefetch_related('states'),
|
||||
to_attr='prefetched_active_materials'
|
||||
)
|
||||
))
|
||||
),
|
||||
*extra_prefetches
|
||||
|
@ -218,10 +215,21 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe
|
|||
parents = Group.objects.filter(pk__in=parent_id_set)
|
||||
parent_replacements = find_history_replacements_active_at(parents, meeting_time)
|
||||
|
||||
timeslot_by_session_pk = {a.session_id: a.timeslot for a in assignments}
|
||||
|
||||
for a in assignments:
|
||||
if a.session and a.session.historic_group and a.session.historic_group.parent_id:
|
||||
a.session.historic_group.historic_parent = parent_replacements.get(a.session.historic_group.parent_id)
|
||||
|
||||
if a.session.current_status == 'resched':
|
||||
a.session.rescheduled_to = timeslot_by_session_pk.get(a.session.tombstone_for_id)
|
||||
|
||||
for d in a.session.prefetched_active_materials:
|
||||
# make sure these are precomputed with the meeting instead
|
||||
# of having to look it up
|
||||
d.get_href(meeting=meeting)
|
||||
d.get_versionless_href(meeting=meeting)
|
||||
|
||||
return assignments
|
||||
|
||||
def read_session_file(type, num, doc):
|
||||
|
@ -423,6 +431,11 @@ def get_announcement_initial(meeting, is_change=False):
|
|||
type = group.type.slug.upper()
|
||||
if group.type.slug == 'wg' and group.state.slug == 'bof':
|
||||
type = 'BOF'
|
||||
|
||||
assignments = SchedTimeSessAssignment.objects.filter(
|
||||
schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
|
||||
).order_by('timeslot__time')
|
||||
|
||||
initial['subject'] = '{name} ({acronym}) {type} {desc} Meeting: {date}{change}'.format(
|
||||
name=group.name,
|
||||
acronym=group.acronym,
|
||||
|
|
|
@ -85,8 +85,11 @@ class Command(BaseCommand):
|
|||
date=datetime.date(2019, 11, 16),
|
||||
days=7,
|
||||
)
|
||||
schedule = Schedule.objects.create(meeting=m, name='Empty-Schedule', owner_id=1,
|
||||
visible=True, public=True)
|
||||
base_schedule = Schedule.objects.create(meeting=m, name='base', owner_id=1,
|
||||
visible=True, public=True)
|
||||
|
||||
schedule = Schedule.objects.create(meeting=m, name='first1', owner_id=1,
|
||||
visible=True, public=True, base=base_schedule)
|
||||
m.schedule = schedule
|
||||
m.save()
|
||||
|
||||
|
|
19
ietf/meeting/migrations/0031_session_tombstone_for.py
Normal file
19
ietf/meeting/migrations/0031_session_tombstone_for.py
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('meeting', '0030_allow_empty_joint_with_sessions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='session',
|
||||
name='tombstone_for',
|
||||
field=models.ForeignKey(blank=True, help_text='This session is the tombstone for a session that was rescheduled', null=True, on_delete=django.db.models.deletion.CASCADE, to='meeting.Session'),
|
||||
),
|
||||
]
|
28
ietf/meeting/migrations/0032_schedule_notes.py
Normal file
28
ietf/meeting/migrations/0032_schedule_notes.py
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 2.0.13 on 2020-07-01 02:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('meeting', '0031_session_tombstone_for'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='schedule',
|
||||
name='notes',
|
||||
field=models.TextField(blank=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='schedule',
|
||||
name='public',
|
||||
field=models.BooleanField(default=True, help_text='Allow others to see this agenda.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='schedule',
|
||||
name='visible',
|
||||
field=models.BooleanField(default=True, help_text='Show in the list of possible agendas for the meeting.', verbose_name='Show in agenda list'),
|
||||
),
|
||||
]
|
20
ietf/meeting/migrations/0033_add_session_origin.py
Normal file
20
ietf/meeting/migrations/0033_add_session_origin.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 2.0.13 on 2020-08-04 06:22
|
||||
|
||||
from django.db import migrations
|
||||
import django.db.models.deletion
|
||||
import ietf.utils.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('meeting', '0032_schedule_notes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='schedule',
|
||||
name='origin',
|
||||
field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='meeting.Schedule'),
|
||||
),
|
||||
]
|
25
ietf/meeting/migrations/0034_add_schedule_base.py
Normal file
25
ietf/meeting/migrations/0034_add_schedule_base.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Generated by Django 2.0.13 on 2020-08-07 09:30
|
||||
|
||||
from django.db import migrations
|
||||
import django.db.models.deletion
|
||||
import ietf.utils.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('meeting', '0033_add_session_origin'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='schedule',
|
||||
name='base',
|
||||
field=ietf.utils.models.ForeignKey(blank=True, help_text='Sessions scheduled in the base show up in this schedule.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='derivedschedule_set', to='meeting.Schedule'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='schedule',
|
||||
name='origin',
|
||||
field=ietf.utils.models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='meeting.Schedule'),
|
||||
),
|
||||
]
|
|
@ -297,7 +297,9 @@ class Meeting(models.Model):
|
|||
min_time = datetime.datetime(1970, 1, 1, 0, 0, 0) # should be Meeting.modified, but we don't have that
|
||||
timeslots_updated = self.timeslot_set.aggregate(Max('modified'))["modified__max"] or min_time
|
||||
sessions_updated = self.session_set.aggregate(Max('modified'))["modified__max"] or min_time
|
||||
assignments_updated = (self.schedule.assignments.aggregate(Max('modified'))["modified__max"] or min_time) if self.schedule else min_time
|
||||
assignments_updated = min_time
|
||||
if self.schedule:
|
||||
assignments_updated = SchedTimeSessAssignment.objects.filter(schedule__in=[self.schedule, self.schedule.base if self.schedule else None]).aggregate(Max('modified'))["modified__max"] or min_time
|
||||
ts = max(timeslots_updated, sessions_updated, assignments_updated)
|
||||
tz = pytz.timezone(settings.PRODUCTION_TIMEZONE)
|
||||
ts = tz.localize(ts)
|
||||
|
@ -397,14 +399,14 @@ class Room(models.Model):
|
|||
return self.functional_name
|
||||
# audio stream support
|
||||
def audio_stream_url(self):
|
||||
urlresource = self.urlresource_set.filter(name_id='audiostream').first()
|
||||
return urlresource.url if urlresource else None
|
||||
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id == 'audiostream']
|
||||
return urlresources[0].url if urlresources else None
|
||||
def video_stream_url(self):
|
||||
urlresource = self.urlresource_set.filter(name_id__in=['meetecho', ]).first()
|
||||
return urlresource.url if urlresource else None
|
||||
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id in ['meetecho']]
|
||||
return urlresources[0].url if urlresources else None
|
||||
def webex_url(self):
|
||||
urlresource = self.urlresource_set.filter(name_id__in=['webex', ]).first()
|
||||
return urlresource.url if urlresource else None
|
||||
urlresources = [ur for ur in self.urlresource_set.all() if ur.name_id in ['webex']]
|
||||
return urlresources[0].url if urlresources else None
|
||||
#
|
||||
class Meta:
|
||||
ordering = ["-id"]
|
||||
|
@ -456,7 +458,7 @@ class TimeSlot(models.Model):
|
|||
@property
|
||||
def session(self):
|
||||
if not hasattr(self, "_session_cache"):
|
||||
self._session_cache = self.sessions.filter(timeslotassignments__schedule=self.meeting.schedule).first()
|
||||
self._session_cache = self.sessions.filter(timeslotassignments__schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting else None]).first()
|
||||
return self._session_cache
|
||||
|
||||
@property
|
||||
|
@ -628,10 +630,14 @@ class Schedule(models.Model):
|
|||
meeting = ForeignKey(Meeting, null=True, related_name='schedule_set')
|
||||
name = models.CharField(max_length=16, blank=False, help_text="Letters, numbers and -:_ allowed.", validators=[RegexValidator(r'^[A-Za-z0-9-:_]*$')])
|
||||
owner = ForeignKey(Person)
|
||||
visible = models.BooleanField(default=True, help_text="Make this agenda available to those who know about it.")
|
||||
public = models.BooleanField(default=True, help_text="Make this agenda publically available.")
|
||||
visible = models.BooleanField("Show in agenda list", default=True, help_text="Show in the list of possible agendas for the meeting.")
|
||||
public = models.BooleanField(default=True, help_text="Allow others to see this agenda.")
|
||||
badness = models.IntegerField(null=True, blank=True)
|
||||
# considering copiedFrom = ForeignKey('Schedule', blank=True, null=True)
|
||||
notes = models.TextField(blank=True)
|
||||
origin = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL, related_name="+")
|
||||
base = ForeignKey('Schedule', blank=True, null=True, on_delete=models.SET_NULL,
|
||||
help_text="Sessions scheduled in the base schedule show up in this schedule too.", related_name="derivedschedule_set",
|
||||
limit_choices_to={'base': None}) # prevent the inheritance from being more than one layer deep (no recursion)
|
||||
|
||||
def __str__(self):
|
||||
return u"%s:%s(%s)" % (self.meeting, self.name, self.owner)
|
||||
|
@ -658,20 +664,6 @@ class Schedule(models.Model):
|
|||
else:
|
||||
return "noemail"
|
||||
|
||||
@property
|
||||
def visible_token(self):
|
||||
if self.visible:
|
||||
return "visible"
|
||||
else:
|
||||
return "hidden"
|
||||
|
||||
@property
|
||||
def public_token(self):
|
||||
if self.public:
|
||||
return "public"
|
||||
else:
|
||||
return "private"
|
||||
|
||||
@property
|
||||
def is_official(self):
|
||||
return (self.meeting.schedule == self)
|
||||
|
@ -943,6 +935,8 @@ class Session(models.Model):
|
|||
modified = models.DateTimeField(auto_now=True)
|
||||
remote_instructions = models.CharField(blank=True,max_length=1024)
|
||||
|
||||
tombstone_for = models.ForeignKey('Session', blank=True, null=True, help_text="This session is the tombstone for a session that was rescheduled", on_delete=models.CASCADE)
|
||||
|
||||
materials = models.ManyToManyField(Document, through=SessionPresentation, blank=True)
|
||||
resources = models.ManyToManyField(ResourceAssociation, blank=True)
|
||||
|
||||
|
@ -1084,7 +1078,7 @@ class Session(models.Model):
|
|||
ss0name = "(%s)" % SessionStatusName.objects.get(slug=status_id).name
|
||||
else:
|
||||
ss0name = "(unscheduled)"
|
||||
ss = self.timeslotassignments.filter(schedule=self.meeting.schedule).order_by('timeslot__time')
|
||||
ss = self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).order_by('timeslot__time')
|
||||
if ss:
|
||||
ss0name = ','.join(x.timeslot.time.strftime("%a-%H%M") for x in ss)
|
||||
return "%s: %s %s %s" % (self.meeting, self.group.acronym, self.name, ss0name)
|
||||
|
@ -1117,11 +1111,8 @@ class Session(models.Model):
|
|||
def reverse_constraints(self):
|
||||
return Constraint.objects.filter(target=self.group, meeting=self.meeting).order_by('name__name')
|
||||
|
||||
def timeslotassignment_for_schedule(self, schedule):
|
||||
return self.timeslotassignments.filter(schedule=schedule).first()
|
||||
|
||||
def official_timeslotassignment(self):
|
||||
return self.timeslotassignment_for_schedule(self.meeting.schedule)
|
||||
return self.timeslotassignments.filter(schedule__in=[self.meeting.schedule, self.meeting.schedule.base if self.meeting.schedule else None]).first()
|
||||
|
||||
def constraints_dict(self, host_scheme):
|
||||
constraint_list = []
|
||||
|
|
|
@ -78,8 +78,9 @@ def make_meeting_test_data(meeting=None, create_interims=False):
|
|||
|
||||
if not meeting:
|
||||
meeting = Meeting.objects.get(number="72", type="ietf")
|
||||
schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-schedule", visible=True, public=True)
|
||||
unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-schedule", visible=True, public=True)
|
||||
base_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="base", visible=True, public=True)
|
||||
schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-schedule", visible=True, public=True, base=base_schedule)
|
||||
unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-schedule", visible=True, public=True, base=base_schedule)
|
||||
|
||||
# test room
|
||||
pname = RoomResourceName.objects.create(name='projector',slug='proj')
|
||||
|
@ -148,7 +149,7 @@ def make_meeting_test_data(meeting=None, create_interims=False):
|
|||
requested_duration=datetime.timedelta(minutes=480),
|
||||
type_id="reg")
|
||||
SchedulingEvent.objects.create(session=reg_session, status_id='schedw', by=system_person)
|
||||
SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=schedule)
|
||||
SchedTimeSessAssignment.objects.create(timeslot=reg_slot, session=reg_session, schedule=base_schedule)
|
||||
|
||||
# Break
|
||||
break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"),
|
||||
|
@ -156,7 +157,7 @@ def make_meeting_test_data(meeting=None, create_interims=False):
|
|||
requested_duration=datetime.timedelta(minutes=30),
|
||||
type_id="break")
|
||||
SchedulingEvent.objects.create(session=break_session, status_id='schedw', by=system_person)
|
||||
SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=schedule)
|
||||
SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=base_schedule)
|
||||
|
||||
meeting.schedule = schedule
|
||||
meeting.save()
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
import time
|
||||
import datetime
|
||||
from pyquery import PyQuery
|
||||
from unittest import skipIf
|
||||
|
||||
import django
|
||||
|
@ -15,9 +14,11 @@ import debug # pyflakes:ignore
|
|||
|
||||
from ietf.doc.factories import DocumentFactory
|
||||
from ietf.group import colors
|
||||
from ietf.person.models import Person
|
||||
from ietf.meeting.factories import SessionFactory
|
||||
from ietf.meeting.test_data import make_meeting_test_data
|
||||
from ietf.meeting.models import Schedule, SchedTimeSessAssignment, Session, Room, TimeSlot, Constraint, ConstraintName
|
||||
from ietf.meeting.models import SchedulingEvent, SessionStatusName
|
||||
from ietf.utils.test_runner import IetfLiveServerTestCase
|
||||
from ietf.utils.pipe import pipe
|
||||
from ietf import settings
|
||||
|
@ -99,6 +100,14 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
time=max(slot1.end_time(), slot2.end_time()) + datetime.timedelta(minutes=10),
|
||||
)
|
||||
|
||||
slot4 = TimeSlot.objects.create(
|
||||
meeting=meeting,
|
||||
type_id='regular',
|
||||
location=room1,
|
||||
duration=datetime.timedelta(hours=2),
|
||||
time=slot1.time + datetime.timedelta(days=1),
|
||||
)
|
||||
|
||||
s1, s2 = Session.objects.filter(meeting=meeting, type='regular')
|
||||
s2.requested_duration = slot2.duration + datetime.timedelta(minutes=10)
|
||||
s2.save()
|
||||
|
@ -106,6 +115,12 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
|
||||
s2b = Session.objects.create(meeting=meeting, group=s2.group, attendees=10, requested_duration=datetime.timedelta(minutes=60), type_id='regular')
|
||||
|
||||
SchedulingEvent.objects.create(
|
||||
session=s2b,
|
||||
status=SessionStatusName.objects.get(slug='appr'),
|
||||
by=Person.objects.get(name='(System)'),
|
||||
)
|
||||
|
||||
Constraint.objects.create(
|
||||
meeting=meeting,
|
||||
source=s1.group,
|
||||
|
@ -117,8 +132,9 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
url = self.absreverse('ietf.meeting.views.edit_meeting_schedule', kwargs=dict(num=meeting.number, name=schedule.name, owner=schedule.owner_email()))
|
||||
self.driver.get(url)
|
||||
|
||||
q = PyQuery(self.driver.page_source)
|
||||
self.assertEqual(len(q('.session')), 3)
|
||||
WebDriverWait(self.driver, 2).until(expected_conditions.presence_of_element_located((By.CSS_SELECTOR, '.edit-meeting-schedule')))
|
||||
|
||||
self.assertEqual(len(self.driver.find_elements_by_css_selector('.session')), 3)
|
||||
|
||||
# select - show session info
|
||||
s2_element = self.driver.find_element_by_css_selector('#session{}'.format(s2.pk))
|
||||
|
@ -231,6 +247,14 @@ class EditMeetingScheduleTests(IetfLiveServerTestCase):
|
|||
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())
|
||||
|
||||
# swap days
|
||||
self.driver.find_element_by_css_selector(".day [data-target=\"#swap-days-modal\"][data-dayid=\"{}\"]".format(slot4.time.date().isoformat())).click()
|
||||
self.assertTrue(self.driver.find_element_by_css_selector("#swap-days-modal").is_displayed())
|
||||
self.driver.find_element_by_css_selector("#swap-days-modal input[name=\"target_day\"][value=\"{}\"]".format(slot1.time.date().isoformat())).click()
|
||||
self.driver.find_element_by_css_selector("#swap-days-modal button[type=\"submit\"]").click()
|
||||
|
||||
self.assertTrue(self.driver.find_elements_by_css_selector('#timeslot{} #session{}'.format(slot4.pk, s1.pk)))
|
||||
|
||||
|
||||
@skipIf(skip_selenium, skip_message)
|
||||
@skipIf(django.VERSION[0]==2, "Skipping test with race conditions under Django 2")
|
||||
|
|
|
@ -126,6 +126,8 @@ class MeetingTests(TestCase):
|
|||
future_meeting = Meeting.objects.create(date=datetime.date(future_year, 7, 22), number=future_num, type_id='ietf',
|
||||
city="Panama City", country="PA", time_zone='America/Panama')
|
||||
|
||||
registration_text = "Registration"
|
||||
|
||||
# utc
|
||||
time_interval = "%s-%s" % (slot.utc_start_time().strftime("%H:%M").lstrip("0"), (slot.utc_start_time() + slot.duration).strftime("%H:%M").lstrip("0"))
|
||||
|
||||
|
@ -151,6 +153,7 @@ class MeetingTests(TestCase):
|
|||
self.assertIn(session.group.parent.acronym.upper(), agenda_content)
|
||||
self.assertIn(slot.location.name, agenda_content)
|
||||
self.assertIn(time_interval, agenda_content)
|
||||
self.assertIn(registration_text, agenda_content)
|
||||
|
||||
# Make sure there's a frame for the agenda and it points to the right place
|
||||
self.assertTrue(any([session.materials.get(type='agenda').get_href() in x.attrib["data-src"] for x in q('tr div.modal-body div.frame')]))
|
||||
|
@ -190,6 +193,7 @@ class MeetingTests(TestCase):
|
|||
self.assertContains(r, session.group.name)
|
||||
self.assertContains(r, session.group.parent.acronym.upper())
|
||||
self.assertContains(r, slot.location.name)
|
||||
self.assertContains(r, registration_text)
|
||||
|
||||
self.assertContains(r, session.materials.get(type='agenda').uploaded_filename)
|
||||
self.assertContains(r, session.materials.filter(type='slides').exclude(states__type__slug='slides',states__slug='deleted').first().uploaded_filename)
|
||||
|
@ -214,6 +218,7 @@ class MeetingTests(TestCase):
|
|||
self.assertNotContains(r, 'CANCELLED')
|
||||
self.assertContains(r, session.group.acronym)
|
||||
self.assertContains(r, slot.location.name)
|
||||
self.assertContains(r, registration_text)
|
||||
|
||||
# week view with a cancelled session
|
||||
SchedulingEvent.objects.create(
|
||||
|
@ -672,16 +677,22 @@ class MeetingTests(TestCase):
|
|||
self.client.login(username='secretary',password='secretary+password')
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code,200)
|
||||
|
||||
new_base = Schedule.objects.create(name="newbase", owner=schedule.owner, meeting=schedule.meeting)
|
||||
response = self.client.post(url, {
|
||||
'name':schedule.name,
|
||||
'visible':True,
|
||||
'public':True,
|
||||
'notes': "New Notes",
|
||||
'base': new_base.pk,
|
||||
}
|
||||
)
|
||||
self.assertEqual(response.status_code,302)
|
||||
schedule = Schedule.objects.get(pk=schedule.pk)
|
||||
self.assertNoFormPostErrors(response)
|
||||
schedule.refresh_from_db()
|
||||
self.assertTrue(schedule.visible)
|
||||
self.assertTrue(schedule.public)
|
||||
self.assertEqual(schedule.notes, "New Notes")
|
||||
self.assertEqual(schedule.base_id, new_base.pk)
|
||||
|
||||
def test_agenda_by_type_ics(self):
|
||||
session=SessionFactory(meeting__type_id='ietf',type_id='lead')
|
||||
|
@ -1072,7 +1083,21 @@ class EditTests(TestCase):
|
|||
person=p,
|
||||
name=ConstraintName.objects.get(slug="bethere"),
|
||||
)
|
||||
|
||||
|
||||
room = Room.objects.get(meeting=meeting, session_types='regular')
|
||||
base_timeslot = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room,
|
||||
duration=datetime.timedelta(minutes=50),
|
||||
time=datetime.datetime.combine(meeting.date + datetime.timedelta(days=2), datetime.time(9, 30)))
|
||||
|
||||
timeslots = list(TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('time'))
|
||||
|
||||
base_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="irg"),
|
||||
attendees=20, requested_duration=datetime.timedelta(minutes=30),
|
||||
type_id='regular')
|
||||
SchedulingEvent.objects.create(session=base_session, status_id='schedw', by=Person.objects.get(user__username='secretary'))
|
||||
SchedTimeSessAssignment.objects.create(timeslot=base_timeslot, session=base_session, schedule=meeting.schedule.base)
|
||||
|
||||
|
||||
# check we have the grid and everything set up as a baseline -
|
||||
# the Javascript tests check that the Javascript can work with
|
||||
# it
|
||||
|
@ -1080,11 +1105,9 @@ class EditTests(TestCase):
|
|||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
|
||||
room = Room.objects.get(meeting=meeting, session_types='regular')
|
||||
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.name)))
|
||||
self.assertTrue(q(".room-name:contains(\"{}\")".format(room.capacity)))
|
||||
|
||||
timeslots = TimeSlot.objects.filter(meeting=meeting, type='regular')
|
||||
self.assertTrue(q("#timeslot{}".format(timeslots[0].pk)))
|
||||
|
||||
for s in [s1, s2]:
|
||||
|
@ -1118,12 +1141,14 @@ class EditTests(TestCase):
|
|||
if s.comments:
|
||||
self.assertIn(s.comments, e.find(".comments").text())
|
||||
|
||||
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())
|
||||
formatted_constraints1 = q("#session{} .session-info .formatted-constraints > *".format(s1.pk))
|
||||
self.assertIn(s2.group.acronym, formatted_constraints1.eq(0).html())
|
||||
self.assertIn(p.name, formatted_constraints1.eq(1).html())
|
||||
|
||||
formatted_constraints2 = q("#session{} .session-info .formatted-constraints > *".format(s2.pk))
|
||||
self.assertIn(p.name, formatted_constraints2.eq(0).html())
|
||||
|
||||
self.assertEqual(len(q("#session{}.readonly".format(base_session.pk))), 1)
|
||||
|
||||
self.assertTrue(q("em:contains(\"You can't edit this schedule\")"))
|
||||
|
||||
|
@ -1136,10 +1161,14 @@ class EditTests(TestCase):
|
|||
self.assertEqual(r.status_code, 403)
|
||||
|
||||
# turn us into owner
|
||||
meeting.schedule.owner = Person.objects.get(user__username="secretary")
|
||||
meeting.schedule.save()
|
||||
schedule = meeting.schedule
|
||||
schedule.owner = Person.objects.get(user__username="secretary")
|
||||
schedule.save()
|
||||
|
||||
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
|
||||
meeting.schedule = None
|
||||
meeting.save()
|
||||
|
||||
url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name=schedule.name))
|
||||
r = self.client.get(url)
|
||||
q = PyQuery(r.content)
|
||||
self.assertTrue(not q("em:contains(\"You can't edit this schedule\")"))
|
||||
|
@ -1152,51 +1181,287 @@ class EditTests(TestCase):
|
|||
'timeslot': timeslots[0].pk,
|
||||
'session': s1.pk,
|
||||
})
|
||||
self.assertEqual(r.content, b"OK")
|
||||
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=s1).timeslot, timeslots[0])
|
||||
self.assertEqual(json.loads(r.content)['success'], True)
|
||||
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
|
||||
|
||||
# move assignment
|
||||
# move assignment on unofficial schedule
|
||||
r = self.client.post(url, {
|
||||
'action': 'assign',
|
||||
'timeslot': timeslots[1].pk,
|
||||
'session': s1.pk,
|
||||
})
|
||||
self.assertEqual(r.content, b"OK")
|
||||
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=meeting.schedule, session=s1).timeslot, timeslots[1])
|
||||
self.assertEqual(json.loads(r.content)['success'], True)
|
||||
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[1])
|
||||
|
||||
# move assignment on official schedule, leaving tombstone
|
||||
meeting.schedule = schedule
|
||||
meeting.save()
|
||||
SchedulingEvent.objects.create(
|
||||
session=s1,
|
||||
status=SessionStatusName.objects.get(slug='sched'),
|
||||
by=Person.objects.get(name='(System)')
|
||||
)
|
||||
r = self.client.post(url, {
|
||||
'action': 'assign',
|
||||
'timeslot': timeslots[0].pk,
|
||||
'session': s1.pk,
|
||||
})
|
||||
json_content = json.loads(r.content)
|
||||
self.assertEqual(json_content['success'], True)
|
||||
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s1).timeslot, timeslots[0])
|
||||
|
||||
sessions_for_group = Session.objects.filter(group=s1.group, meeting=meeting)
|
||||
self.assertEqual(len(sessions_for_group), 2)
|
||||
s_tombstone = [s for s in sessions_for_group if s != s1][0]
|
||||
self.assertEqual(s_tombstone.tombstone_for, s1)
|
||||
tombstone_event = SchedulingEvent.objects.get(session=s_tombstone)
|
||||
self.assertEqual(tombstone_event.status_id, 'resched')
|
||||
|
||||
self.assertEqual(SchedTimeSessAssignment.objects.get(schedule=schedule, session=s_tombstone).timeslot, timeslots[1])
|
||||
self.assertTrue(PyQuery(json_content['tombstone'])("#session{}.readonly".format(s_tombstone.pk)).html())
|
||||
|
||||
# unassign
|
||||
r = self.client.post(url, {
|
||||
'action': 'unassign',
|
||||
'session': s1.pk,
|
||||
})
|
||||
self.assertEqual(r.content, b"OK")
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule, session=s1)), [])
|
||||
self.assertEqual(json.loads(r.content)['success'], True)
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1)), [])
|
||||
|
||||
# try swapping days
|
||||
SchedTimeSessAssignment.objects.create(schedule=schedule, session=s1, timeslot=timeslots[0])
|
||||
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
|
||||
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2, timeslot=timeslots[1])), 1)
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
|
||||
|
||||
def test_copy_meeting_schedule(self):
|
||||
r = self.client.post(url, {
|
||||
'action': 'swapdays',
|
||||
'source_day': timeslots[0].time.date().isoformat(),
|
||||
'target_day': timeslots[2].time.date().isoformat(),
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[0])), [])
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), [])
|
||||
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[2])), 1)
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s2)), [])
|
||||
|
||||
# swap back
|
||||
r = self.client.post(url, {
|
||||
'action': 'swapdays',
|
||||
'source_day': timeslots[2].time.date().isoformat(),
|
||||
'target_day': timeslots[0].time.date().isoformat(),
|
||||
})
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
self.assertEqual(len(SchedTimeSessAssignment.objects.filter(schedule=schedule, session=s1, timeslot=timeslots[0])), 1)
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[1])), [])
|
||||
self.assertEqual(list(SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot=timeslots[2])), [])
|
||||
|
||||
def test_edit_meeting_timeslots_and_misc_sessions(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
|
||||
url = urlreverse("ietf.meeting.views.copy_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
|
||||
# check we have the grid and everything set up as a baseline -
|
||||
# the Javascript tests check that the Javascript can work with
|
||||
# it
|
||||
url = urlreverse("ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions", kwargs=dict(num=meeting.number, owner=meeting.schedule.base.owner_email(), name=meeting.schedule.base.name))
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
|
||||
breakfast_room = Room.objects.get(meeting=meeting, name="Breakfast Room")
|
||||
break_room = Room.objects.get(meeting=meeting, name="Break Area")
|
||||
reg_room = Room.objects.get(meeting=meeting, name="Registration Area")
|
||||
|
||||
for i in range(meeting.days):
|
||||
self.assertTrue(q("[data-day=\"{}\"]".format((meeting.date + datetime.timedelta(days=i)).isoformat())))
|
||||
|
||||
self.assertTrue(q(".room-label:contains(\"{}\")".format(breakfast_room.name)))
|
||||
self.assertTrue(q(".room-label:contains(\"{}\")".format(break_room.name)))
|
||||
self.assertTrue(q(".room-label:contains(\"{}\")".format(reg_room.name)))
|
||||
|
||||
break_slot = TimeSlot.objects.get(location=break_room, type='break')
|
||||
|
||||
room_row = q(".room-row[data-day=\"{}\"][data-room=\"{}\"]".format(break_slot.time.date().isoformat(), break_slot.location_id))
|
||||
self.assertTrue(room_row)
|
||||
self.assertTrue(room_row.find("#timeslot{}".format(break_slot.pk)))
|
||||
|
||||
self.assertTrue(q(".timeslot-form"))
|
||||
|
||||
# add timeslot
|
||||
ietf_group = Group.objects.get(acronym='ietf')
|
||||
|
||||
# copy
|
||||
r = self.client.post(url, {
|
||||
'name': "newtest",
|
||||
'public': "on",
|
||||
'day': meeting.date,
|
||||
'time': '08:30',
|
||||
'duration': '1:30',
|
||||
'location': break_room.pk,
|
||||
'show_location': 'on',
|
||||
'type': 'other',
|
||||
'group': ietf_group.pk,
|
||||
'name': "IETF Testing",
|
||||
'short': "ietf-testing",
|
||||
'scroll': 1234,
|
||||
'action': 'add-timeslot',
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
self.assertIn("#scroll=1234", r['Location'])
|
||||
|
||||
test_timeslot = TimeSlot.objects.get(meeting=meeting, name="IETF Testing")
|
||||
self.assertEqual(test_timeslot.time, datetime.datetime.combine(meeting.date, datetime.time(8, 30)))
|
||||
self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1, minutes=30))
|
||||
self.assertEqual(test_timeslot.location_id, break_room.pk)
|
||||
self.assertEqual(test_timeslot.show_location, True)
|
||||
self.assertEqual(test_timeslot.type_id, 'other')
|
||||
|
||||
test_session = Session.objects.get(meeting=meeting, timeslotassignments__timeslot=test_timeslot)
|
||||
self.assertEqual(test_session.short, 'ietf-testing')
|
||||
self.assertEqual(test_session.group, ietf_group)
|
||||
|
||||
self.assertTrue(SchedulingEvent.objects.filter(session=test_session, status='sched'))
|
||||
|
||||
# edit timeslot
|
||||
r = self.client.get(url, {
|
||||
'timeslot': test_timeslot.pk,
|
||||
'action': 'edit-timeslot',
|
||||
})
|
||||
self.assertEqual(r.status_code, 200)
|
||||
edit_form_html = json.loads(r.content)['form']
|
||||
q = PyQuery(edit_form_html)
|
||||
self.assertEqual(q("[name=name]").val(), test_timeslot.name)
|
||||
self.assertEqual(q("[name=location]").val(), str(test_timeslot.location_id))
|
||||
self.assertEqual(q("[name=timeslot]").val(), str(test_timeslot.pk))
|
||||
self.assertEqual(q("[name=type]").val(), str(test_timeslot.type_id))
|
||||
self.assertEqual(q("[name=group]").val(), str(ietf_group.pk))
|
||||
|
||||
iab_group = Group.objects.get(acronym='iab')
|
||||
|
||||
r = self.client.post(url, {
|
||||
'timeslot': test_timeslot.pk,
|
||||
'day': meeting.date,
|
||||
'time': '09:30',
|
||||
'duration': '1:00',
|
||||
'location': breakfast_room.pk,
|
||||
'type': 'other',
|
||||
'group': iab_group.pk,
|
||||
'name': "IETF Testing 2",
|
||||
'short': "ietf-testing2",
|
||||
'action': 'edit-timeslot',
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
test_timeslot.refresh_from_db()
|
||||
self.assertEqual(test_timeslot.time, datetime.datetime.combine(meeting.date, datetime.time(9, 30)))
|
||||
self.assertEqual(test_timeslot.duration, datetime.timedelta(hours=1))
|
||||
self.assertEqual(test_timeslot.location_id, breakfast_room.pk)
|
||||
self.assertEqual(test_timeslot.show_location, False)
|
||||
self.assertEqual(test_timeslot.type_id, 'other')
|
||||
|
||||
test_session.refresh_from_db()
|
||||
self.assertEqual(test_session.short, 'ietf-testing2')
|
||||
self.assertEqual(test_session.group, iab_group)
|
||||
|
||||
# cancel timeslot
|
||||
r = self.client.post(url, {
|
||||
'timeslot': test_timeslot.pk,
|
||||
'action': 'cancel-timeslot',
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
|
||||
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='newtest')
|
||||
event = SchedulingEvent.objects.filter(
|
||||
session__timeslotassignments__timeslot=test_timeslot
|
||||
).order_by('-id').first()
|
||||
self.assertEqual(event.status_id, 'canceled')
|
||||
|
||||
# delete timeslot
|
||||
test_presentation = Document.objects.create(name='slides-test', type_id='slides')
|
||||
SessionPresentation.objects.create(
|
||||
document=test_presentation,
|
||||
rev='1',
|
||||
session=test_session
|
||||
)
|
||||
|
||||
r = self.client.post(url, {
|
||||
'timeslot': test_timeslot.pk,
|
||||
'action': 'delete-timeslot',
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
|
||||
self.assertEqual(list(TimeSlot.objects.filter(pk=test_timeslot.pk)), [])
|
||||
self.assertEqual(list(Session.objects.filter(pk=test_session.pk)), [])
|
||||
self.assertEqual(test_presentation.get_state_slug(), 'deleted')
|
||||
|
||||
# set agenda note
|
||||
assignment = SchedTimeSessAssignment.objects.filter(session__group__acronym='mars', schedule=meeting.schedule).first()
|
||||
|
||||
url = urlreverse("ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
|
||||
|
||||
r = self.client.post(url, {
|
||||
'timeslot': assignment.timeslot_id,
|
||||
'day': assignment.timeslot.time.date().isoformat(),
|
||||
'time': assignment.timeslot.time.time().isoformat(),
|
||||
'duration': assignment.timeslot.duration,
|
||||
'location': assignment.timeslot.location_id,
|
||||
'type': assignment.timeslot.type_id,
|
||||
'name': assignment.timeslot.name,
|
||||
'agenda_note': "New Test Note",
|
||||
'action': 'edit-timeslot',
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
|
||||
assignment.session.refresh_from_db()
|
||||
self.assertEqual(assignment.session.agenda_note, "New Test Note")
|
||||
|
||||
|
||||
def test_new_meeting_schedule(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
|
||||
# new from scratch
|
||||
url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number))
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(url, {
|
||||
'name': "scratch",
|
||||
'public': "on",
|
||||
'visible': "on",
|
||||
'notes': "New scratch",
|
||||
'base': meeting.schedule.base_id,
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
|
||||
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='scratch')
|
||||
self.assertEqual(new_schedule.public, True)
|
||||
self.assertEqual(new_schedule.visible, True)
|
||||
self.assertEqual(new_schedule.notes, "New scratch")
|
||||
self.assertEqual(new_schedule.origin, None)
|
||||
self.assertEqual(new_schedule.base_id, meeting.schedule.base_id)
|
||||
|
||||
# copy
|
||||
url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=meeting.schedule.owner_email(), name=meeting.schedule.name))
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
r = self.client.post(url, {
|
||||
'name': "copy",
|
||||
'public': "on",
|
||||
'notes': "New copy",
|
||||
'base': meeting.schedule.base_id,
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
|
||||
new_schedule = Schedule.objects.get(meeting=meeting, owner__user__username='secretary', name='copy')
|
||||
self.assertEqual(new_schedule.public, True)
|
||||
self.assertEqual(new_schedule.visible, False)
|
||||
self.assertEqual(new_schedule.notes, "New copy")
|
||||
self.assertEqual(new_schedule.origin, meeting.schedule)
|
||||
self.assertEqual(new_schedule.base_id, meeting.schedule.base_id)
|
||||
|
||||
old_assignments = {(a.session_id, a.timeslot_id) for a in SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule)}
|
||||
for a in SchedTimeSessAssignment.objects.filter(schedule=new_schedule):
|
||||
self.assertIn((a.session_id, a.timeslot_id), old_assignments)
|
||||
# FIXME: test extendedfrom is copied correctly
|
||||
|
||||
def test_save_agenda_as_and_read_permissions(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
@ -1407,7 +1672,7 @@ class SessionDetailsTests(TestCase):
|
|||
class EditScheduleListTests(TestCase):
|
||||
def setUp(self):
|
||||
self.mtg = MeetingFactory(type_id='ietf')
|
||||
ScheduleFactory(meeting=self.mtg,name='Empty-Schedule')
|
||||
ScheduleFactory(meeting=self.mtg, name='secretary1')
|
||||
|
||||
def test_list_schedules(self):
|
||||
url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number})
|
||||
|
@ -1415,6 +1680,75 @@ class EditScheduleListTests(TestCase):
|
|||
r = self.client.get(url)
|
||||
self.assertTrue(r.status_code, 200)
|
||||
|
||||
def test_diff_schedules(self):
|
||||
meeting = make_meeting_test_data()
|
||||
|
||||
url = urlreverse('ietf.meeting.views.diff_schedules',kwargs={'num':meeting.number})
|
||||
login_testing_unauthorized(self,"secretary", url)
|
||||
r = self.client.get(url)
|
||||
self.assertTrue(r.status_code, 200)
|
||||
|
||||
from_schedule = Schedule.objects.get(meeting=meeting, name="test-unofficial-schedule")
|
||||
|
||||
session1 = Session.objects.filter(meeting=meeting, group__acronym='mars').first()
|
||||
session2 = Session.objects.filter(meeting=meeting, group__acronym='ames').first()
|
||||
session3 = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym='mars'),
|
||||
attendees=10, requested_duration=datetime.timedelta(minutes=70),
|
||||
type_id='regular')
|
||||
SchedulingEvent.objects.create(session=session3, status_id='schedw', by=Person.objects.first())
|
||||
|
||||
slot2 = TimeSlot.objects.filter(meeting=meeting, type='regular').order_by('-time').first()
|
||||
slot3 = TimeSlot.objects.create(
|
||||
meeting=meeting, type_id='regular', location=slot2.location,
|
||||
duration=datetime.timedelta(minutes=60),
|
||||
time=slot2.time + datetime.timedelta(minutes=60),
|
||||
)
|
||||
|
||||
# copy
|
||||
new_url = urlreverse("ietf.meeting.views.new_meeting_schedule", kwargs=dict(num=meeting.number, owner=from_schedule.owner_email(), name=from_schedule.name))
|
||||
r = self.client.post(new_url, {
|
||||
'name': "newtest",
|
||||
'public': "on",
|
||||
})
|
||||
self.assertNoFormPostErrors(r)
|
||||
|
||||
to_schedule = Schedule.objects.get(meeting=meeting, name='newtest')
|
||||
|
||||
# make some changes
|
||||
|
||||
edit_url = urlreverse("ietf.meeting.views.edit_meeting_schedule", kwargs=dict(num=meeting.number, owner=to_schedule.owner_email(), name=to_schedule.name))
|
||||
|
||||
# schedule session
|
||||
r = self.client.post(edit_url, {
|
||||
'action': 'assign',
|
||||
'timeslot': slot3.pk,
|
||||
'session': session3.pk,
|
||||
})
|
||||
self.assertEqual(json.loads(r.content)['success'], True)
|
||||
# unschedule session
|
||||
r = self.client.post(edit_url, {
|
||||
'action': 'unassign',
|
||||
'session': session1.pk,
|
||||
})
|
||||
self.assertEqual(json.loads(r.content)['success'], True)
|
||||
# move session
|
||||
r = self.client.post(edit_url, {
|
||||
'action': 'assign',
|
||||
'timeslot': slot2.pk,
|
||||
'session': session2.pk,
|
||||
})
|
||||
self.assertEqual(json.loads(r.content)['success'], True)
|
||||
|
||||
# now get differences
|
||||
r = self.client.get(url, {
|
||||
'from_schedule': from_schedule.name,
|
||||
'to_schedule': to_schedule.name,
|
||||
})
|
||||
self.assertTrue(r.status_code, 200)
|
||||
|
||||
q = PyQuery(r.content)
|
||||
self.assertEqual(len(q(".schedule-diffs tr")), 3)
|
||||
|
||||
def test_delete_schedule(self):
|
||||
url = urlreverse('ietf.meeting.views.delete_schedule',
|
||||
kwargs={'num':self.mtg.number,
|
||||
|
@ -1530,6 +1864,7 @@ class InterimTests(TestCase):
|
|||
meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting
|
||||
meeting.time_zone = 'America/Los_Angeles'
|
||||
meeting.save()
|
||||
|
||||
url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number})
|
||||
login_testing_unauthorized(self, "secretary", url)
|
||||
r = self.client.get(url)
|
||||
|
|
|
@ -28,6 +28,7 @@ safe_for_all_meeting_types = [
|
|||
type_ietf_only_patterns = [
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/edit$' % settings.URL_REGEXPS, views.edit_schedule),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/edit/$' % settings.URL_REGEXPS, views.edit_meeting_schedule),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/timeslots/$' % settings.URL_REGEXPS, views.edit_meeting_timeslots_and_misc_sessions),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/details$' % settings.URL_REGEXPS, views.edit_schedule_properties),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/delete$' % settings.URL_REGEXPS, views.delete_schedule),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/make_official$' % settings.URL_REGEXPS, views.make_schedule_official),
|
||||
|
@ -41,13 +42,15 @@ type_ietf_only_patterns = [
|
|||
url(r'^agenda/%(owner)s/%(schedule_name)s/session/(?P<assignment_id>\d+).json$' % settings.URL_REGEXPS, ajax.assignment_json),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/sessions.json$' % settings.URL_REGEXPS, ajax.assignments_json),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s.json$' % settings.URL_REGEXPS, ajax.schedule_infourl),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/copy/$' % settings.URL_REGEXPS, views.copy_meeting_schedule),
|
||||
url(r'^agenda/%(owner)s/%(schedule_name)s/new/$' % settings.URL_REGEXPS, views.new_meeting_schedule),
|
||||
url(r'^agenda/by-room$', views.agenda_by_room),
|
||||
url(r'^agenda/by-type$', views.agenda_by_type),
|
||||
url(r'^agenda/by-type/(?P<type>[a-z]+)$', views.agenda_by_type),
|
||||
url(r'^agenda/by-type/(?P<type>[a-z]+)/ics$', views.agenda_by_type_ics),
|
||||
url(r'^agendas/list$', views.list_schedules),
|
||||
url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
|
||||
url(r'^agendas/diff/$', views.diff_schedules),
|
||||
url(r'^agenda/new/$', views.new_meeting_schedule),
|
||||
url(r'^timeslots/edit$', views.edit_timeslots),
|
||||
url(r'^timeslot/(?P<slot_id>\d+)/edittype$', views.edit_timeslot_type),
|
||||
url(r'^rooms$', ajax.timeslot_roomsurl),
|
||||
|
|
|
@ -19,7 +19,7 @@ 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, Constraint
|
||||
from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot, Constraint, SchedTimeSessAssignment
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.group.utils import can_manage_materials
|
||||
from ietf.name.models import SessionStatusName, ConstraintName
|
||||
|
@ -28,7 +28,7 @@ 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):
|
||||
official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule=session.meeting.schedule).first()
|
||||
official_timeslot = TimeSlot.objects.filter(sessionassignments__session=session, sessionassignments__schedule__in=[session.meeting.schedule, session.meeting.schedule.base if session.meeting.schedule else None]).first()
|
||||
if official_timeslot:
|
||||
return official_timeslot.time
|
||||
elif use_meeting_date and session.meeting.date:
|
||||
|
@ -442,3 +442,121 @@ def preprocess_constraints_for_meeting_schedule_editor(meeting, sessions):
|
|||
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
|
||||
|
||||
|
||||
def diff_meeting_schedules(from_schedule, to_schedule):
|
||||
"""Compute the difference between the two meeting schedules as a list
|
||||
describing the set of actions that will turn the schedule of from into
|
||||
the schedule of to, like:
|
||||
|
||||
[
|
||||
{'change': 'schedule', 'session': session_id, 'to': timeslot_id},
|
||||
{'change': 'move', 'session': session_id, 'from': timeslot_id, 'to': timeslot_id2},
|
||||
{'change': 'unschedule', 'session': session_id, 'from': timeslot_id},
|
||||
]
|
||||
|
||||
Uses .assignments.all() so that it can be prefetched.
|
||||
"""
|
||||
diffs = []
|
||||
|
||||
from_session_timeslots = {
|
||||
a.session_id: a.timeslot_id
|
||||
for a in from_schedule.assignments.all()
|
||||
}
|
||||
|
||||
session_ids_in_to = set()
|
||||
|
||||
for a in to_schedule.assignments.all():
|
||||
session_ids_in_to.add(a.session_id)
|
||||
|
||||
from_timeslot_id = from_session_timeslots.get(a.session_id)
|
||||
|
||||
if from_timeslot_id is None:
|
||||
diffs.append({'change': 'schedule', 'session': a.session_id, 'to': a.timeslot_id})
|
||||
elif a.timeslot_id != from_timeslot_id:
|
||||
diffs.append({'change': 'move', 'session': a.session_id, 'from': from_timeslot_id, 'to': a.timeslot_id})
|
||||
|
||||
for from_session_id, from_timeslot_id in from_session_timeslots.items():
|
||||
if from_session_id not in session_ids_in_to:
|
||||
diffs.append({'change': 'unschedule', 'session': from_session_id, 'from': from_timeslot_id})
|
||||
|
||||
return diffs
|
||||
|
||||
|
||||
def prefetch_schedule_diff_objects(diffs):
|
||||
session_ids = set()
|
||||
timeslot_ids = set()
|
||||
|
||||
for d in diffs:
|
||||
session_ids.add(d['session'])
|
||||
|
||||
if d['change'] == 'schedule':
|
||||
timeslot_ids.add(d['to'])
|
||||
elif d['change'] == 'move':
|
||||
timeslot_ids.add(d['from'])
|
||||
timeslot_ids.add(d['to'])
|
||||
elif d['change'] == 'unschedule':
|
||||
timeslot_ids.add(d['from'])
|
||||
|
||||
session_lookup = {s.pk: s for s in Session.objects.filter(pk__in=session_ids)}
|
||||
timeslot_lookup = {t.pk: t for t in TimeSlot.objects.filter(pk__in=timeslot_ids).prefetch_related('location')}
|
||||
|
||||
res = []
|
||||
for d in diffs:
|
||||
d_objs = {
|
||||
'change': d['change'],
|
||||
'session': session_lookup.get(d['session'])
|
||||
}
|
||||
|
||||
if d['change'] == 'schedule':
|
||||
d_objs['to'] = timeslot_lookup.get(d['to'])
|
||||
elif d['change'] == 'move':
|
||||
d_objs['from'] = timeslot_lookup.get(d['from'])
|
||||
d_objs['to'] = timeslot_lookup.get(d['to'])
|
||||
elif d['change'] == 'unschedule':
|
||||
d_objs['from'] = timeslot_lookup.get(d['from'])
|
||||
|
||||
res.append(d_objs)
|
||||
|
||||
return res
|
||||
|
||||
def swap_meeting_schedule_timeslot_assignments(schedule, source_timeslots, target_timeslots, source_target_offset):
|
||||
"""Swap the assignments of the two meeting schedule timeslots in one
|
||||
go, automatically matching them up based on the specified offset,
|
||||
e.g. timedelta(days=1). For timeslots where no suitable swap match
|
||||
is found, the sessions are unassigned. Doesn't take tombstones into
|
||||
account."""
|
||||
|
||||
assignments_by_timeslot = defaultdict(list)
|
||||
|
||||
for a in SchedTimeSessAssignment.objects.filter(schedule=schedule, timeslot__in=source_timeslots + target_timeslots):
|
||||
assignments_by_timeslot[a.timeslot_id].append(a)
|
||||
|
||||
timeslots_to_match_up = [(source_timeslots, target_timeslots, source_target_offset), (target_timeslots, source_timeslots, -source_target_offset)]
|
||||
for lhs_timeslots, rhs_timeslots, lhs_offset in timeslots_to_match_up:
|
||||
timeslots_by_location = defaultdict(list)
|
||||
for rts in rhs_timeslots:
|
||||
timeslots_by_location[rts.location_id].append(rts)
|
||||
|
||||
for lts in lhs_timeslots:
|
||||
lts_assignments = assignments_by_timeslot.pop(lts.pk, [])
|
||||
if not lts_assignments:
|
||||
continue
|
||||
|
||||
swapped = False
|
||||
|
||||
most_overlapping_rts, max_overlap = max([
|
||||
(rts, max(datetime.timedelta(0), min(lts.end_time() + lhs_offset, rts.end_time()) - max(lts.time + lhs_offset, rts.time)))
|
||||
for rts in timeslots_by_location.get(lts.location_id, [])
|
||||
] + [(None, datetime.timedelta(0))], key=lambda t: t[1])
|
||||
|
||||
if max_overlap > datetime.timedelta(minutes=5):
|
||||
for a in lts_assignments:
|
||||
a.timeslot = most_overlapping_rts
|
||||
a.modified = datetime.datetime.now()
|
||||
a.save()
|
||||
swapped = True
|
||||
|
||||
if not swapped:
|
||||
for a in lts_assignments:
|
||||
a.delete()
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12077,6 +12077,16 @@
|
|||
"model": "name.sessionstatusname",
|
||||
"pk": "schedw"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
"name": "Rescheduled",
|
||||
"order": 0,
|
||||
"used": true
|
||||
},
|
||||
"model": "name.sessionstatusname",
|
||||
"pk": "resched"
|
||||
},
|
||||
{
|
||||
"fields": {
|
||||
"desc": "",
|
||||
|
|
24
ietf/name/migrations/0018_add_rescheduled_session_name.py
Normal file
24
ietf/name/migrations/0018_add_rescheduled_session_name.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('name', '0017_update_constraintname_order_and_label'),
|
||||
]
|
||||
|
||||
def add_rescheduled_session_status_name(apps, schema_editor):
|
||||
SessionStatusName = apps.get_model('name', 'SessionStatusName')
|
||||
SessionStatusName.objects.get_or_create(
|
||||
slug='resched',
|
||||
name="Rescheduled",
|
||||
)
|
||||
|
||||
def noop(apps, schema_editor):
|
||||
pass
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_rescheduled_session_status_name, noop, elidable=True),
|
||||
]
|
|
@ -65,7 +65,7 @@ class SecrMeetingTestCase(TestCase):
|
|||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertEqual(len(q('#id_schedule_selector option')),3)
|
||||
self.assertEqual({option.get('value') for option in q('#id_schedule_selector option:not([value=""])')}, {'base', 'test-schedule', 'test-unofficial-schedule'})
|
||||
|
||||
def test_add_meeting(self):
|
||||
"Add Meeting"
|
||||
|
@ -92,6 +92,9 @@ class SecrMeetingTestCase(TestCase):
|
|||
new_meeting = Meeting.objects.get(number=number)
|
||||
|
||||
self.assertTrue(new_meeting.schedule)
|
||||
self.assertEqual(new_meeting.schedule.name, 'secretary1')
|
||||
self.assertTrue(new_meeting.schedule.base)
|
||||
self.assertEqual(new_meeting.schedule.base.name, 'base')
|
||||
self.assertEqual(new_meeting.attendees, None)
|
||||
|
||||
def test_edit_meeting(self):
|
||||
|
@ -197,8 +200,7 @@ class SecrMeetingTestCase(TestCase):
|
|||
|
||||
# test delete
|
||||
# first unschedule sessions so we can delete
|
||||
SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule).delete()
|
||||
SchedTimeSessAssignment.objects.filter(schedule=meeting.unofficial_schedule).delete()
|
||||
SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base, meeting.unofficial_schedule]).delete()
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
post_dict = {
|
||||
'room-TOTAL_FORMS': q('input[name="room-TOTAL_FORMS"]').val(),
|
||||
|
@ -341,27 +343,29 @@ class SecrMeetingTestCase(TestCase):
|
|||
|
||||
def test_meetings_misc_session_delete(self):
|
||||
meeting = make_meeting_test_data()
|
||||
slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot
|
||||
url = reverse('ietf.secr.meetings.views.misc_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id})
|
||||
target = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name})
|
||||
schedule = meeting.schedule.base
|
||||
slot = schedule.assignments.filter(timeslot__type='reg').first().timeslot
|
||||
url = reverse('ietf.secr.meetings.views.misc_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name,'slot_id':slot.id})
|
||||
target = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name})
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post(url, {'post':'yes'})
|
||||
self.assertRedirects(response, target)
|
||||
self.assertFalse(meeting.schedule.assignments.filter(timeslot=slot))
|
||||
self.assertFalse(schedule.assignments.filter(timeslot=slot))
|
||||
|
||||
def test_meetings_misc_session_cancel(self):
|
||||
meeting = make_meeting_test_data()
|
||||
slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot
|
||||
url = reverse('ietf.secr.meetings.views.misc_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id})
|
||||
redirect_url = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name})
|
||||
schedule = meeting.schedule.base
|
||||
slot = schedule.assignments.filter(timeslot__type='reg').first().timeslot
|
||||
url = reverse('ietf.secr.meetings.views.misc_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name,'slot_id':slot.id})
|
||||
redirect_url = reverse('ietf.secr.meetings.views.misc_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':schedule.name})
|
||||
self.client.login(username="secretary", password="secretary+password")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.post(url, {'post':'yes'})
|
||||
self.assertRedirects(response, redirect_url)
|
||||
session = slot.sessionassignments.filter(schedule=meeting.schedule).first().session
|
||||
session = slot.sessionassignments.filter(schedule=schedule).first().session
|
||||
self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'canceled')
|
||||
|
||||
def test_meetings_regular_session_edit(self):
|
||||
|
|
|
@ -22,7 +22,6 @@ from ietf.meeting.utils import add_event_info_to_session_qs
|
|||
from ietf.meeting.utils import only_sessions_that_can_meet
|
||||
from ietf.name.models import SessionStatusName
|
||||
from ietf.group.models import Group, GroupEvent
|
||||
from ietf.person.models import Person
|
||||
from ietf.secr.meetings.blue_sheets import create_blue_sheets
|
||||
from ietf.secr.meetings.forms import ( BaseMeetingRoomFormSet, MeetingModelForm, MeetingSelectForm,
|
||||
MeetingRoomForm, MiscSessionForm, TimeSlotForm, RegularSessionEditForm,
|
||||
|
@ -85,21 +84,19 @@ def check_misc_sessions(meeting,schedule):
|
|||
Ensure misc session timeslots exist and have appropriate SchedTimeSessAssignment objects
|
||||
for the specified schedule.
|
||||
'''
|
||||
# FIXME: this is a legacy function: delete it once base schedules are rolled out
|
||||
|
||||
if Schedule.objects.filter(meeting=meeting, base__isnull=False).exists():
|
||||
return
|
||||
|
||||
slots = TimeSlot.objects.filter(meeting=meeting,type__in=('break','reg','other','plenary','lead','offagenda'))
|
||||
plenary = slots.filter(type='plenary').first()
|
||||
if plenary:
|
||||
assignments = plenary.sessionassignments.all()
|
||||
if not assignments.filter(schedule=schedule):
|
||||
source = assignments.first().schedule
|
||||
copy_assignments(slots,source,schedule)
|
||||
|
||||
def copy_assignments(slots,source,target):
|
||||
'''
|
||||
Copy SchedTimeSessAssignment objects from source schedule to target schedule. Slots is
|
||||
a queryset of slots
|
||||
'''
|
||||
for ss in SchedTimeSessAssignment.objects.filter(schedule=source,timeslot__in=slots):
|
||||
SchedTimeSessAssignment.objects.create(schedule=target,session=ss.session,timeslot=ss.timeslot)
|
||||
for ss in SchedTimeSessAssignment.objects.filter(schedule=source,timeslot__in=slots):
|
||||
SchedTimeSessAssignment.objects.create(schedule=schedule,session=ss.session,timeslot=ss.timeslot)
|
||||
|
||||
def get_last_meeting(meeting):
|
||||
last_number = int(meeting.number) - 1
|
||||
|
@ -221,13 +218,23 @@ def add(request):
|
|||
if form.is_valid():
|
||||
meeting = form.save()
|
||||
|
||||
base_schedule = Schedule.objects.create(
|
||||
meeting=meeting,
|
||||
name='base',
|
||||
owner=request.user.person,
|
||||
visible=True,
|
||||
public=True
|
||||
)
|
||||
|
||||
schedule = Schedule.objects.create(meeting = meeting,
|
||||
name = 'Empty-Schedule',
|
||||
owner = Person.objects.get(name='(System)'),
|
||||
name = "{}1".format(request.user.username),
|
||||
owner = request.user.person,
|
||||
visible = True,
|
||||
public = True)
|
||||
public = True,
|
||||
base = base_schedule,
|
||||
)
|
||||
meeting.schedule = schedule
|
||||
|
||||
|
||||
# we want to carry session request lock status over from previous meeting
|
||||
previous_meeting = get_meeting( int(meeting.number) - 1 )
|
||||
meeting.session_request_lock_message = previous_meeting.session_request_lock_message
|
||||
|
@ -295,7 +302,7 @@ def blue_sheet_generate(request, meeting_id):
|
|||
if request.method == "POST":
|
||||
groups = Group.objects.filter(
|
||||
type__in=['wg','rg','ag','rag','program'],
|
||||
session__timeslotassignments__schedule=meeting.schedule).order_by('acronym')
|
||||
session__timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]).order_by('acronym')
|
||||
create_blue_sheets(meeting, groups)
|
||||
|
||||
messages.success(request, 'Blue Sheets generated')
|
||||
|
@ -379,7 +386,7 @@ def misc_sessions(request, meeting_id, schedule_name):
|
|||
check_misc_sessions(meeting,schedule)
|
||||
|
||||
misc_session_types = ['break','reg','other','plenary','lead']
|
||||
assignments = schedule.assignments.filter(timeslot__type__in=misc_session_types)
|
||||
assignments = SchedTimeSessAssignment.objects.filter(schedule__in=[schedule, schedule.base], timeslot__type__in=misc_session_types)
|
||||
assignments = assignments.order_by('-timeslot__type__name','timeslot__time')
|
||||
|
||||
if request.method == 'POST':
|
||||
|
@ -574,7 +581,7 @@ def notifications(request, meeting_id):
|
|||
meeting = get_object_or_404(Meeting, number=meeting_id)
|
||||
last_notice = GroupEvent.objects.filter(type='sent_notification').first()
|
||||
groups = set()
|
||||
for ss in meeting.schedule.assignments.filter(timeslot__type='regular'):
|
||||
for ss in SchedTimeSessAssignment.objects.filter(schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None], timeslot__type='regular'):
|
||||
last_notice = ss.session.group.latest_event(type='sent_notification')
|
||||
if last_notice and ss.modified > last_notice.time:
|
||||
groups.add(ss.session.group)
|
||||
|
@ -655,24 +662,28 @@ def regular_sessions(request, meeting_id, schedule_name):
|
|||
schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name)
|
||||
|
||||
sessions = add_event_info_to_session_qs(
|
||||
only_sessions_that_can_meet(schedule.meeting.session_set)
|
||||
only_sessions_that_can_meet(meeting.session_set)
|
||||
).order_by('group__acronym')
|
||||
|
||||
if request.method == 'POST':
|
||||
if 'cancel' in request.POST:
|
||||
pk = request.POST.get('pk')
|
||||
session = get_object_or_404(sessions, pk=pk)
|
||||
SchedulingEvent.objects.create(
|
||||
session=session,
|
||||
status=SessionStatusName.objects.get(slug='canceled'),
|
||||
by=request.user.person,
|
||||
)
|
||||
messages.success(request, 'Session cancelled')
|
||||
if session.current_status not in ['canceled', 'resched']:
|
||||
SchedulingEvent.objects.create(
|
||||
session=session,
|
||||
status=SessionStatusName.objects.get(slug='canceled'),
|
||||
by=request.user.person,
|
||||
)
|
||||
messages.success(request, 'Session cancelled')
|
||||
|
||||
return redirect('ietf.secr.meetings.views.regular_sessions', meeting_id=meeting_id, schedule_name=schedule_name)
|
||||
|
||||
status_names = {n.slug: n.name for n in SessionStatusName.objects.all()}
|
||||
|
||||
for s in sessions:
|
||||
s.current_status_name = status_names.get(s.current_status, s.current_status)
|
||||
s.can_cancel = s.current_status not in ['canceled', 'resched']
|
||||
|
||||
return render(request, 'meetings/sessions.html', {
|
||||
'meeting': meeting,
|
||||
|
|
|
@ -32,11 +32,10 @@ VIDEO_TITLE_RE = re.compile(r'IETF(?P<number>[\d]+)-(?P<name>.*)-(?P<date>\d{8})
|
|||
def _get_session(number,name,date,time):
|
||||
'''Lookup session using data from video title'''
|
||||
meeting = Meeting.objects.get(number=number)
|
||||
schedule = meeting.schedule
|
||||
timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M')
|
||||
try:
|
||||
assignment = SchedTimeSessAssignment.objects.get(
|
||||
schedule = schedule,
|
||||
schedule__in = [meeting.schedule, meeting.schedule.base],
|
||||
session__group__acronym = name.lower(),
|
||||
timeslot__time = timeslot_time,
|
||||
)
|
||||
|
@ -108,7 +107,7 @@ def get_timeslot_for_filename(filename):
|
|||
meeting=meeting,
|
||||
location__name=room_mapping[match.groupdict()['room']],
|
||||
time=time,
|
||||
sessionassignments__schedule=meeting.schedule,
|
||||
sessionassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None],
|
||||
).distinct()
|
||||
uncancelled_slots = [t for t in slots if not add_event_info_to_session_qs(t.sessions.all()).filter(current_status='canceled').exists()]
|
||||
return uncancelled_slots[0]
|
||||
|
|
|
@ -221,9 +221,12 @@ def recording(request, meeting_num):
|
|||
session.
|
||||
'''
|
||||
meeting = get_object_or_404(Meeting, number=meeting_num)
|
||||
assignments = meeting.schedule.assignments.exclude(session__type__in=('reg','break')).order_by('session__group__acronym')
|
||||
sessions = [ x.session for x in assignments ]
|
||||
|
||||
sessions = Session.objects.filter(
|
||||
timeslotassignments__schedule__in=[meeting.schedule, meeting.schedule.base if meeting.schedule else None]
|
||||
).exclude(
|
||||
type__in=['reg','break']
|
||||
).order_by('group__acronym')
|
||||
|
||||
if request.method == 'POST':
|
||||
form = RecordingForm(request.POST,meeting=meeting)
|
||||
if form.is_valid():
|
||||
|
|
|
@ -461,6 +461,11 @@ input.draft-file-input {
|
|||
Meeting Tool
|
||||
========================================================================== */
|
||||
|
||||
#misc-sessions .from-base-schedule {
|
||||
text-align: centeR;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#misc-session-edit-form input[type="text"] {
|
||||
width: 30em;
|
||||
}
|
||||
|
|
|
@ -33,13 +33,17 @@
|
|||
<td>{{ assignment.timeslot.location }}</td>
|
||||
<td>{{ assignment.timeslot.show_location }}</td>
|
||||
<td>{{ assignment.timeslot.type }}</td>
|
||||
<td><a href="{% url "ietf.secr.meetings.views.misc_session_edit" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Edit</a></td>
|
||||
<td>
|
||||
{% if not assignment.session.type.slug == "break" %}
|
||||
<a href="{% url "ietf.secr.meetings.views.misc_session_cancel" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Cancel</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a href="{% url "ietf.secr.meetings.views.misc_session_delete" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Delete</a></td>
|
||||
{% if assignment.schedule_id == schedule.pk %}
|
||||
<td><a href="{% url "ietf.secr.meetings.views.misc_session_edit" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Edit</a></td>
|
||||
<td>
|
||||
{% if assignment.session.type.slug != "break" %}
|
||||
<a href="{% url "ietf.secr.meetings.views.misc_session_cancel" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Cancel</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td><a href="{% url "ietf.secr.meetings.views.misc_session_delete" meeting_id=meeting.number schedule_name=schedule.name slot_id=assignment.timeslot.id %}">Delete</a></td>
|
||||
{% else %}
|
||||
<td colspan="3" class="from-base-schedule">(from base schedule)</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
|
@ -36,11 +36,13 @@
|
|||
<td>{{ session.current_status_name }}</td>
|
||||
<td><a href="{% url 'ietf.secr.meetings.views.regular_session_edit' meeting_id=meeting.number schedule_name=schedule.name session_id=session.id %}">Edit</a></td>
|
||||
<td>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="pk" value="{{ session.pk }}">
|
||||
<input type="submit" name="cancel" value="Cancel">
|
||||
</form>
|
||||
{% if session.can_cancel %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="pk" value="{{ session.pk }}">
|
||||
<input type="submit" name="cancel" value="Cancel">
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -52,7 +52,7 @@ def get_session(timeslot, schedule=None):
|
|||
# todo, doesn't account for shared timeslot
|
||||
if not schedule:
|
||||
schedule = timeslot.meeting.schedule
|
||||
qs = timeslot.sessions.filter(timeslotassignments__schedule=schedule) #.exclude(states__slug='deleted')
|
||||
qs = timeslot.sessions.filter(timeslotassignments__schedule__in=[schedule, schedule.base if schedule else None]) #.exclude(states__slug='deleted')
|
||||
if qs:
|
||||
return qs[0]
|
||||
else:
|
||||
|
@ -66,7 +66,7 @@ def get_timeslot(session, schedule=None):
|
|||
'''
|
||||
if not schedule:
|
||||
schedule = session.meeting.schedule
|
||||
ss = session.timeslotassignments.filter(schedule=schedule)
|
||||
ss = session.timeslotassignments.filter(schedule__in=[schedule, schedule.base if schedule else None])
|
||||
if ss:
|
||||
return ss[0].timeslot
|
||||
else:
|
||||
|
|
|
@ -1003,6 +1003,12 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
/* === List Meeting Schedules ====================================== */
|
||||
|
||||
.from-base-schedule {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* === Edit Meeting Schedule ====================================== */
|
||||
|
||||
.edit-meeting-schedule .edit-grid {
|
||||
|
@ -1034,6 +1040,18 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
height: 3em;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .day-label .swap-days {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .day-label .swap-days:hover {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule #swap-days-modal .modal-body label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .edit-grid .day-flow {
|
||||
margin-left: 8em;
|
||||
display: flex;
|
||||
|
@ -1118,6 +1136,11 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session.readonly {
|
||||
cursor: default;
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.edit-meeting-schedule .session.selected .session-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -1289,3 +1312,130 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
|
|||
cursor: default;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
/* === Edit Meeting Timeslots and Misc Sessions =================== */
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .day {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .day-label {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .room-row {
|
||||
border-bottom: 1px solid #ccc;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .room-label {
|
||||
width: 12em;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .timeline {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .timeline.hover {
|
||||
background: radial-gradient(#999 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .timeline.selected.hover,
|
||||
.edit-meeting-timeslots-and-misc-sessions .timeline.selected {
|
||||
background: radial-gradient(#999 2px, transparent 2px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .timeslot {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
background-color: #f0f0f0;
|
||||
opacity: 0.8;
|
||||
height: 19px;
|
||||
top: 0px;
|
||||
font-size: 13px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
padding-left: 0.2em;
|
||||
border-left: 1px solid #999;
|
||||
border-right: 1px solid #999;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .timeslot:hover {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .timeslot.selected {
|
||||
background-color: #bbb;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .timeslot .session.cancelled {
|
||||
color: #a00;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-top: 0.2em solid #ccc;
|
||||
padding-top: 0.2em;
|
||||
margin-bottom: 2em;
|
||||
background-color: #fff;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel form {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel form button {
|
||||
margin: 0 0.5em;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form .form-group {
|
||||
margin-right: 1em;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form label {
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form .form-control {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form [name=time],
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form [name=duration] {
|
||||
width: 6em;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form [name=name] {
|
||||
width: 25em;
|
||||
}
|
||||
|
||||
.edit-meeting-timeslots-and-misc-sessions .scheduling-panel .flowing-form [name=short] {
|
||||
width: 10em;
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
jQuery(document).ready(function () {
|
||||
let content = jQuery(".edit-meeting-schedule");
|
||||
|
||||
function failHandler(xhr, textStatus, error) {
|
||||
let errorText = error;
|
||||
function reportServerError(xhr, textStatus, error) {
|
||||
let errorText = error || textStatus;
|
||||
if (xhr && xhr.responseText)
|
||||
errorText += "\n\n" + xhr.responseText;
|
||||
alert("Error: " + errorText);
|
||||
}
|
||||
|
||||
let sessions = content.find(".session");
|
||||
let sessions = content.find(".session").not(".readonly");
|
||||
let timeslots = content.find(".timeslot");
|
||||
let days = content.find(".day-flow .day");
|
||||
|
||||
|
@ -120,7 +120,7 @@ jQuery(document).ready(function () {
|
|||
});
|
||||
|
||||
|
||||
if (ietfData.can_edit) {
|
||||
if (!content.find(".edit-grid").hasClass("read-only")) {
|
||||
// dragging
|
||||
sessions.on("dragstart", function (event) {
|
||||
event.originalEvent.dataTransfer.setData("text/plain", this.id);
|
||||
|
@ -130,7 +130,6 @@ jQuery(document).ready(function () {
|
|||
});
|
||||
sessions.on("dragend", function () {
|
||||
jQuery(this).removeClass("dragging");
|
||||
|
||||
});
|
||||
|
||||
sessions.prop('draggable', true);
|
||||
|
@ -161,31 +160,47 @@ jQuery(document).ready(function () {
|
|||
});
|
||||
|
||||
dropElements.on('drop', function (event) {
|
||||
jQuery(this).parent().removeClass("dropping");
|
||||
let dropElement = jQuery(this);
|
||||
|
||||
let sessionId = event.originalEvent.dataTransfer.getData("text/plain");
|
||||
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session")
|
||||
if ((event.originalEvent.dataTransfer.getData("text/plain") || "").slice(0, "session".length) != "session") {
|
||||
dropElement.parent().removeClass("dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionElement = sessions.filter("#" + sessionId);
|
||||
if (sessionElement.length == 0)
|
||||
if (sessionElement.length == 0) {
|
||||
dropElement.parent().removeClass("dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault(); // prevent opening as link
|
||||
|
||||
if (sessionElement.parent().is(this))
|
||||
let dragParent = sessionElement.parent();
|
||||
if (dragParent.is(this)) {
|
||||
dropElement.parent().removeClass("dropping");
|
||||
return;
|
||||
}
|
||||
|
||||
let dropElement = jQuery(this);
|
||||
let dropParent = dropElement.parent();
|
||||
|
||||
function failHandler(xhr, textStatus, error) {
|
||||
dropElement.parent().removeClass("dropping");
|
||||
reportServerError(xhr, textStatus, error);
|
||||
}
|
||||
|
||||
function done(response) {
|
||||
if (response != "OK") {
|
||||
failHandler(null, null, response);
|
||||
dropElement.parent().removeClass("dropping");
|
||||
|
||||
if (!response.success) {
|
||||
reportServerError(null, null, response);
|
||||
return;
|
||||
}
|
||||
|
||||
dropElement.append(sessionElement); // move element
|
||||
if (response.tombstone)
|
||||
dragParent.append(response.tombstone);
|
||||
|
||||
updateCurrentSchedulingHints();
|
||||
if (dropParent.hasClass("unassigned-sessions"))
|
||||
sortUnassigned();
|
||||
|
@ -193,7 +208,7 @@ jQuery(document).ready(function () {
|
|||
|
||||
if (dropParent.hasClass("unassigned-sessions")) {
|
||||
jQuery.ajax({
|
||||
url: ietfData.urls.assign,
|
||||
url: window.location.href,
|
||||
method: "post",
|
||||
timeout: 5 * 1000,
|
||||
data: {
|
||||
|
@ -204,7 +219,7 @@ jQuery(document).ready(function () {
|
|||
}
|
||||
else {
|
||||
jQuery.ajax({
|
||||
url: ietfData.urls.assign,
|
||||
url: window.location.href,
|
||||
method: "post",
|
||||
data: {
|
||||
action: "assign",
|
||||
|
@ -215,6 +230,32 @@ jQuery(document).ready(function () {
|
|||
}).fail(failHandler).done(done);
|
||||
}
|
||||
});
|
||||
|
||||
// swap days
|
||||
content.find(".swap-days").on("click", function () {
|
||||
let originDay = this.dataset.dayid;
|
||||
let modal = content.find("#swap-days-modal");
|
||||
let radios = modal.find(".modal-body label");
|
||||
radios.removeClass("text-muted");
|
||||
radios.find("input[name=target_day]").prop("disabled", false).prop("checked", false);
|
||||
|
||||
let originRadio = radios.find("input[name=target_day][value=" + originDay + "]");
|
||||
originRadio.parent().addClass("text-muted");
|
||||
originRadio.prop("disabled", true);
|
||||
|
||||
modal.find(".modal-title .day").text(jQuery.trim(originRadio.parent().text()));
|
||||
modal.find("input[name=source_day]").val(originDay);
|
||||
|
||||
updateSwapDaysSubmitButton();
|
||||
});
|
||||
|
||||
function updateSwapDaysSubmitButton() {
|
||||
content.find("#swap-days-modal button[type=submit]").prop("disabled", content.find("#swap-days-modal input[name=target_day]:checked").length == 0);
|
||||
}
|
||||
|
||||
content.find("#swap-days-modal input[name=target_day]").on("change", function () {
|
||||
updateSwapDaysSubmitButton();
|
||||
});
|
||||
}
|
||||
|
||||
// hints for the current schedule
|
||||
|
|
156
ietf/static/ietf/js/edit-meeting-timeslots-and-misc-sessions.js
Normal file
156
ietf/static/ietf/js/edit-meeting-timeslots-and-misc-sessions.js
Normal file
|
@ -0,0 +1,156 @@
|
|||
jQuery(document).ready(function () {
|
||||
function reportServerError(xhr, textStatus, error) {
|
||||
let errorText = error || textStatus;
|
||||
if (xhr && xhr.responseText)
|
||||
errorText += "\n\n" + xhr.responseText;
|
||||
alert("Error: " + errorText);
|
||||
}
|
||||
|
||||
let content = jQuery(".edit-meeting-timeslots-and-misc-sessions");
|
||||
|
||||
if (content.data('scroll'))
|
||||
jQuery(document).scrollTop(+content.data('scroll'));
|
||||
else {
|
||||
let scrollFragment = "#scroll=";
|
||||
if (window.location.hash.slice(0, scrollFragment.length) == scrollFragment && !isNaN(+window.location.hash.slice(scrollFragment.length))) {
|
||||
jQuery(document).scrollTop(+window.location.hash.slice(scrollFragment.length));
|
||||
history.replaceState(null, document.title, window.location.pathname + window.location.search);
|
||||
}
|
||||
}
|
||||
|
||||
function reportServerError(xhr, textStatus, error) {
|
||||
let errorText = error || textStatus;
|
||||
if (xhr && xhr.responseText)
|
||||
errorText += "\n\n" + xhr.responseText;
|
||||
alert("Error: " + errorText);
|
||||
}
|
||||
|
||||
let timeslots = content.find(".timeslot");
|
||||
|
||||
timeslots.each(function () {
|
||||
jQuery(this).tooltip({title: jQuery(this).text()});
|
||||
});
|
||||
|
||||
content.find(".day-grid").on("click", cancelCurrentActivity);
|
||||
|
||||
let schedulingPanel = content.find(".scheduling-panel");
|
||||
|
||||
function cancelCurrentActivity() {
|
||||
content.find(".selected").removeClass("selected");
|
||||
|
||||
schedulingPanel.hide();
|
||||
schedulingPanel.find(".panel-content").children().remove();
|
||||
// if we came from a failed POST, that's no longer relevant so overwrite history
|
||||
history.replaceState(null, document.title, window.location.pathname + window.location.search);
|
||||
}
|
||||
|
||||
if (!content.hasClass("read-only")) {
|
||||
// we handle the hover effect in Javascript because we don't want
|
||||
// it to show in case the timeslot itself is hovered
|
||||
content.find(".room-label,.timeline").on("mouseover", function () {
|
||||
jQuery(this).closest(".day").find(".timeline.hover").removeClass("hover");
|
||||
jQuery(this).closest(".room-row").find(".timeline").addClass("hover");
|
||||
}).on("mouseleave", function (){
|
||||
jQuery(this).closest(".day").find(".timeline.hover").removeClass("hover");
|
||||
});
|
||||
|
||||
content.find(".timeline .timeslot").on("mouseover", function (e) {
|
||||
e.stopPropagation();
|
||||
jQuery(this).closest(".day").find(".timeline.hover").removeClass("hover");
|
||||
}).on("mouseleave", function (e) {
|
||||
jQuery(this).closest(".day").find(".timeline.hover").removeClass("hover");
|
||||
});
|
||||
|
||||
content.find(".room-row").on("click", function (e) {
|
||||
e.stopPropagation();
|
||||
cancelCurrentActivity();
|
||||
|
||||
jQuery(this).find(".timeline").addClass("selected");
|
||||
|
||||
schedulingPanel.find(".panel-content").append(content.find(".add-timeslot-template").html());
|
||||
schedulingPanel.find("[name=day]").val(this.dataset.day);
|
||||
schedulingPanel.find("[name=location]").val(this.dataset.room);
|
||||
schedulingPanel.find("[name=type]").trigger("change");
|
||||
schedulingPanel.show();
|
||||
schedulingPanel.find("[name=time]").focus();
|
||||
});
|
||||
}
|
||||
|
||||
content.find(".timeline .timeslot").on("click", function (e) {
|
||||
e.stopPropagation();
|
||||
|
||||
let element = jQuery(this);
|
||||
|
||||
element.addClass("selected");
|
||||
|
||||
jQuery.ajax({
|
||||
url: window.location.href,
|
||||
method: "get",
|
||||
timeout: 5 * 1000,
|
||||
data: {
|
||||
action: "edit-timeslot",
|
||||
timeslot: this.id.slice("timeslot".length)
|
||||
}
|
||||
}).fail(reportServerError).done(function (response) {
|
||||
if (!response.form) {
|
||||
reportServerError(null, null, response);
|
||||
return;
|
||||
}
|
||||
|
||||
cancelCurrentActivity();
|
||||
element.addClass("selected");
|
||||
|
||||
schedulingPanel.find(".panel-content").append(response.form);
|
||||
schedulingPanel.find(".timeslot-form [name=type]").trigger("change");
|
||||
schedulingPanel.find(".timeslot-form").show();
|
||||
schedulingPanel.show();
|
||||
});
|
||||
});
|
||||
|
||||
content.on("change click", ".timeslot-form [name=type]", function () {
|
||||
let form = jQuery(this).closest("form");
|
||||
|
||||
let hide = {};
|
||||
|
||||
form.find("[name=group],[name=short],[name=\"agenda_note\"]").prop('disabled', false).closest(".form-group").show();
|
||||
|
||||
if (this.value == "break") {
|
||||
form.find("[name=short]").closest(".form-group").hide();
|
||||
}
|
||||
else if (this.value == "plenary") {
|
||||
let group = form.find("[name=group]");
|
||||
group.val(group.data('ietf'));
|
||||
}
|
||||
else if (this.value == "regular") {
|
||||
form.find("[name=short]").closest(".form-group").hide();
|
||||
}
|
||||
|
||||
if (this.value != "regular")
|
||||
form.find("[name=\"agenda_note\"]").closest(".form-group").hide();
|
||||
|
||||
if (['break', 'reg', 'reserved', 'unavail', 'regular'].indexOf(this.value) != -1) {
|
||||
let group = form.find("[name=group]");
|
||||
group.prop('disabled', true);
|
||||
group.closest(".form-group").hide();
|
||||
}
|
||||
});
|
||||
|
||||
content.on("submit", ".timeslot-form", function () {
|
||||
let form = jQuery(this).closest("form");
|
||||
form.find("[name=scroll]").remove();
|
||||
form.append("<input type=hidden name=scroll value=" + jQuery(document).scrollTop() + ">");
|
||||
});
|
||||
|
||||
content.on("click", "button[type=submit][name=action][value=\"delete-timeslot\"],button[type=submit][name=action][value=\"cancel-timeslot\"]", function (e) {
|
||||
let msg = this.value == "delete-timeslot" ? "Delete this time slot?" : "Cancel the session in this time slot?";
|
||||
if (!confirm(msg)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
schedulingPanel.find(".close").on("click", function () {
|
||||
cancelCurrentActivity();
|
||||
});
|
||||
|
||||
schedulingPanel.find('.timeslot-form [name=type]').trigger("change");
|
||||
});
|
|
@ -235,7 +235,7 @@
|
|||
<span class="hidden-xs">
|
||||
{% if item.timeslot.type.slug == 'other' %}
|
||||
{% if item.session.agenda or item.session.remote_instructions or item.session.agenda_note %}
|
||||
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
|
||||
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
|
||||
{% else %}
|
||||
{% for slide in item.session.slides %}
|
||||
<a href="{{slide.get_href}}">{{ slide.title|clean_whitespace }}</a>
|
||||
|
@ -324,6 +324,20 @@
|
|||
<span class="label label-danger pull-right">CANCELLED</span>
|
||||
{% endif %}
|
||||
|
||||
{% if item.session.current_status == 'resched' %}
|
||||
<span class="label label-danger pull-right">
|
||||
RESCHEDULED
|
||||
{% if item.session.rescheduled_to %}
|
||||
TO
|
||||
{% if "-utc" in request.path %}
|
||||
{{ item.session.rescheduled_to.utc_start_time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.utc_end_time|date:"G:i" }}
|
||||
{% else %}
|
||||
{{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if item.session.agenda_note|first_url|conference_url %}
|
||||
<br><a href={{item.session.agenda_note|first_url}}>{{item.session.agenda_note|slice:":23"}}</a>
|
||||
{% elif item.session.agenda_note %}
|
||||
|
@ -333,7 +347,7 @@
|
|||
</td>
|
||||
<td class="text-nowrap text-right">
|
||||
<span class="hidden-xs">
|
||||
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
|
||||
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
{% endif %}{% if item.timeslot.type_id == 'regular' %}{% if item.session.historic_group %}{% ifchanged %}
|
||||
|
||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}
|
||||
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% endif %}
|
||||
{% endifchanged %}{{ item.timeslot.location.name|ljust:14 }} {{ item.session.historic_group.historic_parent.acronym|upper|ljust:4 }} {{ item.session.historic_group.acronym|ljust:10 }} {{ item.session.historic_group.name }} {% if item.session.historic_group.state_id == "bof" %}BOF{% elif item.session.historic_group.type_id == "wg" %}WG{% endif %}{% if item.session.agenda_note %} - {{ item.session.agenda_note }}{% endif %}{% if item.session.current_status == 'canceled' %} *** CANCELLED ***{% elif item.session.current_status == 'resched' %} *** RESCHEDULED{% if item.session.rescheduled_to %} TO {{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}{% endif %} ***{% endif %}
|
||||
{% endif %}{% endif %}{% if item.timeslot.type.slug == "break" %}
|
||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }}{% if schedule.meeting.break_area and item.timeslot.show_location %} - {{ schedule.meeting.break_area }}{% endif %}{% endif %}{% if item.timeslot.type.slug == "other" %}
|
||||
{{ item.timeslot.time_desc }} {{ item.timeslot.name }} - {{ item.timeslot.location.name }}{% endif %}{% endfor %}
|
||||
|
|
|
@ -29,7 +29,7 @@ ul.sessionlist { list-style:none; padding-left:2em; margin-bottom:10px;}
|
|||
<li class="roomlistentry"><h3>{{room.grouper|default:"Location Unavailable"}}</h3>
|
||||
<ul class="sessionlist">
|
||||
{% for ss in room.list %}
|
||||
<li class="sessionlistentry type-{{ss.timeslot.type.slug}}">{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}</li>
|
||||
<li class="sessionlistentry type-{{ss.timeslot.type_id}} {% if ss.schedule_id != meeting.schedule_id %}from-base-schedule{% endif %}">{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}} {{ss.session.short_name}}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
|
|
|
@ -29,7 +29,7 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
|
|||
{% block content %}
|
||||
{% include "meeting/meeting_heading.html" with updated=meeting.updated selected="by-type" title_extra="by Session Type" %}
|
||||
|
||||
{% regroup assignments by session.type.slug as type_list %}
|
||||
{% regroup assignments by session.type_id as type_list %}
|
||||
<ul class="typelist">
|
||||
{% for type in type_list %}
|
||||
<li class="typelistentry {% cycle 'even' 'odd' %}">
|
||||
|
@ -41,11 +41,11 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
|
|||
<h3>{{ day.grouper }}</h3>
|
||||
<table class="sessiontable">
|
||||
{% for ss in day.list %}
|
||||
<tr>
|
||||
<tr {% if ss.schedule_id != meeting.schedule_id %}class="from-base-schedule"{% endif %}>
|
||||
<td>{{ss.timeslot.time|date:"H:i"}}-{{ss.timeslot.end_time|date:"H:i"}}</td>
|
||||
<td>{{ss.timeslot.get_hidden_location}}</td>
|
||||
<td class="type-{{ss.session.type.slug}}">{{ss.session.short_name}} </td>
|
||||
<td>{% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %} <a href="{% url 'ietf.meeting.views.session_details' num=ss.session.meeting.number acronym=ss.session.group.acronym %}">Materials</a>{% else %} {% endif %}</td>
|
||||
<td class="type-{{ss.session.type_id}}">{{ss.session.short_name}} </td>
|
||||
<td>{% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %} <a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=ss.session.group.acronym %}">Materials</a>{% else %} {% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
|
|
40
ietf/templates/meeting/diff_schedules.html
Normal file
40
ietf/templates/meeting/diff_schedules.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2020, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load ietf_filters %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>{% block title %}Differences between Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
|
||||
|
||||
<form method="get">
|
||||
{% bootstrap_form form %}
|
||||
<button type="submit">Show differences</button>
|
||||
</form>
|
||||
|
||||
{% if diffs != None %}
|
||||
<h2>Differences from {{ from_schedule.name }} ({{ from_schedule.owner }}) to {{ to_schedule.name }} ({{ to_schedule.owner }}) </h2>
|
||||
|
||||
{% if diffs %}
|
||||
<table class="table table-condensed schedule-diffs">
|
||||
{% for d in diffs %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if d.change == 'schedule' %}
|
||||
Scheduled <b>{{ d.session.session_label }}</b> to <b>{{ d.to.time|date:"l G:i" }} at {{ d.to.location.name }}</b>
|
||||
{% elif d.change == 'move' %}
|
||||
Moved <b>{{ d.session.session_label }}</b> from {{ d.from.time|date:"l G:i" }} at {{ d.from.location.name }} to <b>{{ d.to.time|date:"l G:i" }} at {{ d.to.location.name }}</b>
|
||||
{% elif d.change == 'unschedule' %}
|
||||
Unscheduled <b>{{ d.session.session_label }}</b> from {{ d.from.time|date:"l G:i" }} at {{ d.from.location.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
No differences in scheduled sessions found!
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock content %}
|
|
@ -12,12 +12,9 @@
|
|||
{% endfor %}
|
||||
{% endblock morecss %}
|
||||
|
||||
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting schedule{% endblock %}
|
||||
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type='text/javascript'>
|
||||
var ietfData = {{ js_data|safe }};
|
||||
</script>
|
||||
<script type="text/javascript" src="{% static 'ietf/js/edit-meeting-schedule.js' %}"></script>
|
||||
{% endblock js %}
|
||||
|
||||
|
@ -27,14 +24,20 @@
|
|||
<div class="edit-meeting-schedule">
|
||||
|
||||
<p class="pull-right">
|
||||
<a href="{% url "ietf.meeting.views.copy_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">Copy schedule</a>
|
||||
{% if can_edit_properties %}
|
||||
<a href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">Edit properties</a>
|
||||
|
||||
·
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">New agenda</a>
|
||||
·
|
||||
|
||||
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">All schedules for meeting</a>
|
||||
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Schedule name: {{ schedule.name }}
|
||||
Agenda name: {{ schedule.name }}
|
||||
|
||||
·
|
||||
|
||||
|
@ -43,7 +46,7 @@
|
|||
{% if not can_edit %}
|
||||
·
|
||||
|
||||
<em>You can't edit this schedule. Take a copy first.</em>
|
||||
<strong><em>You can't edit this schedule. Make a <a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">new agenda from this</a>.</em></strong>
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
|
@ -74,7 +77,7 @@
|
|||
{% for day in days %}
|
||||
<div class="day">
|
||||
<div class="day-label">
|
||||
<strong>{{ day.day|date:"l" }}</strong><br>
|
||||
<strong>{{ day.day|date:"l" }}</strong> <i class="fa fa-exchange swap-days" data-dayid="{{ day.day.isoformat }}" data-toggle="modal" data-target="#swap-days-modal"></i><br>
|
||||
{{ day.day|date:"N j, Y" }}
|
||||
</div>
|
||||
|
||||
|
@ -88,9 +91,9 @@
|
|||
</div>
|
||||
|
||||
<div class="drop-target">
|
||||
{% for assignment, session in t.session_assignments %}
|
||||
{% include "meeting/edit_meeting_schedule_session.html" %}
|
||||
{% endfor %}
|
||||
{% for assignment, session in t.session_assignments %}
|
||||
{% include "meeting/edit_meeting_schedule_session.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
@ -167,5 +170,34 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="swap-days-modal" class="modal" role="dialog" aria-labelledby="swap-days-modal-title">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<form class="modal-content" method="post">{% csrf_token %}
|
||||
<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="swap-days-modal-title">Swap <span class="day"></span> with</h4>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="source_day" value="">
|
||||
|
||||
<div class="modal-body">
|
||||
{% for day in days %}
|
||||
<label>
|
||||
<input type="radio" name="target_day" value="{{ day.day.isoformat }}"> {{ day.day|date:"l, N j, Y" }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="submit" name="action" value="swapdays" class="btn btn-primary">Swap days</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<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 id="session{{ session.pk }}" class="session {% if not session.group.parent.scheduling_color %}untoggleable{% endif %} {% if session.parent_acronym %}parent-{{ session.parent_acronym }}{% endif %} {% if session.readonly %}readonly{% 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 {% if session.group and session.group.is_bof %}bof-session{% endif %}">
|
||||
{{ session.scheduling_label }}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load staticfiles %}
|
||||
{% load ietf_filters %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block title %}{{ schedule.name }}: IETF {{ meeting.number }} meeting agenda{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script type="text/javascript" src="{% static 'ietf/js/edit-meeting-timeslots-and-misc-sessions.js' %}"></script>
|
||||
{% endblock js %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<div class="edit-meeting-timeslots-and-misc-sessions {% if not can_edit %}read-only{% endif %}" {% if scroll %}data-scroll="{{ scroll }}"{% endif %}>
|
||||
|
||||
<p class="pull-right">
|
||||
<a href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">Other Agendas</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Meeting time slots and misc. sessions for agenda: {{ schedule.name }} {% if not can_edit %}<em>(you do not have permission to edit time slots)</em>{% endif %}
|
||||
</p>
|
||||
|
||||
<div class="day-grid">
|
||||
{% for day in day_grid %}
|
||||
<div class="day">
|
||||
<div class="day-label">
|
||||
<strong>{{ day.day|date:"l" }}</strong>
|
||||
{{ day.day|date:"N j, Y" }}
|
||||
</div>
|
||||
|
||||
{% for room, timeslots in day.room_timeslots %}
|
||||
<div class="room-row" data-room="{{ room.pk }}" data-day="{{ day.day.isoformat }}">
|
||||
<div class="room-label" title="{{ room.name }}">
|
||||
<strong>{{ room.name }}</strong>
|
||||
{% if room.capacity %}{{ room.capacity }}{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="timeline">
|
||||
{% for t in timeslots %}
|
||||
<div id="timeslot{{ t.pk }}" class="timeslot" style="left: {{ t.left_offset|floatformat }}%; width: {{ t.layout_width|floatformat }}%;">
|
||||
{% for s in t.assigned_sessions %}
|
||||
<span class="session {% if s.current_status == 'canceled' or s.current_status == 'resched' %}cancelled{% endif %}">
|
||||
{% if s.name %}
|
||||
{{ s.name }}
|
||||
{% if s.group %}
|
||||
({{ s.group.acronym }})
|
||||
{% endif %}
|
||||
{% elif s.group %}
|
||||
{{ s.group.acronym }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% empty %}
|
||||
{% if t.type_id == 'regular' %}
|
||||
(session)
|
||||
{% elif t.name %}
|
||||
{{ t.name }}
|
||||
{% else %}
|
||||
{{ t.type.name }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="time-label">{{ t.time|date:"G:i" }}-{{ t.end_time|date:"G:i" }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="add-timeslot-template" style="display:none">
|
||||
{% include "meeting/edit_timeslot_form.html" with timeslot_form_action='add' timeslot_form=empty_timeslot_form %}
|
||||
</div>
|
||||
|
||||
<div class="scheduling-panel" style="{% if not edit_timeslot_form and not add_timeslot_form %}display:none{% endif %}">
|
||||
<i class="close fa fa-times pull-right"></i>
|
||||
|
||||
<div class="panel-content">
|
||||
{% if edit_timeslot_form %}
|
||||
{% include "meeting/edit_timeslot_form.html" with timeslot_form_action='edit' timeslot_form=edit_timeslot_form %}
|
||||
{% elif add_timeslot_form %}
|
||||
{% include "meeting/edit_timeslot_form.html" with timeslot_form_action='add' timeslot_form=add_timeslot_form %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
41
ietf/templates/meeting/edit_timeslot_form.html
Normal file
41
ietf/templates/meeting/edit_timeslot_form.html
Normal file
|
@ -0,0 +1,41 @@
|
|||
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load bootstrap3 %}
|
||||
{% if not timeslot_form.active_assignment or timeslot_form.active_assignment.schedule_id == schedule.pk %}
|
||||
<form class="timeslot-form" method="post">{% csrf_token %}
|
||||
<div class="flowing-form">
|
||||
{% bootstrap_field timeslot_form.day %}
|
||||
{% bootstrap_field timeslot_form.time %}
|
||||
{% bootstrap_field timeslot_form.duration %}
|
||||
|
||||
{% bootstrap_field timeslot_form.location %}
|
||||
{% bootstrap_field timeslot_form.show_location %}
|
||||
|
||||
{% bootstrap_field timeslot_form.type %}
|
||||
{% bootstrap_field timeslot_form.group %}
|
||||
{% bootstrap_field timeslot_form.name %}
|
||||
{% bootstrap_field timeslot_form.short %}
|
||||
{% if 'agenda_note' in timeslot_form.fields %}
|
||||
{% bootstrap_field timeslot_form.agenda_note %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if can_edit %}
|
||||
<button type="submit" class="btn btn-primary" name="action" value="{{ timeslot_form_action }}-timeslot">
|
||||
{% if timeslot_form_action == 'add' %}Add time slot{% else %}Save{% endif %} slot
|
||||
</button>
|
||||
|
||||
{% if timeslot %}
|
||||
<input type="hidden" name="timeslot" value="{{ timeslot.pk }}">
|
||||
|
||||
{% if timeslot.type_id != 'break' and timeslot.can_cancel %}
|
||||
<button type="submit" class="btn btn-danger" name="action" value="cancel-timeslot" title="Cancel session">Cancel session</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" class="btn btn-danger" name="action" value="delete-timeslot" title="Delete time slot">Delete</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% elif schedule.base %}
|
||||
<p class="text-center">You cannot edit this session here - it is set up in the <a href="{% url 'ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions' meeting.number schedule.base.owner_email schedule.base.name %}">base schedule</a></p>
|
||||
{% endif %}
|
|
@ -1,10 +1,10 @@
|
|||
{% load ietf_filters %}{% if is_change %}MEETING DETAILS HAVE CHANGED. SEE LATEST DETAILS BELOW.
|
||||
|
||||
{% endif %}The {{ group.name }} ({{ group.acronym }}) {% if group.type.slug == 'wg' and group.state.slug == 'bof' %}BOF{% else %}{{group.type.name}}{% endif %} will hold
|
||||
{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ meeting.schedule.assignments.first.timeslot.time | date:"H:i" }} to {{ meeting.schedule.assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ meeting.schedule.assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ meeting.schedule.assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}.
|
||||
{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ assignments.first.timeslot.time | date:"H:i" }} to {{ assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone}}{% if meeting.time_zone != 'UTC' %} ({{ assignments.first.timeslot.utc_start_time | date:"H:i" }} to {{ assignments.first.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}.
|
||||
{% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting.
|
||||
|
||||
{% for assignment in meeting.schedule.assignments.all %}Session {{ forloop.counter }}:
|
||||
{% for assignment in assignments %}Session {{ forloop.counter }}:
|
||||
{{ assignment.timeslot.time | date:"Y-m-d" }} {{ assignment.timeslot.time | date:"H:i" }} to {{ assignment.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}{% if meeting.time_zone != 'UTC' %}({{ assignment.timeslot.utc_start_time | date:"H:i" }} to {{ assignment.timeslot.utc_end_time | date:"H:i" }} UTC){% endif %}
|
||||
{% endfor %}{% endif %}
|
||||
{% if meeting.city %}Meeting Location:
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<dd>{{ meeting.country }}</dd>
|
||||
<dt>Timezone</dt>
|
||||
<dd>{{ meeting.time_zone }}</dd>
|
||||
{% for assignment in meeting.schedule.assignments.all %}
|
||||
{% for assignment in assignments %}
|
||||
<br>
|
||||
<dt>Date</dt>
|
||||
<dd>{{ assignment.timeslot.time|date:"Y-m-d" }}
|
||||
|
|
|
@ -367,6 +367,9 @@ promiselist.push(ss_promise);
|
|||
<div class="agenda_save_box">
|
||||
|
||||
<div id="agenda_title"><b>Agenda name: </b><span>{{schedule.name}}</span></div>
|
||||
{% if can_edit_properties %}
|
||||
<div><b>Properties</b> <a href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">Edit</a></div>
|
||||
{% endif %}
|
||||
<div id="agenda_saveas">
|
||||
<form action="{{saveasurl}}" method="post">{% csrf_token %}
|
||||
{{ saveas.as_p }}
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load static %}
|
||||
{% load staticfiles %}
|
||||
{% load ietf_filters %}
|
||||
{% load bootstrap3 %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>{% block title %}Copy schedule {{ schedule.name }}{% endblock %}</h1>
|
||||
<h1>{% block title %}{% if schedule %}Copy agenda {{ schedule.name }} to new agenda{% else %}New agenda{% endif %}{% endblock %}</h1>
|
||||
|
||||
<form class="form-horizontal" method="post">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-default">Copy schedule</button>
|
||||
<button type="submit" class="btn btn-primary">Create agenda</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,21 +1,15 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
|
||||
{% block title %}IETF {{ meeting.number }} Meeting Agenda: {{ schedule.owner }} / {{ schedule.name }}{% endblock %}
|
||||
|
||||
{% block morecss %}
|
||||
{% for area in area_list %}
|
||||
.{{ area.upcase_acronym}}-scheme, .meeting_event th.{{ area.upcase_acronym}}-scheme, #{{ area.upcase_acronym }}-groups, #selector-{{ area.upcase_acronym }} { color:{{ area.fg_color }}; background-color: {{ area.bg_color }} }
|
||||
{% endfor %}
|
||||
{% endblock morecss %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<div id="read_only">
|
||||
<p>You do not have access this agenda. It belongs to {{ schedule.owner }}.</p>
|
||||
|
||||
<p><a href="{% url "ietf.meeting.views.list_schedules" meeting.number %}">List your meetings</a>.</p>
|
||||
<p><a href="{% url "ietf.meeting.views.list_schedules" meeting.number %}">Other agendas for this meeting</a>.</p>
|
||||
|
||||
<div class="wrapper custom_text_stuff"></div>
|
||||
</div>
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
<script src="{% static 'ietf/bootstrap/js/bootstrap.min.js' %}"></script>
|
||||
<script type="text/javascript">
|
||||
|
||||
{% autoescape off %}
|
||||
var room_names = [{% for room in rooms %}"{{room.name}}"{% if not forloop.last %},{% endif %}{% endfor %}];
|
||||
var room_functional_names = [{% for room in rooms %}"{{room.functional_name}}"{% if not forloop.last %},{% endif %}{% endfor %}];
|
||||
var room_typelabels = [{% for room in rooms %}"{% for type in room.session_types.all %}{{type.name}}{% if not forloop.last %}, {% endif %}{% endfor %}"{% if not forloop.last %},{% endif %}{% endfor %}];
|
||||
|
||||
var items = new Array();
|
||||
|
||||
{% autoescape off %}
|
||||
{% for slot in unavailable %}
|
||||
if (room_names.indexOf("{{slot.get_hidden_location}}") >= 0 )
|
||||
{
|
||||
|
@ -24,7 +24,7 @@
|
|||
{% for ss in assignments %}
|
||||
if (room_names.indexOf("{{ss.timeslot.get_hidden_location}}") >= 0 )
|
||||
{
|
||||
items.push({room_index:room_names.indexOf("{{ss.timeslot.get_hidden_location}}"),day:{{ss.day}}, delta_from_beginning:{{ss.delta_from_beginning}},time:"{{ss.timeslot.time|date:"Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}", verbose_time:"{{ss.timeslot.time|date:"D M d Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}",duration:{{ss.timeslot.duration.total_seconds}}, type:"{{ss.timeslot.type}}", {% if ss.session.name %}name:"{{ss.session.name|escapejs}}",{% if ss.session.group.acronym %} wg:"{{ss.session.group.acronym}}",{%endif%}{% else %}{% if ss.timeslot.type.name == "Break" %}name:"{{ss.timeslot.name|escapejs}}", area:"break", wg:"break",{% elif ss.timeslot.type.slug == "unavail" %}name:"Unavailable",{% else %}name:"{{ss.session.group.name|escapejs}}{%if ss.session.group.state.name == "BOF"%} BOF{%endif%}",wg:"{{ss.session.group.acronym}}",state:"{{ss.session.group.state}}",area:"{{ss.session.group.parent.acronym}}",{% endif %}{% endif %} dayname:"{{ ss.timeslot.time|date:"l"|upper }}, {{ ss.timeslot.time|date:"F j, Y" }}"{% if ss.session.agenda %}, agenda:"{{ss.session.agenda.get_href}}"{% endif %} });
|
||||
items.push({room_index:room_names.indexOf("{{ss.timeslot.get_hidden_location}}"),day:{{ss.day}}, delta_from_beginning:{{ss.delta_from_beginning}},time:"{{ss.timeslot.time|date:"Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}", verbose_time:"{{ss.timeslot.time|date:"D M d Hi"}}-{{ss.timeslot.end_time|date:"Hi"}}",duration:{{ss.timeslot.duration.total_seconds}}, type:"{{ss.timeslot.type}}", {% if ss.session.name %}name:"{{ss.session.name|escapejs}}",{% if ss.session.group.acronym %} wg:"{{ss.session.group.acronym}}",{%endif%}{% else %}{% if ss.timeslot.type.name == "Break" %}name:"{{ss.timeslot.name|escapejs}}", area:"break", wg:"break",{% elif ss.timeslot.type_id == "unavail" %}name:"Unavailable",{% else %}name:"{{ss.session.group.name|escapejs}}{%if ss.session.group.state.name == "BOF"%} BOF{%endif%}",wg:"{{ss.session.group.acronym}}",state:"{{ss.session.group.state}}",area:"{{ss.session.group.parent.acronym}}",{% endif %}{% endif %} dayname:"{{ ss.timeslot.time|date:"l"|upper }}, {{ ss.timeslot.time|date:"F j, Y" }}"{% if ss.session.agenda %}, agenda:"{{ss.session.agenda.get_href}}"{% endif %}, from_base_schedule: {% if ss.schedule_id != meeting.schedule_id %}true{% else %}false{% endif %} });
|
||||
}
|
||||
{% endfor %}
|
||||
{% endautoescape %}
|
||||
|
@ -353,6 +353,9 @@
|
|||
e.style.padding=padding;
|
||||
e.style.fontFamily="sans-serif";
|
||||
e.style.fontSize="8pt";
|
||||
if (items[i].from_base_schedule)
|
||||
e.style.opacity = 0.5;
|
||||
|
||||
e.id=i;
|
||||
|
||||
e.onmouseover=resize_func(e,sess_top,room_left,
|
||||
|
|
|
@ -1,45 +1,81 @@
|
|||
{% extends "base.html" %}
|
||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||
{# Copyright The IETF Trust 2015-2020, All Rights Reserved #}
|
||||
{% load origin %}
|
||||
{% load static %}
|
||||
{% load ietf_filters %}
|
||||
|
||||
{% block title %}IETF {{ meeting.number }} Meeting Agenda List{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% origin %}
|
||||
<h1>IETF {{meeting.number}} Agenda List</h1>
|
||||
|
||||
{% comment %}
|
||||
<div>
|
||||
<p><a href="{% url "ietf.meeting.views.edit_timeslots" meeting.number %}">Edit Timeslots</a></p>
|
||||
</div>
|
||||
{% endcomment %}
|
||||
<h1>{% block title %}Possible Meeting Agendas for IETF {{ meeting.number }}{% endblock %}</h1>
|
||||
|
||||
<div>
|
||||
{% regroup schedules by is_official as classed_schedules %}
|
||||
{% for class in classed_schedules %}
|
||||
{% for schedules, own, label in schedule_groups %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">{{class.grouper|yesno:"Official,Unofficial"}} Schedule{{class.list|length|pluralize}}</div>
|
||||
<div class="panel-heading">
|
||||
{{ label }}
|
||||
{% if own %}
|
||||
<div class="pull-right" >
|
||||
<a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number %}"><i class="fa fa-plus"></i> New Agenda</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr>
|
||||
<th class="col-md-4">Name</th>
|
||||
<th class="col-md-4">Owner</th>
|
||||
<th class="col-md-2">Name</th>
|
||||
<th class="col-md-2">Owner</th>
|
||||
<th class="col-md-1">Origin</th>
|
||||
<th class="col-md-1">Base</th>
|
||||
<th class="col-md-3">Notes</th>
|
||||
<th class="col-md-1">Visible</th>
|
||||
<th class="col-md-1">Public</th>
|
||||
<th class="col-md-1"></th>
|
||||
<td></td>
|
||||
</tr>
|
||||
{% for schedule in schedules %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.owner_email schedule.name %}" title="Show regular sessions in agenda">{{ schedule.name }}</a>
|
||||
</td>
|
||||
<td>{{ schedule.owner }}</td>
|
||||
<td>
|
||||
{% if schedule.origin %}
|
||||
<a href="{% url "ietf.meeting.views.edit_schedule" meeting.number schedule.origin.owner_email schedule.origin.name %}">{{ schedule.origin.name }}</a>
|
||||
<a href="{% url "ietf.meeting.views.diff_schedules" meeting.number %}?from_schedule={{ schedule.origin.name|urlencode }}&to_schedule={{ schedule.name|urlencode }}" title="{{ schedule.changes_from_origin }} change{{ schedule.changes_from_origin|pluralize }} from {{ schedule.origin.name }}">+{{ schedule.changes_from_origin }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if schedule.base %}
|
||||
<a href="{% url "ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions" meeting.number schedule.base.owner_email schedule.base.name %}">{{ schedule.base.name }}</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ schedule.notes|linebreaksbr }}</td>
|
||||
<td>
|
||||
{% if schedule.visible %}
|
||||
<div class="label label-success">visible</div>
|
||||
{% else %}
|
||||
<div class="label label-danger">hidden</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if schedule.public %}
|
||||
<div class="label label-success">public</div>
|
||||
{% else %}
|
||||
<div class="label label-danger">private</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if schedule.can_edit_properties %}
|
||||
<a class="edit-schedule-properties" href="{% url "ietf.meeting.views.edit_schedule_properties" meeting.number schedule.owner_email schedule.name %}?next={{ request.get_full_path|urlencode }}">
|
||||
<i title="Edit agenda properties" class="fa fa-edit"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{% url "ietf.meeting.views.edit_meeting_timeslots_and_misc_sessions" meeting.number schedule.owner_email schedule.name %}">
|
||||
<i title="Show time slots and misc. sessions for agenda" class="fa fa-calendar"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% for schedule in class.list %}
|
||||
<tr>
|
||||
<td><a href="{% url "ietf.meeting.views.edit_schedule" schedule.meeting.number schedule.owner_email schedule.name %}">
|
||||
{{ schedule.name }}</a></td>
|
||||
<td>{{ schedule.owner }}</td>
|
||||
<td>{{ schedule.visible_token }}</td>
|
||||
<td>{{ schedule.public_token }}</td>
|
||||
<td><a class="btn btn-default" href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">
|
||||
EDIT</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue