From 0555879777cb924f133d4f68fbae842ac97a5be2 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 18 Nov 2019 17:22:19 +0000 Subject: [PATCH 1/9] Rename Meeting.agenda to Meeting.schedule together with a bunch of related internal things - Legacy-Id: 17051 --- ietf/meeting/ajax.py | 111 +++++++++--------- ietf/meeting/factories.py | 10 +- ietf/meeting/forms.py | 6 +- ietf/meeting/helpers.py | 10 +- .../0021_rename_meeting_agenda_to_schedule.py | 28 +++++ ietf/meeting/models.py | 36 +++--- ietf/meeting/resources.py | 4 +- ietf/meeting/test_data.py | 8 +- ietf/meeting/tests_api.py | 94 +++++++-------- ietf/meeting/tests_js.py | 8 +- ietf/meeting/tests_views.py | 54 ++++----- ietf/meeting/urls.py | 20 ++-- ietf/meeting/utils.py | 4 +- ietf/meeting/views.py | 84 ++++++------- ietf/secr/meetings/forms.py | 3 +- ietf/secr/meetings/tests.py | 54 ++++----- ietf/secr/meetings/views.py | 14 +-- ietf/secr/proceedings/proc_utils.py | 6 +- ietf/secr/proceedings/tests.py | 2 +- ietf/secr/proceedings/views.py | 2 +- ietf/secr/sreq/views.py | 2 +- ietf/secr/templates/meetings/view.html | 6 +- .../proceedings/interim_directory.html | 4 +- ietf/secr/utils/meeting.py | 6 +- ietf/templates/meeting/agenda_by_type.html | 2 +- ietf/templates/meeting/delete_schedule.html | 2 +- .../meeting/interim_announcement.txt | 4 +- .../meeting/interim_request_details.html | 2 +- ietf/templates/meeting/landscape_edit.html | 2 +- .../meeting/make_schedule_official.html | 2 +- ietf/templates/meeting/meeting_heading.html | 4 +- ...vate_agenda.html => private_schedule.html} | 2 +- ietf/templates/meeting/properties_edit.html | 2 +- .../{agenda_list.html => schedule_list.html} | 14 +-- 34 files changed, 322 insertions(+), 290 deletions(-) create mode 100644 ietf/meeting/migrations/0021_rename_meeting_agenda_to_schedule.py rename ietf/templates/meeting/{private_agenda.html => private_schedule.html} (87%) rename ietf/templates/meeting/{agenda_list.html => schedule_list.html} (73%) diff --git a/ietf/meeting/ajax.py b/ietf/meeting/ajax.py index 18389b17e..a7aae7acb 100644 --- a/ietf/meeting/ajax.py +++ b/ietf/meeting/ajax.py @@ -1,3 +1,4 @@ +# Copyright The IETF Trust 2013-2019, All Rights Reserved import json from django.shortcuts import get_object_or_404, redirect @@ -7,9 +8,9 @@ from django.http import Http404 from django.views.decorators.http import require_POST from ietf.ietfauth.utils import role_required, has_role -from ietf.meeting.helpers import get_meeting, get_schedule, agenda_permissions, get_person_by_email, get_schedule_by_name +from ietf.meeting.helpers import get_meeting, get_schedule, schedule_permissions, get_person_by_email, get_schedule_by_name from ietf.meeting.models import TimeSlot, Session, Schedule, Room, Constraint, SchedTimeSessAssignment, ResourceAssociation -from ietf.meeting.views import edit_timeslots, edit_agenda +from ietf.meeting.views import edit_timeslots, edit_schedule import debug # pyflakes:ignore @@ -43,8 +44,8 @@ def get_meeting_schedule(num, owner, name): -# should asking if an agenda is read-only require any kind of permission? -def agenda_permission_api(request, num, owner, name): +# should asking if an schedule is read-only require any kind of permission? +def schedule_permission_api(request, num, owner, name): meeting = get_meeting(num) person = get_person_by_email(owner) schedule = get_schedule_by_name(meeting, person, name) @@ -56,7 +57,7 @@ def agenda_permission_api(request, num, owner, name): owner_href = "" if schedule is not None: - cansee,canedit,secretariat = agenda_permissions(meeting, schedule, request.user) + cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user) owner_href = request.build_absolute_uri(schedule.owner.json_url()) if has_role(request.user, "Area Director") or secretariat: @@ -256,39 +257,39 @@ def timeslot_sloturl(request, num=None, slotid=None): return timeslot_delslot(request, meeting, slotid) ############################################################################# -## Agenda List API +## Schedule List API ############################################################################# -AgendaEntryForm = modelform_factory(Schedule, exclude=('meeting','owner')) -EditAgendaEntryForm = modelform_factory(Schedule, exclude=('meeting','owner', 'name')) +ScheduleEntryForm = modelform_factory(Schedule, exclude=('meeting','owner')) +EditScheduleEntryForm = modelform_factory(Schedule, exclude=('meeting','owner', 'name')) @role_required('Area Director','Secretariat') -def agenda_list(request, mtg): - agendas = mtg.schedule_set.all() +def schedule_list(request, mtg): + schedules = mtg.schedule_set.all() json_array=[] - for agenda in agendas: - json_array.append(agenda.json_dict(request.build_absolute_uri('/'))) + for schedule in schedules: + json_array.append(schedule.json_dict(request.build_absolute_uri('/'))) return HttpResponse(json.dumps(json_array), content_type="application/json") # duplicates save-as functionality below. @role_required('Area Director','Secretariat') -def agenda_add(request, meeting): - newagendaform = AgendaEntryForm(request.POST) - if not newagendaform.is_valid(): +def schedule_add(request, meeting): + newscheduleform = ScheduleEntryForm(request.POST) + if not newscheduleform.is_valid(): return HttpResponse(status=404) - newagenda = newagendaform.save(commit=False) - newagenda.meeting = meeting - newagenda.owner = request.user.person - newagenda.save() + newschedule = newscheduleform.save(commit=False) + newschedule.meeting = meeting + newschedule.owner = request.user.person + newschedule.save() if "HTTP_ACCEPT" in request.META and "application/json" in request.META['HTTP_ACCEPT']: - return redirect(agenda_infourl, meeting.number, newagenda.owner_email(), newagenda.name) + return redirect(schedule_infourl, meeting.number, newschedule.owner_email(), newschedule.name) else: - return redirect(edit_agenda, meeting.number, newagenda.owner_email(), newagenda.name) + return redirect(edit_schedule, meeting.number, newschedule.owner_email(), newschedule.name) @require_POST -def agenda_update(request, meeting, schedule): +def schedule_update(request, meeting, schedule): # forms are completely useless for update actions that want to # accept a subset of values. (huh? we could use required=False) @@ -297,7 +298,7 @@ def agenda_update(request, meeting, schedule): if not user.is_authenticated: return HttpResponse({'error':'no permission'}, status=403) - cansee,canedit,secretariat = agenda_permissions(meeting, schedule, request.user) + cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user) #read_only = not canedit ## not used # TODO: Secretariat should always get canedit @@ -316,53 +317,53 @@ def agenda_update(request, meeting, schedule): schedule.save() # enforce that a non-public schedule can not be the public one. - if meeting.agenda == schedule and not schedule.public: - meeting.agenda = None + if meeting.schedule == schedule and not schedule.public: + meeting.schedule = None meeting.save() if "HTTP_ACCEPT" in request.META and "application/json" in request.META['HTTP_ACCEPT']: return HttpResponse(json.dumps(schedule.json_dict(request.build_absolute_uri('/'))), content_type="application/json") else: - return redirect(edit_agenda, meeting.number, schedule.owner_email(), schedule.name) + return redirect(edit_schedule, meeting.number, schedule.owner_email(), schedule.name) @role_required('Secretariat') -def agenda_del(request, meeting, schedule): +def schedule_del(request, meeting, schedule): schedule.delete_assignments() - #debug.log("deleting meeting: %s agenda: %s" % (meeting, meeting.agenda)) - if meeting.agenda == schedule: - meeting.agenda = None + #debug.log("deleting meeting: %s schedule: %s" % (meeting, meeting.schedule)) + if meeting.schedule == schedule: + meeting.schedule = None meeting.save() schedule.delete() return HttpResponse('{"error":"none"}', status = 200) -def agenda_infosurl(request, num=None): +def schedule_infosurl(request, num=None): meeting = get_meeting(num) if request.method == 'GET': - return agenda_list(request, meeting) + return schedule_list(request, meeting) elif request.method == 'POST': - return agenda_add(request, meeting) + return schedule_add(request, meeting) # unacceptable action return HttpResponse(status=406) -def agenda_infourl(request, num=None, owner=None, name=None): +def schedule_infourl(request, num=None, owner=None, name=None): meeting = get_meeting(num) person = get_person_by_email(owner) schedule = get_schedule_by_name(meeting, person, name) if schedule is None: raise Http404("No meeting information for meeting %s schedule %s available" % (num,name)) - #debug.log("results in agenda: %u / %s" % (schedule.id, request.method)) + #debug.log("results in schedule: %u / %s" % (schedule.id, request.method)) if request.method == 'GET': return HttpResponse(json.dumps(schedule.json_dict(request.build_absolute_uri('/'))), content_type="application/json") elif request.method == 'POST': - return agenda_update(request, meeting, schedule) + return schedule_update(request, meeting, schedule) elif request.method == 'DELETE': - return agenda_del(request, meeting, schedule) + return schedule_del(request, meeting, schedule) else: return HttpResponse(status=406) @@ -377,22 +378,22 @@ def meeting_get(request, meeting): @role_required('Secretariat') def meeting_update(request, meeting): - # at present, only the official agenda can be updated from this interface. + # at present, only the official schedule can be updated from this interface. - #debug.log("1 meeting.agenda: %s / %s / %s" % (meeting.agenda, update_dict, request.body)) - if "agenda" in request.POST: - value = request.POST["agenda"] - #debug.log("4 meeting.agenda: %s" % (value)) + #debug.log("1 meeting.schedule: %s / %s / %s" % (meeting.schedule, update_dict, request.body)) + if "schedule" in request.POST: + value = request.POST["schedule"] + #debug.log("4 meeting.schedule: %s" % (value)) if not value or value == "None": # value == "None" is just weird, better with empty string - meeting.set_official_agenda(None) + meeting.set_official_schedule(None) else: schedule = get_schedule(meeting, value) if not schedule.public: return HttpResponse(status = 406) - #debug.log("3 meeting.agenda: %s" % (schedule)) - meeting.set_official_agenda(schedule) + #debug.log("3 meeting.schedule: %s" % (schedule)) + meeting.set_official_schedule(schedule) - #debug.log("2 meeting.agenda: %s" % (meeting.agenda)) + #debug.log("2 meeting.schedule: %s" % (meeting.schedule)) meeting.save() return meeting_get(request, meeting) @@ -442,9 +443,9 @@ def sessions_json(request, num): # this creates an entirely *NEW* schedtimesessassignment def assignments_post(request, meeting, schedule): - cansee,canedit,secretariat = agenda_permissions(meeting, schedule, request.user) + cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user) if not canedit: - return HttpResponse(json.dumps({'error':'no permission to modify this agenda'}), + return HttpResponse(json.dumps({'error':'no permission to modify this schedule'}), status = 403, content_type="application/json") @@ -491,7 +492,7 @@ def assignments_get(request, num, schedule): return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2), content_type="application/json") -# this returns the list of scheduled sessions for the given named agenda +# this returns the list of scheduled sessions for the given named schedule def assignments_json(request, num, owner, name): info = get_meeting_schedule(num, owner, name) # The return values from get_meeting_schedule() are silly, in that it @@ -512,9 +513,9 @@ def assignments_json(request, num, owner, name): # accepts both POST and PUT in order to implement Postel Doctrine. def assignment_update(request, meeting, schedule, ss): - cansee,canedit,secretariat = agenda_permissions(meeting, schedule, request.user) + cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user) if not canedit: - return HttpResponse(json.dumps({'error':'no permission to update this agenda'}), + return HttpResponse(json.dumps({'error':'no permission to update this schedule'}), status = 403, content_type="application/json") @@ -530,9 +531,9 @@ def assignment_update(request, meeting, schedule, ss): content_type="application/json") def assignment_delete(request, meeting, schedule, ss): - cansee,canedit,secretariat = agenda_permissions(meeting, schedule, request.user) + cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user) if not canedit: - return HttpResponse(json.dumps({'error':'no permission to update this agenda'}), + return HttpResponse(json.dumps({'error':'no permission to update this schedule'}), status = 403, content_type="application/json") @@ -552,10 +553,10 @@ def assignment_delete(request, meeting, schedule, ss): content_type="application/json") def assignment_get(request, meeting, schedule, ss): - cansee,canedit,secretariat = agenda_permissions(meeting, schedule, request.user) + cansee,canedit,secretariat = schedule_permissions(meeting, schedule, request.user) if not cansee: - return HttpResponse(json.dumps({'error':'no permission to see this agenda'}), + return HttpResponse(json.dumps({'error':'no permission to see this schedule'}), status = 403, content_type="application/json") diff --git a/ietf/meeting/factories.py b/ietf/meeting/factories.py index df54dd646..0da2c2320 100644 --- a/ietf/meeting/factories.py +++ b/ietf/meeting/factories.py @@ -63,9 +63,9 @@ class MeetingFactory(factory.DjangoModelFactory): @factory.post_generation - def populate_agenda(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument + def populate_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument ''' - Create a default agenda, unless the factory is called + Create a default schedule, unless the factory is called with populate_agenda=False ''' if extracted is None: @@ -73,7 +73,7 @@ class MeetingFactory(factory.DjangoModelFactory): if create and extracted: for x in range(3): TimeSlotFactory(meeting=obj) - obj.agenda = ScheduleFactory(meeting=obj) + obj.schedule = ScheduleFactory(meeting=obj) obj.save() class SessionFactory(factory.DjangoModelFactory): @@ -96,7 +96,7 @@ class SessionFactory(factory.DjangoModelFactory): extracted = True if create and extracted: ts = obj.meeting.timeslot_set.all() - obj.timeslotassignments.create(timeslot=ts[random.randrange(len(ts))],schedule=obj.meeting.agenda) + obj.timeslotassignments.create(timeslot=ts[random.randrange(len(ts))],schedule=obj.meeting.schedule) class ScheduleFactory(factory.DjangoModelFactory): class Meta: @@ -175,4 +175,4 @@ class SlideSubmissionFactory(factory.DjangoModelFactory): make_file = factory.PostGeneration( lambda obj, create, extracted, **kwargs: open(obj.staged_filepath(),'a').close() - ) \ No newline at end of file + ) diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index ebb1edace..a79eb29b7 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -187,11 +187,11 @@ class InterimMeetingModelForm(forms.ModelForm): if kwargs.get('commit', True): # create schedule with meeting meeting.save() # pre-save so we have meeting.pk for schedule - if not meeting.agenda: - meeting.agenda = Schedule.objects.create( + if not meeting.schedule: + meeting.schedule = Schedule.objects.create( meeting=meeting, owner=Person.objects.get(name='(System)')) - meeting.save() # save with agenda + meeting.save() # save with schedule # create directories make_materials_directories(meeting) diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 54c467c9f..0c15b4446 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -138,14 +138,14 @@ def get_ietf_meeting(num=None): def get_schedule(meeting, name=None): if name is None: - schedule = meeting.agenda + schedule = meeting.schedule else: 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.agenda + schedule = meeting.schedule else: schedule = get_object_or_404(meeting.schedule_set, id=int(schedid)) return schedule @@ -271,7 +271,7 @@ def convert_draft_to_pdf(doc_name): pipe("ps2pdf "+psname+" "+outpath) os.unlink(psname) -def agenda_permissions(meeting, schedule, user): +def schedule_permissions(meeting, schedule, user): # do this in positive logic. cansee = False canedit = False @@ -389,7 +389,7 @@ def create_interim_meeting(group, date, city='', country='', timezone='UTC', owner=person, visible=True, public=True) - meeting.agenda = schedule + meeting.schedule = schedule meeting.save() return meeting @@ -622,7 +622,7 @@ def update_interim_session_assignment(form): SchedTimeSessAssignment.objects.create( timeslot=slot, session=session, - schedule=session.meeting.agenda) + schedule=session.meeting.schedule) def populate_important_dates(meeting): assert ImportantDate.objects.filter(meeting=meeting).exists() is False diff --git a/ietf/meeting/migrations/0021_rename_meeting_agenda_to_schedule.py b/ietf/meeting/migrations/0021_rename_meeting_agenda_to_schedule.py new file mode 100644 index 000000000..63bc646e7 --- /dev/null +++ b/ietf/meeting/migrations/0021_rename_meeting_agenda_to_schedule.py @@ -0,0 +1,28 @@ +# Copyright The IETF Trust 2019, All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2019-11-18 04:01 +from __future__ import unicode_literals + +from django.db import migrations +import django.db.models.deletion +import ietf.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0020_remove_future_break_sessions'), + ] + + operations = [ + migrations.RenameField( + model_name='meeting', + old_name='agenda', + new_name='schedule', + ), + migrations.AlterField( + model_name='schedule', + name='meeting', + field=ietf.utils.models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='schedule_set', to='meeting.Meeting'), + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 5e733c51e..04a0d4c3a 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -102,7 +102,7 @@ class Meeting(models.Model): reg_area = models.CharField(blank=True, max_length=255) agenda_info_note = models.TextField(blank=True, help_text="Text in this field will be placed at the top of the html agenda page for the meeting. HTML can be used, but will not be validated.") agenda_warning_note = models.TextField(blank=True, help_text="Text in this field will be placed more prominently at the top of the html agenda page for the meeting. HTML can be used, but will not be validated.") - agenda = ForeignKey('Schedule',null=True,blank=True, related_name='+') + schedule = ForeignKey('Schedule',null=True,blank=True, related_name='+') session_request_lock_message = models.CharField(blank=True,max_length=255) # locked if not empty proceedings_final = models.BooleanField(default=False, help_text="Are the proceedings for this meeting complete?") acknowledgements = models.TextField(blank=True, help_text="Acknowledgements for use in meeting proceedings. Use ReStructuredText markup.") @@ -217,8 +217,8 @@ class Meeting(models.Model): # unfortunately, using the datetime aware json encoder seems impossible, # so the dates are formatted as strings here. agenda_url = "" - if self.agenda: - agenda_url = urljoin(host_scheme, self.agenda.base_url()) + if self.schedule: + agenda_url = urljoin(host_scheme, self.schedule.base_url()) return { 'href': urljoin(host_scheme, self.json_url()), 'name': self.number, @@ -290,16 +290,16 @@ class Meeting(models.Model): pass return '' - def set_official_agenda(self, agenda): - if self.agenda != agenda: - self.agenda = agenda + def set_official_schedule(self, schedule): + if self.schedule != schedule: + self.schedule = schedule self.save() def updated(self): 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.agenda.assignments.aggregate(Max('modified'))["modified__max"] or min_time) if self.agenda else min_time + assignments_updated = (self.schedule.assignments.aggregate(Max('modified'))["modified__max"] or min_time) if self.schedule else min_time ts = max(timeslots_updated, sessions_updated, assignments_updated) tz = pytz.timezone(settings.PRODUCTION_TIMEZONE) ts = tz.localize(ts) @@ -459,7 +459,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.agenda).first() + self._session_cache = self.sessions.filter(timeslotassignments__schedule=self.meeting.schedule).first() return self._session_cache @property @@ -609,15 +609,15 @@ class TimeSlot(models.Model): @python_2_unicode_compatible class Schedule(models.Model): """ - Each person may have multiple agendas saved. - An Agenda may be made visible, which means that it will show up in + Each person may have multiple schedules saved. + An Schedule may be made visible, which means that it will show up in public drop down menus, etc. It may also be made public, which means that someone who knows about it by name/id would be able to reference - it. A non-visible, public agenda might be passed around by the + it. A non-visible, public schedule might be passed around by the Secretariat to IESG members for review. Only the owner may edit the - agenda, others may copy it + schedule, others may copy it """ - meeting = ForeignKey(Meeting, null=True) + meeting = ForeignKey(Meeting, null=True, related_name='schedule_set') name = models.CharField(max_length=16, blank=False) owner = ForeignKey(Person) visible = models.BooleanField(default=True, help_text="Make this agenda available to those who know about it.") @@ -666,7 +666,7 @@ class Schedule(models.Model): @property def is_official(self): - return (self.meeting.agenda == self) + return (self.meeting.schedule == self) # returns a dictionary {group -> [schedtimesessassignment+]} # and it has [] if the session is not placed. @@ -720,7 +720,7 @@ class Schedule(models.Model): class SchedTimeSessAssignment(models.Model): """ This model provides an N:M relationship between Session and TimeSlot. - Each relationship is attached to the named agenda, which is owned by + Each relationship is attached to the named schedule, which is owned by a specific person/user. """ timeslot = ForeignKey('TimeSlot', null=False, blank=False, related_name='sessionassignments') @@ -1032,7 +1032,7 @@ class Session(models.Model): ss0name = "(%s)" % self.status.name else: ss0name = "(unscheduled)" - ss = self.timeslotassignments.filter(schedule=self.meeting.agenda).order_by('timeslot__time') + ss = self.timeslotassignments.filter(schedule=self.meeting.schedule).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) @@ -1065,11 +1065,11 @@ 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_agenda(self, schedule): + def timeslotassignment_for_schedule(self, schedule): return self.timeslotassignments.filter(schedule=schedule).first() def official_timeslotassignment(self): - return self.timeslotassignment_for_agenda(self.meeting.agenda) + return self.timeslotassignment_for_schedule(self.meeting.schedule) def constraints_dict(self, host_scheme): constraint_list = [] diff --git a/ietf/meeting/resources.py b/ietf/meeting/resources.py index a8783c295..6bcdf33cb 100644 --- a/ietf/meeting/resources.py +++ b/ietf/meeting/resources.py @@ -18,7 +18,7 @@ from ietf.meeting.models import ( Meeting, ResourceAssociation, Constraint, Room from ietf.name.resources import MeetingTypeNameResource class MeetingResource(ModelResource): type = ToOneField(MeetingTypeNameResource, 'type') - agenda = ToOneField('ietf.meeting.resources.ScheduleResource', 'agenda', null=True) + schedule = ToOneField('ietf.meeting.resources.ScheduleResource', 'schedule', null=True) updated = DateTimeField(attribute='updated') class Meta: cache = SimpleCache() @@ -48,7 +48,7 @@ class MeetingResource(ModelResource): "agenda_warning_note": ALL, "session_request_lock_message": ALL, "type": ALL_WITH_RELATIONS, - "agenda": ALL_WITH_RELATIONS, + "schedule": ALL_WITH_RELATIONS, "proceedings_final": ALL, } api.meeting.register(MeetingResource()) diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index 433b1dc1c..e311c4be3 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -37,7 +37,7 @@ def make_interim_meeting(group,date,status='sched'): SchedTimeSessAssignment.objects.create( timeslot=slot, session=session, - schedule=session.meeting.agenda) + schedule=session.meeting.schedule) # agenda name = "agenda-%s-%s-%s" % (meeting.number, group.acronym, "01") rev = '00' @@ -77,8 +77,8 @@ def make_meeting_test_data(meeting=None): if not meeting: meeting = Meeting.objects.get(number="72", type="ietf") - schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-agenda", visible=True, public=True) - unofficial_schedule = Schedule.objects.create(meeting=meeting, owner=plainman, name="test-unofficial-agenda", visible=True, public=True) + 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) # test room pname = RoomResourceName.objects.create(name='projector',slug='proj') @@ -156,7 +156,7 @@ def make_meeting_test_data(meeting=None): scheduled=datetime.datetime.now(),type_id="break") SchedTimeSessAssignment.objects.create(timeslot=break_slot, session=break_session, schedule=schedule) - meeting.agenda = schedule + meeting.schedule = schedule meeting.save() # Convenience for the tests diff --git a/ietf/meeting/tests_api.py b/ietf/meeting/tests_api.py index dc9b788bf..06f267da3 100644 --- a/ietf/meeting/tests_api.py +++ b/ietf/meeting/tests_api.py @@ -21,16 +21,16 @@ from ietf.utils.mail import outbox class ApiTests(TestCase): - def test_update_agenda(self): + def test_update_schedule(self): meeting = make_meeting_test_data() - schedule = Schedule.objects.get(meeting__number=72,name="test-agenda") + schedule = Schedule.objects.get(meeting__number=72,name="test-schedule") mars_session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() ames_session = Session.objects.filter(meeting=meeting, group__acronym="ames").first() - mars_scheduled = SchedTimeSessAssignment.objects.get(session=mars_session,schedule__name='test-agenda') + mars_scheduled = SchedTimeSessAssignment.objects.get(session=mars_session,schedule__name='test-schedule') mars_slot = mars_scheduled.timeslot - ames_scheduled = SchedTimeSessAssignment.objects.get(session=ames_session,schedule__name='test-agenda') + ames_scheduled = SchedTimeSessAssignment.objects.get(session=ames_session,schedule__name='test-schedule') ames_slot = ames_scheduled.timeslot def do_unschedule(assignment): @@ -96,16 +96,16 @@ class ApiTests(TestCase): r = do_extend(schedule,mars_scheduled) self.assertEqual(r.status_code, 201) self.assertTrue("error" not in r.json()) - self.assertEqual(mars_session.timeslotassignments.filter(schedule__name='test-agenda').count(),2) + self.assertEqual(mars_session.timeslotassignments.filter(schedule__name='test-schedule').count(),2) # Unschedule mars r = do_unschedule(mars_scheduled) self.assertEqual(r.status_code, 200) self.assertNotIn("error", r.json()) # Make sure it got both the original and extended session - self.assertEqual(mars_session.timeslotassignments.filter(schedule__name='test-agenda').count(),0) + self.assertEqual(mars_session.timeslotassignments.filter(schedule__name='test-schedule').count(),0) - self.assertEqual(SchedTimeSessAssignment.objects.get(session=ames_session,schedule__name='test-agenda').timeslot, mars_slot) + self.assertEqual(SchedTimeSessAssignment.objects.get(session=ames_session,schedule__name='test-schedule').timeslot, mars_slot) def test_constraints_json(self): @@ -212,9 +212,9 @@ class ApiTests(TestCase): r = self.client.get(url) self.assertEqual(r.status_code, 200) info = r.json() - self.assertEqual(set([x['short_name'] for x in info]),set([s.session.short_name for s in meeting.agenda.assignments.filter(session__type_id='session')])) + self.assertEqual(set([x['short_name'] for x in info]),set([s.session.short_name for s in meeting.schedule.assignments.filter(session__type_id='session')])) - schedule = meeting.agenda + schedule = meeting.schedule url = urlreverse("ietf.meeting.ajax.assignments_json", kwargs=dict(num=meeting.number,owner=schedule.owner_email(),name=schedule.name)) r = self.client.get(url) @@ -281,45 +281,45 @@ class ApiTests(TestCase): def test_schedule_json(self): meeting = make_meeting_test_data() - url = urlreverse("ietf.meeting.ajax.agenda_infourl", + url = urlreverse("ietf.meeting.ajax.schedule_infourl", kwargs=dict(num=meeting.number, - owner=meeting.agenda.owner_email(), - name=meeting.agenda.name)) + owner=meeting.schedule.owner_email(), + name=meeting.schedule.name)) r = self.client.get(url) info = r.json() - self.assertEqual(info["schedule_id"], meeting.agenda.pk) + self.assertEqual(info["schedule_id"], meeting.schedule.pk) def test_create_new_schedule(self): meeting = make_meeting_test_data() - url = urlreverse("ietf.meeting.ajax.agenda_infosurl", + url = urlreverse("ietf.meeting.ajax.schedule_infosurl", kwargs=dict(num=meeting.number)) post_data = { - 'name': 'new-agenda', + 'name': 'new-schedule', } # unauthorized post self.client.login(username="plain", password="plain+password") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 403) - self.assertTrue(not meeting.schedule_set.filter(name='new-agenda')) + self.assertTrue(not meeting.schedule_set.filter(name='new-schedule')) - # create new agenda + # create new schedule self.client.login(username="secretary", password="secretary+password") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) - self.assertTrue(meeting.schedule_set.filter(name='new-agenda')) + self.assertTrue(meeting.schedule_set.filter(name='new-schedule')) - def test_update_schedule(self): + def test_update_meeting_schedule(self): meeting = make_meeting_test_data() - self.assertTrue(meeting.agenda.visible) + self.assertTrue(meeting.schedule.visible) - url = urlreverse("ietf.meeting.ajax.agenda_infourl", + url = urlreverse("ietf.meeting.ajax.schedule_infourl", kwargs=dict(num=meeting.number, - owner=meeting.agenda.owner_email(), - name=meeting.agenda.name)) + owner=meeting.schedule.owner_email(), + name=meeting.schedule.name)) post_data = { 'visible': 'false', @@ -334,21 +334,21 @@ class ApiTests(TestCase): r = self.client.post(url, post_data) self.assertEqual(r.status_code, 403) - # change agenda + # change schedule self.client.login(username="secretary", password="secretary+password") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 302) - changed_schedule = Schedule.objects.get(pk=meeting.agenda.pk) + changed_schedule = Schedule.objects.get(pk=meeting.schedule.pk) self.assertTrue(not changed_schedule.visible) self.assertEqual(changed_schedule.name, "new-test-name") def test_delete_schedule(self): meeting = make_meeting_test_data() - url = urlreverse("ietf.meeting.ajax.agenda_infourl", + url = urlreverse("ietf.meeting.ajax.schedule_infourl", kwargs=dict(num=meeting.number, - owner=meeting.agenda.owner_email(), - name=meeting.agenda.name)) + owner=meeting.schedule.owner_email(), + name=meeting.schedule.name)) # unauthorized delete self.client.login(username="plain", password="plain+password") r = self.client.delete(url) @@ -358,16 +358,16 @@ class ApiTests(TestCase): self.client.login(username="secretary", password="secretary+password") r = self.client.delete(url) self.assertEqual(r.status_code, 200) - self.assertTrue(not Schedule.objects.filter(pk=meeting.agenda.pk)) + self.assertTrue(not Schedule.objects.filter(pk=meeting.schedule.pk)) - def test_set_meeting_agenda(self): + def test_set_meeting_schedule(self): meeting = make_meeting_test_data() - schedule = meeting.agenda + schedule = meeting.schedule url = urlreverse("ietf.meeting.ajax.meeting_json", kwargs=dict(num=meeting.number)) post_data = { - "agenda": "", + "schedule": "", } # unauthorized post self.client.login(username="ad", password="ad+password") @@ -378,18 +378,18 @@ class ApiTests(TestCase): self.client.login(username="secretary", password="secretary+password") r = self.client.post(url, post_data) self.assertEqual(r.status_code, 200) - self.assertTrue(not Meeting.objects.get(pk=meeting.pk).agenda) + self.assertTrue(not Meeting.objects.get(pk=meeting.pk).schedule) - # set agenda - first fail with non-public + # set schedule - first fail with non-public post_data = { - "agenda": schedule.name, + "schedule": schedule.name, } schedule.public = False schedule.save() r = self.client.post(url, post_data) self.assertTrue(r.status_code != 200) - self.assertTrue(not Meeting.objects.get(pk=meeting.pk).agenda) + self.assertTrue(not Meeting.objects.get(pk=meeting.pk).schedule) # then go through with public schedule.public = True @@ -399,7 +399,7 @@ class ApiTests(TestCase): prior_length= len(outbox) r = self.client.post(url, post_data) self.assertEqual(r.status_code, 200) - self.assertEqual(Meeting.objects.get(pk=meeting.pk).agenda, schedule) + self.assertEqual(Meeting.objects.get(pk=meeting.pk).schedule, schedule) self.assertEqual(len(outbox),prior_length) def test_read_only(self): @@ -407,20 +407,20 @@ class ApiTests(TestCase): # Secretariat self.client.login(username="secretary", password="secretary+password") - url = '/meeting/%s/agenda/%s/%s/permissions' % (meeting.number, meeting.agenda.owner.email_address(), meeting.agenda.name); + url = '/meeting/%s/agenda/%s/%s/permissions' % (meeting.number, meeting.schedule.owner.email_address(), meeting.schedule.name); r = self.client.get(url) self.assertEqual(r.status_code, 200) info = r.json() self.assertEqual(info['secretariat'], True) - self.assertEqual(urlsplit(info['owner_href'])[2], "/person/%s.json" % meeting.agenda.owner_id) + self.assertEqual(urlsplit(info['owner_href'])[2], "/person/%s.json" % meeting.schedule.owner_id) self.assertEqual(info['read_only'], True) self.assertEqual(info['save_perm'], True) # owner - self.client.login(username=meeting.agenda.owner.user.username, - password=meeting.agenda.owner.user.username+"+password") - url = '/meeting/%s/agenda/%s/%s/permissions' % (meeting.number, meeting.agenda.owner.email_address(), meeting.agenda.name); + self.client.login(username=meeting.schedule.owner.user.username, + password=meeting.schedule.owner.user.username+"+password") + url = '/meeting/%s/agenda/%s/%s/permissions' % (meeting.number, meeting.schedule.owner.email_address(), meeting.schedule.name); r = self.client.get(url) self.assertEqual(r.status_code, 200) @@ -434,7 +434,7 @@ class ApiTests(TestCase): scheduled = SchedTimeSessAssignment.objects.filter( session__meeting=meeting, session__group__acronym="mars").first() - url = '/meeting/%s/agenda/%s/%s/session/%u.json' % (meeting.number, meeting.agenda.owner_email(), meeting.agenda.name, scheduled.pk) + url = '/meeting/%s/agenda/%s/%s/session/%u.json' % (meeting.number, meeting.schedule.owner_email(), meeting.schedule.name, scheduled.pk) post_data = { "pinned": True @@ -448,11 +448,11 @@ class ApiTests(TestCase): self.assertTrue(not SchedTimeSessAssignment.objects.get(pk=scheduled.pk).pinned) # set pinned - meeting.agenda.owner = Person.objects.get(user__username="secretary") - meeting.agenda.save() + meeting.schedule.owner = Person.objects.get(user__username="secretary") + meeting.schedule.save() - # need to rebuild URL, since the agenda owner has changed. - url = '/meeting/%s/agenda/%s/%s/session/%u.json' % (meeting.number, meeting.agenda.owner_email(), meeting.agenda.name, scheduled.pk) + # need to rebuild URL, since the schedule owner has changed. + url = '/meeting/%s/agenda/%s/%s/session/%u.json' % (meeting.number, meeting.schedule.owner_email(), meeting.schedule.name, scheduled.pk) self.client.login(username="secretary", password="secretary+password") r = self.client.put(url, post_data) diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index b306814aa..50ec81424 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -84,10 +84,10 @@ class ScheduleEditTests(StaticLiveServerTestCase): def testUnschedule(self): - self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-agenda').count(),1) + self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule').count(),1) self.login() - url = self.absreverse('ietf.meeting.views.edit_agenda',kwargs=dict(num='72',name='test-agenda',owner='plain@example.com')) + url = self.absreverse('ietf.meeting.views.edit_schedule',kwargs=dict(num='72',name='test-schedule',owner='plain@example.com')) self.driver.get(url) q = PyQuery(self.driver.page_source) @@ -101,7 +101,7 @@ class ScheduleEditTests(StaticLiveServerTestCase): self.assertTrue(len(q('#sortable-list #session_1'))>0) time.sleep(0.1) # The API that modifies the database runs async - self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-agenda').count(),0) + self.assertEqual(SchedTimeSessAssignment.objects.filter(session__meeting__number=72,session__group__acronym='mars',schedule__name='test-schedule').count(),0) @skipIf(skip_selenium, skip_message) class SlideReorderTests(StaticLiveServerTestCase): @@ -173,5 +173,5 @@ class SlideReorderTests(StaticLiveServerTestCase): # condition_data() # # def testOpenSchedule(self): -# url = urlreverse('ietf.meeting.views.edit_agenda', kwargs=dict(num='72',name='test-agenda')) +# url = urlreverse('ietf.meeting.views.edit_schedule', kwargs=dict(num='72',name='test-schedule')) # r = self.client.get(url) diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 4d19c4c66..7c23c3301 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -96,7 +96,7 @@ class MeetingTests(TestCase): def test_meeting_agenda(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() - slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule=meeting.agenda) + slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule=meeting.schedule) # self.write_materials_files(meeting, session) # @@ -475,7 +475,7 @@ class MeetingTests(TestCase): # Create an extra session t2 = TimeSlotFactory.create(meeting=meeting, time=datetime.datetime.combine(meeting.date, datetime.time(11, 30))) s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False) - SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.agenda) + SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule) # url = urlreverse('ietf.meeting.views.ical_agenda', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, }) r = self.client.get(url) @@ -537,14 +537,14 @@ class MeetingTests(TestCase): response = self.client.get(url) self.assertEqual(response.status_code, 302) - def test_edit_agenda_properties(self): + def test_edit_schedule_properties(self): self.client.login(username='secretary',password='secretary+password') - url = urlreverse('ietf.meeting.views.edit_agenda_properties',kwargs={'owner':'does@notexist.example','name':'doesnotexist','num':00}) + url = urlreverse('ietf.meeting.views.edit_schedule_properties',kwargs={'owner':'does@notexist.example','name':'doesnotexist','num':00}) response = self.client.get(url) self.assertEqual(response.status_code,404) self.client.logout() schedule = ScheduleFactory(meeting__type_id='ietf',visible=False,public=False) - url = urlreverse('ietf.meeting.views.edit_agenda_properties',kwargs={'owner':schedule.owner.email(),'name':schedule.name,'num':schedule.meeting.number}) + url = urlreverse('ietf.meeting.views.edit_schedule_properties',kwargs={'owner':schedule.owner.email(),'name':schedule.name,'num':schedule.meeting.number}) response = self.client.get(url) self.assertEqual(response.status_code,302) self.client.login(username='secretary',password='secretary+password') @@ -590,27 +590,27 @@ class EditTests(TestCase): fg_group_colors[area_upper] = "#333" bg_group_colors[area_upper] = "#aaa" - def test_edit_agenda(self): + def test_edit_schedule(self): meeting = make_meeting_test_data() self.client.login(username="secretary", password="secretary+password") - r = self.client.get(urlreverse("ietf.meeting.views.edit_agenda", kwargs=dict(num=meeting.number))) + r = self.client.get(urlreverse("ietf.meeting.views.edit_schedule", kwargs=dict(num=meeting.number))) self.assertContains(r, "load_assignments") def test_save_agenda_as_and_read_permissions(self): meeting = make_meeting_test_data() # try to get non-existing agenda - url = urlreverse("ietf.meeting.views.edit_agenda", kwargs=dict(num=meeting.number, - owner=meeting.agenda.owner_email(), + url = urlreverse("ietf.meeting.views.edit_schedule", kwargs=dict(num=meeting.number, + owner=meeting.schedule.owner_email(), name="foo")) r = self.client.get(url) self.assertEqual(r.status_code, 404) # save as new name (requires valid existing agenda) - url = urlreverse("ietf.meeting.views.edit_agenda", kwargs=dict(num=meeting.number, - owner=meeting.agenda.owner_email(), - name=meeting.agenda.name)) + url = urlreverse("ietf.meeting.views.edit_schedule", kwargs=dict(num=meeting.number, + owner=meeting.schedule.owner_email(), + name=meeting.schedule.name)) self.client.login(username="ad", password="ad+password") r = self.client.post(url, { 'savename': "foo", @@ -622,7 +622,7 @@ class EditTests(TestCase): # get schedule = meeting.get_schedule_by_name("foo") - url = urlreverse("ietf.meeting.views.edit_agenda", kwargs=dict(num=meeting.number, + url = urlreverse("ietf.meeting.views.edit_schedule", kwargs=dict(num=meeting.number, owner=schedule.owner_email(), name="foo")) r = self.client.get(url) @@ -655,9 +655,9 @@ class EditTests(TestCase): meeting = make_meeting_test_data() # save as new name (requires valid existing agenda) - url = urlreverse("ietf.meeting.views.edit_agenda", kwargs=dict(num=meeting.number, - owner=meeting.agenda.owner_email(), - name=meeting.agenda.name)) + url = urlreverse("ietf.meeting.views.edit_schedule", kwargs=dict(num=meeting.number, + owner=meeting.schedule.owner_email(), + name=meeting.schedule.name)) self.client.login(username="ad", password="ad+password") r = self.client.post(url, { 'savename': "/no/this/should/not/work/it/is/too/long", @@ -706,12 +706,12 @@ class EditTests(TestCase): def test_slot_to_the_right(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() - mars_scheduled = session.timeslotassignments.get(schedule__name='test-agenda') - mars_slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule__name='test-agenda') + mars_scheduled = session.timeslotassignments.get(schedule__name='test-schedule') + mars_slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule__name='test-schedule') mars_ends = mars_slot.time + mars_slot.duration session = Session.objects.filter(meeting=meeting, group__acronym="ames").first() - ames_slot_qs = TimeSlot.objects.filter(sessionassignments__session=session,sessionassignments__schedule__name='test-agenda') + ames_slot_qs = TimeSlot.objects.filter(sessionassignments__session=session,sessionassignments__schedule__name='test-schedule') ames_slot_qs.update(time=mars_ends + datetime.timedelta(seconds=11 * 60)) self.assertTrue(not mars_slot.slot_to_the_right) @@ -784,8 +784,8 @@ class EditScheduleListTests(TestCase): self.mtg = MeetingFactory(type_id='ietf') ScheduleFactory(meeting=self.mtg,name='Empty-Schedule') - def test_list_agendas(self): - url = urlreverse('ietf.meeting.views.list_agendas',kwargs={'num':self.mtg.number}) + def test_list_schedules(self): + url = urlreverse('ietf.meeting.views.list_schedules',kwargs={'num':self.mtg.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) self.assertTrue(r.status_code, 200) @@ -793,8 +793,8 @@ class EditScheduleListTests(TestCase): def test_delete_schedule(self): url = urlreverse('ietf.meeting.views.delete_schedule', kwargs={'num':self.mtg.number, - 'owner':self.mtg.agenda.owner.email_address(), - 'name':self.mtg.agenda.name, + 'owner':self.mtg.schedule.owner.email_address(), + 'name':self.mtg.schedule.name, }) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) @@ -802,7 +802,7 @@ class EditScheduleListTests(TestCase): r = self.client.post(url,{'save':1}) self.assertTrue(r.status_code, 403) self.assertEqual(self.mtg.schedule_set.count(),2) - self.mtg.agenda=None + self.mtg.schedule=None self.mtg.save() r = self.client.get(url) self.assertTrue(r.status_code, 200) @@ -811,7 +811,7 @@ class EditScheduleListTests(TestCase): self.assertEqual(self.mtg.schedule_set.count(),1) def test_make_schedule_official(self): - schedule = self.mtg.schedule_set.exclude(id=self.mtg.agenda.id).first() + schedule = self.mtg.schedule_set.exclude(id=self.mtg.schedule.id).first() url = urlreverse('ietf.meeting.views.make_schedule_official', kwargs={'num':self.mtg.number, 'owner':schedule.owner.email_address(), @@ -823,7 +823,7 @@ class EditScheduleListTests(TestCase): r = self.client.post(url,{'save':1}) self.assertTrue(r.status_code, 302) mtg = Meeting.objects.get(number=self.mtg.number) - self.assertEqual(mtg.agenda,schedule) + self.assertEqual(mtg.schedule,schedule) # ------------------------------------------------- # Interim Meeting Tests @@ -1594,7 +1594,7 @@ class InterimTests(TestCase): # Create an extra session t2 = TimeSlotFactory.create(meeting=meeting, time=datetime.datetime.combine(meeting.date, datetime.time(11, 30))) s2 = SessionFactory.create(meeting=meeting, group=s1.group, add_to_schedule=False) - SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.agenda) + SchedTimeSessAssignment.objects.create(timeslot=t2, session=s2, schedule=meeting.schedule) # url = urlreverse('ietf.meeting.views.ical_agenda', kwargs={'num':meeting.number, 'acronym':s1.group.acronym, }) r = self.client.get(url) diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py index e52a3ab69..e2f255bf8 100644 --- a/ietf/meeting/urls.py +++ b/ietf/meeting/urls.py @@ -1,4 +1,4 @@ -# Copyright The IETF Trust 2007, All Rights Reserved +# Copyright The IETF Trust 2007-2019, All Rights Reserved from django.conf.urls import include from django.views.generic import RedirectView @@ -24,8 +24,8 @@ safe_for_all_meeting_types = [ type_ietf_only_patterns = [ - url(r'^agenda/%(owner)s/%(schedule_name)s/edit$' % settings.URL_REGEXPS, views.edit_agenda), - url(r'^agenda/%(owner)s/%(schedule_name)s/details$' % settings.URL_REGEXPS, views.edit_agenda_properties), + url(r'^agenda/%(owner)s/%(schedule_name)s/edit$' % settings.URL_REGEXPS, views.edit_schedule), + 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), url(r'^agenda/%(owner)s/%(schedule_name)s(\.(?P.html))?/?$' % settings.URL_REGEXPS, views.agenda), @@ -34,16 +34,16 @@ type_ietf_only_patterns = [ url(r'^agenda/%(owner)s/%(schedule_name)s/by-room/?$' % settings.URL_REGEXPS, views.agenda_by_room), url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/?$' % settings.URL_REGEXPS, views.agenda_by_type), url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/(?P[a-z]+)$' % settings.URL_REGEXPS, views.agenda_by_type), - url(r'^agenda/%(owner)s/%(schedule_name)s/permissions$' % settings.URL_REGEXPS, ajax.agenda_permission_api), + url(r'^agenda/%(owner)s/%(schedule_name)s/permissions$' % settings.URL_REGEXPS, ajax.schedule_permission_api), url(r'^agenda/%(owner)s/%(schedule_name)s/session/(?P\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.agenda_infourl), + url(r'^agenda/%(owner)s/%(schedule_name)s.json$' % settings.URL_REGEXPS, ajax.schedule_infourl), url(r'^agenda/by-room$', views.agenda_by_room), url(r'^agenda/by-type$', views.agenda_by_type), url(r'^agenda/by-type/(?P[a-z]+)$', views.agenda_by_type), url(r'^agenda/by-type/(?P[a-z]+)/ics$', views.agenda_by_type_ics), - url(r'^agendas/list$', views.list_agendas), - url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_agendas', permanent=True)), + 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'^timeslots/edit$', views.edit_timeslots), url(r'^timeslot/(?P\d+)/edittype$', views.edit_timeslot_type), url(r'^rooms$', ajax.timeslot_roomsurl), @@ -51,8 +51,8 @@ type_ietf_only_patterns = [ url(r'^timeslots$', ajax.timeslot_slotsurl), url(r'^timeslots.json$', ajax.timeslot_slotsurl), url(r'^timeslot/(?P\d+).json$', ajax.timeslot_sloturl), - url(r'^agendas$', ajax.agenda_infosurl), - url(r'^agendas.json$', ajax.agenda_infosurl), + url(r'^agendas$', ajax.schedule_infosurl), + url(r'^agendas.json$', ajax.schedule_infosurl), url(r'^agenda/(?P[-a-z0-9]+)-drafts.pdf$', views.session_draft_pdf), url(r'^agenda/(?P[-a-z0-9]+)-drafts.tgz$', views.session_draft_tarfile), url(r'^sessions.json', ajax.sessions_json), @@ -73,7 +73,7 @@ type_ietf_only_patterns_id_optional = [ url(r'^agenda(?P-utc)?(?P.html)?/?$', views.agenda), url(r'^agenda(?P.txt)$', views.agenda), url(r'^agenda(?P.csv)$', views.agenda), - url(r'^agenda/edit$', views.edit_agenda), + url(r'^agenda/edit$', views.edit_schedule), url(r'^requests$', views.meeting_requests), url(r'^agenda/agenda\.ics$', views.ical_agenda), url(r'^agenda\.ics$', views.ical_agenda), diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 528ad6924..022100488 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -23,7 +23,7 @@ from ietf.secr.proceedings.proc_utils import import_audio_files def group_sessions(sessions): def sort_key(session): - official_sessions = session.timeslotassignments.filter(schedule=session.meeting.agenda) + official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule) if official_sessions: return official_sessions.first().timeslot.time elif session.meeting.date: @@ -77,7 +77,7 @@ def sort_sessions(sessions): # (or the time of the session request if the session isn't scheduled). def time_sort_key(session): - official_sessions = session.timeslotassignments.filter(schedule=session.meeting.agenda) + official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule) if official_sessions: return official_sessions.first().timeslot.time else: diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 26a2d65f6..6b1b6f1ce 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -57,7 +57,7 @@ from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list from ietf.meeting.helpers import get_all_assignments_from_schedule from ietf.meeting.helpers import get_modified_from_assignments from ietf.meeting.helpers import get_wg_list, find_ads_for_meeting -from ietf.meeting.helpers import get_meeting, get_schedule, agenda_permissions, get_ietf_meeting +from ietf.meeting.helpers import get_meeting, get_schedule, schedule_permissions, get_ietf_meeting from ietf.meeting.helpers import preprocess_assignments_for_agenda, read_agenda_file from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request @@ -133,7 +133,7 @@ def materials(request, num=None): schedule = get_schedule(meeting, None) sessions = ( Session.objects .filter(meeting__number=meeting.number, timeslotassignments__schedule=schedule) - .select_related('meeting__agenda','status','group__state','group__parent', ) + .select_related('meeting__schedule','status','group__state','group__parent', ) ) for session in sessions: session.past_cutoff_date = past_cutoff_date @@ -244,7 +244,7 @@ class SaveAsForm(forms.Form): savename = forms.CharField(max_length=16) @role_required('Area Director','Secretariat') -def agenda_create(request, num=None, owner=None, name=None): +def schedule_create(request, num=None, owner=None, name=None): meeting = get_meeting(num) person = get_person_by_email(owner) schedule = get_schedule_by_name(meeting, person, name) @@ -252,29 +252,29 @@ def agenda_create(request, num=None, owner=None, name=None): if schedule is None: # here we have to return some ajax to display an error. messages.error("Error: No meeting information for meeting %s owner %s schedule %s available" % (num, owner, name)) # pylint: disable=no-value-for-parameter - return redirect(edit_agenda, num=num, owner=owner, name=name) + return redirect(edit_schedule, num=num, owner=owner, name=name) # authorization was enforced by the @group_require decorator above. saveasform = SaveAsForm(request.POST) if not saveasform.is_valid(): messages.info(request, "This name is not valid. Please choose another one.") - return redirect(edit_agenda, num=num, owner=owner, name=name) + return redirect(edit_schedule, num=num, owner=owner, name=name) savedname = saveasform.cleaned_data['savename'] if not ascii_alphanumeric(savedname): messages.info(request, "This name contains illegal characters. Please choose another one.") - return redirect(edit_agenda, num=num, owner=owner, name=name) + return redirect(edit_schedule, num=num, owner=owner, name=name) # create the new schedule, and copy the assignments try: sched = meeting.schedule_set.get(name=savedname, owner=request.user.person) if sched: - return redirect(edit_agenda, num=meeting.number, owner=sched.owner_email(), name=sched.name) + return redirect(edit_schedule, num=meeting.number, owner=sched.owner_email(), name=sched.name) else: - messages.info(request, "Agenda creation failed. Please try again.") - return redirect(edit_agenda, num=num, owner=owner, name=name) + messages.info(request, "Schedule creation failed. Please try again.") + return redirect(edit_schedule, num=num, owner=owner, name=name) except Schedule.DoesNotExist: pass @@ -313,7 +313,7 @@ def agenda_create(request, num=None, owner=None, name=None): # now redirect to this new schedule. - return redirect(edit_agenda, meeting.number, newschedule.owner_email(), newschedule.name) + return redirect(edit_schedule, meeting.number, newschedule.owner_email(), newschedule.name) @role_required('Secretariat') @@ -344,16 +344,16 @@ def edit_timeslots(request, num=None): #@role_required('Area Director','Secretariat') # disable the above security for now, check it below. @ensure_csrf_cookie -def edit_agenda(request, num=None, owner=None, name=None): +def edit_schedule(request, num=None, owner=None, name=None): if request.method == 'POST': - return agenda_create(request, num, owner, name) + return schedule_create(request, num, owner, name) user = request.user meeting = get_meeting(num) person = get_person_by_email(owner) if name is None: - schedule = meeting.agenda + schedule = meeting.schedule else: schedule = get_schedule_by_name(meeting, person, name) if schedule is None: @@ -364,13 +364,13 @@ def edit_agenda(request, num=None, owner=None, name=None): rooms = meeting.room_set.filter(session_types__slug='session').distinct().order_by("capacity") saveas = SaveAsForm() - saveasurl=reverse(edit_agenda, + saveasurl=reverse(edit_schedule, args=[meeting.number, schedule.owner_email(), schedule.name]) - can_see, can_edit,secretariat = agenda_permissions(meeting, schedule, user) + can_see, can_edit,secretariat = schedule_permissions(meeting, schedule, user) if not can_see: - return render(request, "meeting/private_agenda.html", + return render(request, "meeting/private_schedule.html", {"schedule":schedule, "meeting": meeting, "meeting_base_url":meeting_base_url, @@ -413,32 +413,32 @@ def edit_agenda(request, num=None, owner=None, name=None): }) ############################################################################## -# show the properties associated with an agenda (visible, public) +# show the properties associated with a schedule (visible, public) # -AgendaPropertiesForm = modelform_factory(Schedule, fields=('name','visible', 'public')) +SchedulePropertiesForm = modelform_factory(Schedule, fields=('name','visible', 'public')) # The meeing urls.py won't allow empy num, owmer, or name values @role_required('Area Director','Secretariat') -def edit_agenda_properties(request, num=None, owner=None, name=None): +def edit_schedule_properties(request, num=None, owner=None, name=None): meeting = get_meeting(num) person = get_person_by_email(owner) schedule = get_schedule_by_name(meeting, person, name) if schedule is None: raise Http404("No meeting information for meeting %s owner %s schedule %s available" % (num, owner, name)) - cansee, canedit, secretariat = agenda_permissions(meeting, schedule, request.user) + cansee, canedit, secretariat = schedule_permissions(meeting, schedule, request.user) if not (canedit or has_role(request.user,'Secretariat')): - return HttpResponseForbidden("You may not edit this agenda") + return HttpResponseForbidden("You may not edit this schedule") else: if request.method == 'POST': - form = AgendaPropertiesForm(instance=schedule,data=request.POST) + form = SchedulePropertiesForm(instance=schedule,data=request.POST) if form.is_valid(): form.save() - return HttpResponseRedirect(reverse('ietf.meeting.views.list_agendas',kwargs={'num': num})) + return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num': num})) else: - form = AgendaPropertiesForm(instance=schedule) + form = SchedulePropertiesForm(instance=schedule) return render(request, "meeting/properties_edit.html", {"schedule":schedule, "form":form, @@ -446,11 +446,11 @@ def edit_agenda_properties(request, num=None, owner=None, name=None): }) ############################################################################## -# show list of agendas. +# show list of schedules. # @role_required('Area Director','Secretariat') -def list_agendas(request, num=None ): +def list_schedules(request, num=None ): meeting = get_meeting(num) user = request.user @@ -463,7 +463,7 @@ def list_agendas(request, num=None ): schedules = sorted(list(schedules),key=lambda x:not x.is_official) - return render(request, "meeting/agenda_list.html", + return render(request, "meeting/schedule_list.html", {"meeting": meeting, "schedules": schedules, }) @@ -484,7 +484,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc="" assert num is None or num.isdigit() meeting = get_ietf_meeting(num) - if not meeting or (meeting.number.isdigit() and int(meeting.number) <= 64 and (not meeting.agenda or not meeting.agenda.assignments.exists())): + if not meeting or (meeting.number.isdigit() and int(meeting.number) <= 64 and (not meeting.schedule or not meeting.schedule.assignments.exists())): if ext == '.html' or (meeting and meeting.number.isdigit() and 0 < int(meeting.number) <= 64): return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s' % num ) else: @@ -946,7 +946,7 @@ def json_agenda(request, num=None ): sessions = [] locations = set() parent_acronyms = set() - assignments = meeting.agenda.assignments.exclude(session__type__in=['lead','offagenda','break','reg']) + assignments = meeting.schedule.assignments.exclude(session__type__in=['lead','offagenda','break','reg']) # Update the assignments with historic information, i.e., valid at the # time of the meeting assignments = preprocess_assignments_for_agenda(assignments, meeting) @@ -1076,7 +1076,7 @@ def get_sessions(num, acronym): sessions = Session.objects.filter(meeting=meeting,short=acronym,type__in=['session','plenary','other']) def sort_key(session): - official_sessions = session.timeslotassignments.filter(schedule=session.meeting.agenda) + official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule) if official_sessions: return official_sessions.first().timeslot.time else: @@ -1094,7 +1094,7 @@ def session_details(request, num, acronym ): for session in sessions: session.type_counter = Counter() - ss = session.timeslotassignments.filter(schedule=meeting.agenda).order_by('timeslot__time') + ss = session.timeslotassignments.filter(schedule=meeting.schedule).order_by('timeslot__time') if ss: session.time = ', '.join(x.timeslot.time.strftime("%A %b-%d-%Y %H%M") for x in ss) if session.status.slug == 'canceled': @@ -1704,9 +1704,9 @@ def make_schedule_official(request, num, owner, name): schedule.public = True schedule.visible = True schedule.save() - meeting.agenda = schedule + meeting.schedule = schedule meeting.save() - return HttpResponseRedirect(reverse('ietf.meeting.views.list_agendas',kwargs={'num':num})) + return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num':num})) if not schedule.public: messages.warning(request,"This schedule will be made public as it is made official.") @@ -1731,15 +1731,15 @@ def delete_schedule(request, num, owner, name): if schedule.name=='Empty-Schedule': return HttpResponseForbidden('You may not delete the default empty schedule') - if schedule == meeting.agenda: - return HttpResponseForbidden('You may not delete the official agenda for %s'%meeting) + if schedule == meeting.schedule: + return HttpResponseForbidden('You may not delete the official schedule for %s'%meeting) if not ( has_role(request.user, 'Secretariat') or person.user == request.user ): return HttpResponseForbidden("You may not delete other user's schedules") if request.method == 'POST': schedule.delete() - return HttpResponseRedirect(reverse('ietf.meeting.views.list_agendas',kwargs={'num':num})) + return HttpResponseRedirect(reverse('ietf.meeting.views.list_schedules',kwargs={'num':num})) return render(request, "meeting/delete_schedule.html", { 'schedule' : schedule, @@ -2143,7 +2143,7 @@ def upcoming_ical(request): assignments = [] for meeting in meetings: - items = meeting.agenda.assignments.order_by( + items = meeting.schedule.assignments.order_by( 'session__type__slug', 'timeslot__time') assignments.extend(items) @@ -2175,7 +2175,7 @@ def upcoming_ical(request): def floor_plan(request, num=None, floor=None, ): meeting = get_meeting(num) - schedule = meeting.agenda + schedule = meeting.schedule floors = FloorPlan.objects.filter(meeting=meeting).order_by('order') if floor: floors = [ f for f in floors if xslugify(f.name) == floor ] @@ -2189,7 +2189,7 @@ def proceedings(request, num=None): meeting = get_meeting(num) - if (meeting.number.isdigit() and int(meeting.number) <= 64) or not meeting.agenda or not meeting.agenda.assignments.exists(): + if (meeting.number.isdigit() and int(meeting.number) <= 64) or not meeting.schedule or not meeting.schedule.assignments.exists(): return HttpResponseRedirect( 'https://www.ietf.org/proceedings/%s' % num ) begin_date = meeting.get_submission_start_date() @@ -2220,7 +2220,7 @@ def finalize_proceedings(request, num=None): meeting = get_meeting(num) - if (meeting.number.isdigit() and int(meeting.number) <= 64) or not meeting.agenda or not meeting.agenda.assignments.exists() or meeting.proceedings_final: + if (meeting.number.isdigit() and int(meeting.number) <= 64) or not meeting.schedule or not meeting.schedule.assignments.exists() or meeting.proceedings_final: raise Http404 if request.method=='POST': @@ -2404,7 +2404,7 @@ def edit_timeslot_type(request, num, slot_id): else: form = TimeSlotTypeForm(instance=timeslot) - sessions = timeslot.sessions.filter(timeslotassignments__schedule=meeting.agenda) + sessions = timeslot.sessions.filter(timeslotassignments__schedule=meeting.schedule) return render(request, 'meeting/edit_timeslot_type.html', {'timeslot':timeslot,'form':form,'sessions':sessions}) @@ -2425,7 +2425,7 @@ def request_minutes(request, num=None): return HttpResponseRedirect(reverse('ietf.meeting.views.materials',kwargs={'num':num})) else: needs_minutes = set() - for a in meeting.agenda.assignments.filter(session__group__type_id__in=('wg','rg','ag')).exclude(session__status='canceled'): + for a in meeting.schedule.assignments.filter(session__group__type_id__in=('wg','rg','ag')).exclude(session__status='canceled'): if not a.session.all_meeting_minutes(): group = a.session.group if group.parent and group.parent.type_id in ('area','irtf'): diff --git a/ietf/secr/meetings/forms.py b/ietf/secr/meetings/forms.py index 80b085c6e..242118df4 100644 --- a/ietf/secr/meetings/forms.py +++ b/ietf/secr/meetings/forms.py @@ -1,3 +1,4 @@ +# Copyright The IETF Trust 2013-2019, All Rights Reserved import re from django import forms @@ -102,7 +103,7 @@ class MeetingModelForm(forms.ModelForm): idsubmit_cutoff_warning_days = ietf.utils.fields.DurationField() class Meta: model = Meeting - exclude = ('type', 'agenda', 'session_request_lock_message') + exclude = ('type', 'schedule', 'session_request_lock_message') def __init__(self,*args,**kwargs): diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index a4e4a0d17..f9f762e2f 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -93,7 +93,7 @@ class SecrMeetingTestCase(TestCase): self.assertEqual(Meeting.objects.count(),count + 1) new_meeting = Meeting.objects.get(number=number) - self.assertTrue(new_meeting.agenda) + self.assertTrue(new_meeting.schedule) self.assertEqual(new_meeting.attendees, None) def test_edit_meeting(self): @@ -151,9 +151,9 @@ class SecrMeetingTestCase(TestCase): meeting = make_meeting_test_data() mars_group = Group.objects.get(acronym='mars') ames_group = Group.objects.get(acronym='ames') - ames_stsa = meeting.agenda.assignments.get(session__group=ames_group) + ames_stsa = meeting.schedule.assignments.get(session__group=ames_group) assert ames_stsa.session.status_id == 'schedw' - mars_stsa = meeting.agenda.assignments.get(session__group=mars_group) + mars_stsa = meeting.schedule.assignments.get(session__group=mars_group) mars_stsa.session.status = SessionStatusName.objects.get(slug='appr') mars_stsa.session.save() url = reverse('ietf.secr.meetings.views.notifications',kwargs={'meeting_id':72}) @@ -169,7 +169,7 @@ class SecrMeetingTestCase(TestCase): person = Person.objects.get(name="(System)") GroupEvent.objects.create(group=mars_group,time=now,type='sent_notification', by=person,desc='sent scheduled notification for %s' % meeting) - ss = meeting.agenda.assignments.get(session__group=ames_group) + ss = meeting.schedule.assignments.get(session__group=ames_group) ss.modified = then ss.save() self.client.login(username="secretary", password="secretary+password") @@ -184,14 +184,14 @@ class SecrMeetingTestCase(TestCase): response = self.client.post(url) self.assertEqual(response.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 1) - ames_stsa = meeting.agenda.assignments.get(session__group=ames_group) + ames_stsa = meeting.schedule.assignments.get(session__group=ames_group) assert ames_stsa.session.status_id == 'sched' - mars_stsa = meeting.agenda.assignments.get(session__group=mars_group) + mars_stsa = meeting.schedule.assignments.get(session__group=mars_group) assert mars_stsa.session.status_id == 'sched' def test_meetings_rooms(self): meeting = make_meeting_test_data() - url = reverse('ietf.secr.meetings.views.rooms',kwargs={'meeting_id':72,'schedule_name':'test-agenda'}) + url = reverse('ietf.secr.meetings.views.rooms',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -200,7 +200,7 @@ class SecrMeetingTestCase(TestCase): # test delete # first unschedule sessions so we can delete - SchedTimeSessAssignment.objects.filter(schedule=meeting.agenda).delete() + SchedTimeSessAssignment.objects.filter(schedule=meeting.schedule).delete() SchedTimeSessAssignment.objects.filter(schedule=meeting.unofficial_schedule).delete() self.client.login(username="secretary", password="secretary+password") post_dict = { @@ -217,7 +217,7 @@ class SecrMeetingTestCase(TestCase): def test_meetings_times(self): make_meeting_test_data() - url = reverse('ietf.secr.meetings.views.times',kwargs={'meeting_id':72,'schedule_name':'test-agenda'}) + url = reverse('ietf.secr.meetings.views.times',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -237,12 +237,12 @@ class SecrMeetingTestCase(TestCase): expected_deletion_count = qs.filter(time=qs.first().time).count() url = reverse('ietf.secr.meetings.views.times_delete',kwargs={ 'meeting_id':meeting.number, - 'schedule_name':meeting.agenda.name, + 'schedule_name':meeting.schedule.name, 'time':qs.first().time.strftime("%Y:%m:%d:%H:%M") }) redirect_url = reverse('ietf.secr.meetings.views.times',kwargs={ 'meeting_id':meeting.number, - 'schedule_name':meeting.agenda.name + 'schedule_name':meeting.schedule.name }) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) @@ -257,7 +257,7 @@ class SecrMeetingTestCase(TestCase): timeslot = TimeSlot.objects.filter(meeting=meeting,type='session').first() url = reverse('ietf.secr.meetings.views.times_edit',kwargs={ 'meeting_id':72, - 'schedule_name':'test-agenda', + 'schedule_name':'test-schedule', 'time':timeslot.time.strftime("%Y:%m:%d:%H:%M") }) self.client.login(username="secretary", password="secretary+password") @@ -272,7 +272,7 @@ class SecrMeetingTestCase(TestCase): def test_meetings_nonsession(self): make_meeting_test_data() - url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-agenda'}) + url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -281,7 +281,7 @@ class SecrMeetingTestCase(TestCase): meeting = make_meeting_test_data() room = meeting.room_set.first() group = Group.objects.get(acronym='secretariat') - url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-agenda'}) + url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) self.client.login(username="secretary", password="secretary+password") response = self.client.post(url, { 'day':'1', @@ -302,7 +302,7 @@ class SecrMeetingTestCase(TestCase): def test_meetings_nonsession_add_invalid(self): make_meeting_test_data() group = Group.objects.get(acronym='secretariat') - url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-agenda'}) + url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) self.client.login(username="secretary", password="secretary+password") response = self.client.post(url, { 'day':'1', @@ -320,8 +320,8 @@ class SecrMeetingTestCase(TestCase): meeting = make_meeting_test_data() session = meeting.session_set.exclude(name='').first() # get first non-session session timeslot = session.official_timeslotassignment().timeslot - url = reverse('ietf.secr.meetings.views.non_session_edit',kwargs={'meeting_id':72,'schedule_name':meeting.agenda.name,'slot_id':timeslot.pk}) - redirect_url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-agenda'}) + url = reverse('ietf.secr.meetings.views.non_session_edit',kwargs={'meeting_id':72,'schedule_name':meeting.schedule.name,'slot_id':timeslot.pk}) + redirect_url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) new_time = timeslot.time + datetime.timedelta(days=1) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) @@ -342,34 +342,34 @@ class SecrMeetingTestCase(TestCase): def test_meetings_non_session_delete(self): meeting = make_meeting_test_data() - slot = meeting.agenda.assignments.filter(timeslot__type='reg').first().timeslot - url = reverse('ietf.secr.meetings.views.non_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.agenda.name,'slot_id':slot.id}) - target = reverse('ietf.secr.meetings.views.non_session', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.agenda.name}) + slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot + url = reverse('ietf.secr.meetings.views.non_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id}) + target = reverse('ietf.secr.meetings.views.non_session', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.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.agenda.assignments.filter(timeslot=slot)) + self.assertFalse(meeting.schedule.assignments.filter(timeslot=slot)) def test_meetings_non_session_cancel(self): meeting = make_meeting_test_data() - slot = meeting.agenda.assignments.filter(timeslot__type='reg').first().timeslot - url = reverse('ietf.secr.meetings.views.non_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.agenda.name,'slot_id':slot.id}) - redirect_url = reverse('ietf.secr.meetings.views.non_session', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.agenda.name}) + slot = meeting.schedule.assignments.filter(timeslot__type='reg').first().timeslot + url = reverse('ietf.secr.meetings.views.non_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id}) + redirect_url = reverse('ietf.secr.meetings.views.non_session', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.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.agenda).first().session + session = slot.sessionassignments.filter(schedule=meeting.schedule).first().session self.assertEqual(session.status_id, 'canceled') def test_meetings_session_edit(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting,group__acronym='mars').first() - url = reverse('ietf.secr.meetings.views.session_edit', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.agenda.name,'session_id':session.id}) - redirect_url = reverse('ietf.secr.meetings.views.sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.agenda.name}) + url = reverse('ietf.secr.meetings.views.session_edit', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'session_id':session.id}) + redirect_url = reverse('ietf.secr.meetings.views.sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 4a5f4d38b..23d646c10 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -40,7 +40,7 @@ def assign(session,timeslot,meeting,schedule=None): Robust function to assign a session to a timeslot. Much simplyfied 2014-03-26. ''' if schedule == None: - schedule = meeting.agenda + schedule = meeting.schedule SchedTimeSessAssignment.objects.create(schedule=schedule, session=session, timeslot=timeslot) @@ -122,7 +122,7 @@ def is_combined(session,meeting,schedule=None): Check to see if this session is using two combined timeslots ''' if schedule == None: - schedule = meeting.agenda + schedule = meeting.schedule if session.timeslotassignments.filter(schedule=schedule).count() > 1: return True else: @@ -230,7 +230,7 @@ def add(request): owner = Person.objects.get(name='(System)'), visible = True, public = True) - meeting.agenda = schedule + meeting.schedule = schedule # we want to carry session request lock status over from previous meeting previous_meeting = get_meeting( int(meeting.number) - 1 ) @@ -300,7 +300,7 @@ def blue_sheet_generate(request, meeting_id): # TODO: Why aren't 'ag' in here as well? groups = Group.objects.filter( type__in=['wg','rg'], - session__timeslotassignments__schedule=meeting.agenda).order_by('acronym') + session__timeslotassignments__schedule=meeting.schedule).order_by('acronym') create_blue_sheets(meeting, groups) messages.success(request, 'Blue Sheets generated') @@ -560,7 +560,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.agenda.assignments.filter(timeslot__type='session'): + for ss in meeting.schedule.assignments.filter(timeslot__type='session'): last_notice = ss.session.group.latest_event(type='sent_notification') if last_notice and ss.modified > last_notice.time: groups.add(ss.session.group) @@ -569,7 +569,7 @@ def notifications(request, meeting_id): if request.method == "POST": # ensure session state is scheduled - for ss in meeting.agenda.assignments.all(): + for ss in meeting.schedule.assignments.all(): session = ss.session if session.status.slug in ["schedw", "appr"]: session.status_id = "sched" @@ -848,7 +848,7 @@ def view(request, meeting_id): ''' meeting = get_object_or_404(Meeting, number=meeting_id) - + return render(request, 'meetings/view.html', { 'meeting': meeting}, ) diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 14863eb4b..14c3efef5 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -34,7 +34,7 @@ VIDEO_TITLE_RE = re.compile(r'IETF(?P[\d]+)-(?P.*)-(?P\d{8}) def _get_session(number,name,date,time): '''Lookup session using data from video title''' meeting = Meeting.objects.get(number=number) - schedule = meeting.agenda + schedule = meeting.schedule timeslot_time = datetime.datetime.strptime(date + time,'%Y%m%d%H%M') try: assignment = SchedTimeSessAssignment.objects.get( @@ -73,7 +73,7 @@ def import_audio_files(meeting): timeslot = get_timeslot_for_filename(filename) if timeslot: sessionassignments = timeslot.sessionassignments.filter( - schedule=timeslot.meeting.agenda, + schedule=timeslot.meeting.schedule, session__status='sched', ).exclude(session__agenda_note__icontains='canceled').order_by('timeslot__time') if not sessionassignments: @@ -103,7 +103,7 @@ def get_timeslot_for_filename(filename): meeting=meeting, location__name=room_mapping[match.groupdict()['room']], time=time, - sessionassignments__schedule=meeting.agenda, + sessionassignments__schedule=meeting.schedule, ).exclude(sessions__status_id='canceled').distinct() return slots.get() except (ObjectDoesNotExist, KeyError): diff --git a/ietf/secr/proceedings/tests.py b/ietf/secr/proceedings/tests.py index f4d21ee6b..432cf46cb 100644 --- a/ietf/secr/proceedings/tests.py +++ b/ietf/secr/proceedings/tests.py @@ -134,7 +134,7 @@ class RecordingTestCase(TestCase): ames_session.status = scheduled ames_session.save() timeslot = mars_session.official_timeslotassignment().timeslot - SchedTimeSessAssignment.objects.create(timeslot=timeslot,session=ames_session,schedule=meeting.agenda) + SchedTimeSessAssignment.objects.create(timeslot=timeslot,session=ames_session,schedule=meeting.schedule) self.create_audio_file_for_timeslot(timeslot) import_audio_files(meeting) doc = mars_session.materials.filter(type='recording').first() diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py index 4180dd3d1..ceb1f28c4 100644 --- a/ietf/secr/proceedings/views.py +++ b/ietf/secr/proceedings/views.py @@ -232,7 +232,7 @@ def recording(request, meeting_num): session. ''' meeting = get_object_or_404(Meeting, number=meeting_num) - assignments = meeting.agenda.assignments.exclude(session__type__in=('reg','break')).order_by('session__group__acronym') + assignments = meeting.schedule.assignments.exclude(session__type__in=('reg','break')).order_by('session__group__acronym') sessions = [ x.session for x in assignments ] if request.method == 'POST': diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index 04a2ec4ba..8ddd68ee5 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -327,7 +327,7 @@ def edit(request, *args, **kwargs): def session_save(session): session.save() - if session.status_id == "schedw" and session.meeting.agenda != None: + if session.status_id == "schedw" and session.meeting.schedule != None: # send an email to iesg-secretariat to alert to change pass diff --git a/ietf/secr/templates/meetings/view.html b/ietf/secr/templates/meetings/view.html index 2c8c2c085..3564c3782 100644 --- a/ietf/secr/templates/meetings/view.html +++ b/ietf/secr/templates/meetings/view.html @@ -37,18 +37,18 @@
  • -
  • +
  • -
  • +
  • diff --git a/ietf/secr/templates/proceedings/interim_directory.html b/ietf/secr/templates/proceedings/interim_directory.html index d20b1e8af..57b5dba69 100644 --- a/ietf/secr/templates/proceedings/interim_directory.html +++ b/ietf/secr/templates/proceedings/interim_directory.html @@ -14,8 +14,8 @@ {{ meeting.date }} {{ meeting.group.acronym }} - {% if meeting.agenda %} - Agenda + {% if meeting.schedule %} + Agenda {% else %} Agenda {% endif %} diff --git a/ietf/secr/utils/meeting.py b/ietf/secr/utils/meeting.py index d6d2a67b4..d3e41831e 100644 --- a/ietf/secr/utils/meeting.py +++ b/ietf/secr/utils/meeting.py @@ -1,3 +1,5 @@ +# Copyright The IETF Trust 2013-2019, All Rights Reserved + import os from django.conf import settings @@ -49,7 +51,7 @@ def get_session(timeslot, schedule=None): ''' # todo, doesn't account for shared timeslot if not schedule: - schedule = timeslot.meeting.agenda + schedule = timeslot.meeting.schedule qs = timeslot.sessions.filter(timeslotassignments__schedule=schedule) #.exclude(states__slug='deleted') if qs: return qs[0] @@ -63,7 +65,7 @@ def get_timeslot(session, schedule=None): schedule to use the meeting "official" schedule. ''' if not schedule: - schedule = session.meeting.agenda + schedule = session.meeting.schedule ss = session.timeslotassignments.filter(schedule=schedule) if ss: return ss[0].timeslot diff --git a/ietf/templates/meeting/agenda_by_type.html b/ietf/templates/meeting/agenda_by_type.html index 6e964e20f..f4fea82e5 100644 --- a/ietf/templates/meeting/agenda_by_type.html +++ b/ietf/templates/meeting/agenda_by_type.html @@ -33,7 +33,7 @@ li.daylistentry { margin-left:2em; font-weight: 400; }
      {% for type in type_list %}
    • -

      {{type.grouper|title}}

      {% if schedule == meeting.agenda %}Download to Calendar{% endif %} +

      {{type.grouper|title}}

      {% if schedule == meeting.schedule %}Download to Calendar{% endif %}
        {% regroup type.list by timeslot.time|date:"l Y-M-d" as daylist %} {% for day in daylist %} diff --git a/ietf/templates/meeting/delete_schedule.html b/ietf/templates/meeting/delete_schedule.html index fe51a0a35..76254bcae 100644 --- a/ietf/templates/meeting/delete_schedule.html +++ b/ietf/templates/meeting/delete_schedule.html @@ -19,7 +19,7 @@ {# Null Form #} {% buttons %} - Cancel + Cancel {% endbuttons %} diff --git a/ietf/templates/meeting/interim_announcement.txt b/ietf/templates/meeting/interim_announcement.txt index cbe42dd30..28d11ea2e 100644 --- a/ietf/templates/meeting/interim_announcement.txt +++ b/ietf/templates/meeting/interim_announcement.txt @@ -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 == "rg" %}Research Group{% elif group.state.slug == "active" %}Working Group{% elif group.state.slug == 'bof' %}BOF{% endif %} will hold -{% if meeting.session_set.count == 1 %}a{% if meeting.city %}n {% else %} virtual {% endif %}interim meeting on {{ meeting.date }} from {{ meeting.agenda.assignments.first.timeslot.time | date:"H:i" }} to {{ meeting.agenda.assignments.first.timeslot.end_time | date:"H:i" }} {{ meeting.time_zone }}. +{% 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 }}. {% else %}a multi-day {% if not meeting.city %}virtual {% endif %}interim meeting. -{% for assignment in meeting.agenda.assignments.all %}Session {{ forloop.counter }}: +{% for assignment in meeting.schedule.assignments.all %}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 }} {% endfor %}{% endif %} {% if meeting.city %}Meeting Location: diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html index 0aaafa36a..8e8ed82f9 100644 --- a/ietf/templates/meeting/interim_request_details.html +++ b/ietf/templates/meeting/interim_request_details.html @@ -26,7 +26,7 @@
        {{ meeting.country }}
        Timezone
        {{ meeting.time_zone }}
        - {% for assignment in meeting.agenda.assignments.all %} + {% for assignment in meeting.schedule.assignments.all %}
        Date
        {{ assignment.timeslot.time|date:"Y-m-d" }} diff --git a/ietf/templates/meeting/landscape_edit.html b/ietf/templates/meeting/landscape_edit.html index 41d30755d..0c61fea86 100644 --- a/ietf/templates/meeting/landscape_edit.html +++ b/ietf/templates/meeting/landscape_edit.html @@ -117,7 +117,7 @@ promiselist.push(ss_promise);
        diff --git a/ietf/templates/meeting/make_schedule_official.html b/ietf/templates/meeting/make_schedule_official.html index 9c4f73c79..c762e7307 100644 --- a/ietf/templates/meeting/make_schedule_official.html +++ b/ietf/templates/meeting/make_schedule_official.html @@ -20,7 +20,7 @@ {# Null Form #} {% buttons %} - Cancel + Cancel {% endbuttons %} diff --git a/ietf/templates/meeting/meeting_heading.html b/ietf/templates/meeting/meeting_heading.html index 0d0aab7ec..133415f12 100644 --- a/ietf/templates/meeting/meeting_heading.html +++ b/ietf/templates/meeting/meeting_heading.html @@ -5,7 +5,7 @@ {% origin %} - {% if schedule != meeting.agenda %} + {% if schedule != meeting.schedule %}

        This is schedule {{schedule.owner.email}}/{{ schedule.name }}, not the official schedule.

        @@ -34,7 +34,7 @@
      • UTC Agenda
      • {% if user|has_role:"Secretariat,Area Director,IAB" %} - {% if schedule != meeting.agenda %} + {% if schedule != meeting.schedule %}
      • by Room
      • diff --git a/ietf/templates/meeting/private_agenda.html b/ietf/templates/meeting/private_schedule.html similarity index 87% rename from ietf/templates/meeting/private_agenda.html rename to ietf/templates/meeting/private_schedule.html index bf7a4f165..de5362546 100644 --- a/ietf/templates/meeting/private_agenda.html +++ b/ietf/templates/meeting/private_schedule.html @@ -15,7 +15,7 @@

        You do not have access this agenda. It belongs to {{ schedule.owner }}.

        -

        List your meetings.

        +

        List your meetings.

        diff --git a/ietf/templates/meeting/properties_edit.html b/ietf/templates/meeting/properties_edit.html index 7bd9865a9..19ca01aa7 100644 --- a/ietf/templates/meeting/properties_edit.html +++ b/ietf/templates/meeting/properties_edit.html @@ -25,7 +25,7 @@ {% bootstrap_form form %} {% buttons %} - Cancel + Cancel {% endbuttons %} diff --git a/ietf/templates/meeting/agenda_list.html b/ietf/templates/meeting/schedule_list.html similarity index 73% rename from ietf/templates/meeting/agenda_list.html rename to ietf/templates/meeting/schedule_list.html index 41445b306..3ea142ea2 100644 --- a/ietf/templates/meeting/agenda_list.html +++ b/ietf/templates/meeting/schedule_list.html @@ -30,14 +30,14 @@ Public - {% for agenda in class.list %} + {% for schedule in class.list %} - - {{ agenda.name }} - {{ agenda.owner }} - {{ agenda.visible_token }} - {{ agenda.public_token }} - + + {{ schedule.name }} + {{ schedule.owner }} + {{ schedule.visible_token }} + {{ schedule.public_token }} + EDIT {% endfor %} From 3957743b8508830f9b294248b13bf7d2eaefac35 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 5 Dec 2019 12:41:09 +0000 Subject: [PATCH 2/9] Move Session.status, .requested, and .requested_by to a new SchedulingEvent - Legacy-Id: 17122 --- ietf/api/views.py | 2 +- ietf/doc/tests.py | 11 +- ietf/doc/tests_material.py | 11 +- ietf/doc/views_doc.py | 12 +- ietf/group/models.py | 2 +- ietf/group/views.py | 13 +- ietf/mailtrigger/models.py | 5 +- ietf/meeting/admin.py | 45 ++- ietf/meeting/ajax.py | 10 +- ietf/meeting/factories.py | 26 +- ietf/meeting/forms.py | 11 +- ietf/meeting/helpers.py | 47 ++- .../migrations/0022_schedulingevent.py | 31 ++ .../0023_create_scheduling_events.py | 67 ++++ .../migrations/0024_auto_20191204_1731.py | 28 ++ ietf/meeting/models.py | 102 +++--- ietf/meeting/resources.py | 26 +- ietf/meeting/test_data.py | 30 +- ietf/meeting/tests_views.py | 78 +++-- ietf/meeting/utils.py | 175 ++++++++-- ietf/meeting/views.py | 302 +++++++++++------- ietf/secr/meetings/tests.py | 13 +- ietf/secr/meetings/views.py | 115 ++++--- ietf/secr/proceedings/forms.py | 7 +- ietf/secr/proceedings/proc_utils.py | 30 +- ietf/secr/proceedings/tests.py | 17 +- ietf/secr/proceedings/views.py | 4 +- ietf/secr/sreq/tests.py | 20 +- ietf/secr/sreq/urls.py | 4 +- ietf/secr/sreq/views.py | 208 +++++++----- ietf/secr/templates/meetings/non_session.html | 2 +- .../secr/templates/meetings/session_edit.html | 8 +- ietf/secr/templates/meetings/sessions.html | 2 +- ietf/secr/templates/sreq/view.html | 2 +- ietf/secr/utils/group.py | 35 +- ietf/settings.py | 2 +- .../doc/material/presentations-row.html | 4 +- ietf/templates/meeting/agenda.html | 4 +- ietf/templates/meeting/agenda.txt | 2 +- ietf/templates/meeting/interim_announce.html | 12 +- ietf/templates/meeting/interim_pending.html | 80 +++-- .../meeting/interim_request_cancel.html | 4 +- .../meeting/interim_request_details.html | 18 +- ietf/templates/meeting/past.html | 58 ++-- ietf/templates/meeting/proceedings.html | 106 +++--- ietf/templates/meeting/requests.html | 6 +- .../meeting/session_details_panel.html | 4 +- ietf/templates/meeting/upcoming.html | 16 +- 48 files changed, 1143 insertions(+), 674 deletions(-) create mode 100644 ietf/meeting/migrations/0022_schedulingevent.py create mode 100644 ietf/meeting/migrations/0023_create_scheduling_events.py create mode 100644 ietf/meeting/migrations/0024_auto_20191204_1731.py diff --git a/ietf/api/views.py b/ietf/api/views.py index 2376d23e4..c25a25f56 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -69,7 +69,7 @@ class PersonalInformationExportView(DetailView, JsonExportMixin): person = get_object_or_404(self.model, user=request.user) expand = ['searchrule', 'documentauthor', 'ad_document_set', 'ad_dochistory_set', 'docevent', 'ballotpositiondocevent', 'deletedevent', 'email_set', 'groupevent', 'role', 'rolehistory', 'iprdisclosurebase', - 'iprevent', 'liaisonstatementevent', 'whitelisted', 'schedule', 'constraint', 'session', 'message', + 'iprevent', 'liaisonstatementevent', 'whitelisted', 'schedule', 'constraint', 'schedulingevent', 'message', 'sendqueue', 'nominee', 'topicfeedbacklastseen', 'alias', 'email', 'apikeys', 'personevent', 'reviewersettings', 'reviewsecretarysettings', 'unavailableperiod', 'reviewwish', 'nextreviewerinteam', 'reviewrequest', 'meetingregistration', 'submissionevent', 'preapproval', diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 63d986e59..0f5a29736 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -39,7 +39,7 @@ from ietf.doc.utils import create_ballot_if_not_open from ietf.group.models import Group from ietf.group.factories import GroupFactory, RoleFactory from ietf.ipr.factories import HolderIprDisclosureFactory -from ietf.meeting.models import Meeting, Session, SessionPresentation +from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.name.models import SessionStatusName, BallotPositionName from ietf.person.models import Person @@ -712,11 +712,14 @@ class DocTestCase(TestCase): name = "session-72-mars-1", meeting = Meeting.objects.get(number='72'), group = Group.objects.get(acronym='mars'), - status = SessionStatusName.objects.create(slug='scheduled', name='Scheduled'), modified = datetime.datetime.now(), - requested_by = Person.objects.get(user__username="marschairman"), type_id = "session", - ) + ) + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.create(slug='scheduled'), + by = Person.objects.get(user__username="marschairman"), + ) SessionPresentation.objects.create(session=session, document=doc, rev=doc.rev) r = self.client.get(urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=doc.name))) diff --git a/ietf/doc/tests_material.py b/ietf/doc/tests_material.py index 47c71f90d..c67525cdd 100644 --- a/ietf/doc/tests_material.py +++ b/ietf/doc/tests_material.py @@ -20,7 +20,7 @@ from ietf.doc.models import Document, State, DocAlias, NewRevisionDocEvent from ietf.group.factories import RoleFactory from ietf.group.models import Group from ietf.meeting.factories import MeetingFactory -from ietf.meeting.models import Meeting, Session, SessionPresentation +from ietf.meeting.models import Meeting, Session, SessionPresentation, SchedulingEvent from ietf.name.models import SessionStatusName from ietf.person.models import Person from ietf.utils.test_utils import TestCase, login_testing_unauthorized @@ -158,11 +158,14 @@ class GroupMaterialTests(TestCase): name = "session-42-mars-1", meeting = Meeting.objects.get(number='42'), group = Group.objects.get(acronym='mars'), - status = SessionStatusName.objects.create(slug='scheduled', name='Scheduled'), modified = datetime.datetime.now(), - requested_by = Person.objects.get(user__username="marschairman"), type_id="session", - ) + ) + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.create(slug='scheduled'), + by = Person.objects.get(user__username="marschairman"), + ) SessionPresentation.objects.create(session=session, document=doc, rev=doc.rev) url = urlreverse('ietf.doc.views_material.edit_material', kwargs=dict(name=doc.name, action="revise")) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index 603c1650a..f14b963c0 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -68,13 +68,13 @@ from ietf.group.models import Role, Group from ietf.group.utils import can_manage_group_type, can_manage_materials, group_features_role_filter from ietf.ietfauth.utils import ( has_role, is_authorized_in_doc_stream, user_is_person, role_required, is_individual_draft_author) -from ietf.name.models import StreamName, BallotPositionName +from ietf.name.models import StreamName, BallotPositionName, SessionStatusName from ietf.utils.history import find_history_active_at from ietf.doc.forms import TelechatForm, NotifyForm from ietf.doc.mails import email_comment from ietf.mailtrigger.utils import gather_relevant_expansions from ietf.meeting.models import Session -from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions +from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions, add_event_info_to_session_qs from ietf.review.models import ReviewAssignment from ietf.review.utils import can_request_review_of_doc, review_assignments_to_list_for_docs from ietf.review.utils import no_review_from_teams_on_doc @@ -1345,9 +1345,13 @@ def add_sessionpresentation(request,name): def all_presentations(request, name): doc = get_object_or_404(Document, name=name) + sessions = add_event_info_to_session_qs( + doc.session_set.filter(type__in=['session','plenary','other']) + ).filter(current_status__in=['sched','schedw','appr','canceled']) - sessions = doc.session_set.filter(status__in=['sched','schedw','appr','canceled'], - type__in=['session','plenary','other']) + status_names = {n.slug: n.name for n in SessionStatusName.objects.all()} + for session in sessions: + session.current_status_name = status_names.get(session.current_status, session.current_status) future, in_progress, past = group_sessions(sessions) diff --git a/ietf/group/models.py b/ietf/group/models.py index aecbf3287..ceb60f23b 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -70,7 +70,7 @@ class GroupInfo(models.Model): return list(set([ role for role in self.parent.role_set.filter(name__in=['ad', 'chair']) ])) def is_bof(self): - return (self.state.slug in ["bof", "bof-conc"]) + return self.state_id in ["bof", "bof-conc"] class Meta: abstract = True diff --git a/ietf/group/views.py b/ietf/group/views.py index e757709f1..a93d71930 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -88,7 +88,7 @@ from ietf.group.utils import (get_charter_text, can_manage_group_type, from ietf.ietfauth.utils import has_role, is_authorized_in_group from ietf.mailtrigger.utils import gather_relevant_expansions from ietf.meeting.helpers import get_meeting -from ietf.meeting.utils import group_sessions +from ietf.meeting.utils import group_sessions, add_event_info_to_session_qs from ietf.name.models import GroupTypeName, StreamName from ietf.person.models import Email from ietf.review.models import ReviewRequest, ReviewAssignment, ReviewerSettings, ReviewSecretarySettings @@ -750,9 +750,14 @@ def meetings(request, acronym=None, group_type=None): four_years_ago = datetime.datetime.now()-datetime.timedelta(days=4*365) - sessions = group.session_set.filter(status__in=['sched','schedw','appr','canceled'], - meeting__date__gt=four_years_ago, - type__in=['session','plenary','other']) + sessions = add_event_info_to_session_qs( + group.session_set.filter( + meeting__date__gt=four_years_ago, + type__in=['session','plenary','other'] + ) + ).filter( + current_status__in=['sched','schedw','appr','canceled'], + ) future, in_progress, past = group_sessions(sessions) diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index e55324ac0..a7a8e13c8 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -355,7 +355,10 @@ class Recipient(models.Model): addrs=[] if 'session' in kwargs: session = kwargs['session'] - addrs.append(session.requested_by.role_email('chair').address) + from ietf.meeting.models import SchedulingEvent + first_event = SchedulingEvent.objects.filter(session=session).select_related('by').order_by('time', 'id').first() + if first_event and first_event.status_id in ['appw', 'schedw']: + addrs.append(first_event.by.role_email('chair').address) return addrs def gather_review_team_ads(self, **kwargs): diff --git a/ietf/meeting/admin.py b/ietf/meeting/admin.py index 0cac9d327..fcc3c5966 100644 --- a/ietf/meeting/admin.py +++ b/ietf/meeting/admin.py @@ -8,7 +8,7 @@ from django.contrib import admin from ietf.meeting.models import (Meeting, Room, Session, TimeSlot, Constraint, Schedule, SchedTimeSessAssignment, ResourceAssociation, FloorPlan, UrlResource, - SessionPresentation, ImportantDate, SlideSubmission, ) + SessionPresentation, ImportantDate, SlideSubmission, SchedulingEvent) class UrlResourceAdmin(admin.ModelAdmin): @@ -80,13 +80,37 @@ class ConstraintAdmin(admin.ModelAdmin): admin.site.register(Constraint, ConstraintAdmin) -class SessionAdmin(admin.ModelAdmin): - list_display = ["meeting", "name", "group", "attendees", "requested", "status"] - list_filter = ["meeting", ] - raw_id_fields = ["meeting", "group", "requested_by", "materials"] - search_fields = ["meeting__number", "name", "group__name", "group__acronym", ] - ordering = ["-requested"] +class SchedulingEventInline(admin.TabularInline): + model = SchedulingEvent + raw_id_fields = ["by"] +class SessionAdmin(admin.ModelAdmin): + list_display = ["meeting", "name", "group", "attendees", "requested", "current_status"] + list_filter = ["meeting", ] + raw_id_fields = ["meeting", "group", "materials"] + search_fields = ["meeting__number", "name", "group__name", "group__acronym", ] + ordering = ["-id"] + inlines = [SchedulingEventInline] + + + def get_queryset(self, request): + qs = super(SessionAdmin, self).get_queryset(request) + return qs.prefetch_related('schedulingevent_set') + + def current_status(self, instance): + events = sorted(instance.schedulingevent_set.all(), key=lambda e: (e.time, e.id)) + if events: + return events[-1].time + else: + return None + + def requested(self, instance): + events = sorted(instance.schedulingevent_set.all(), key=lambda e: (e.time, e.id)) + if events: + return events[0].time + else: + return None + def name_lower(self, instance): return instance.name.name.lower() @@ -94,6 +118,13 @@ class SessionAdmin(admin.ModelAdmin): admin.site.register(Session, SessionAdmin) +class SchedulingEventAdmin(admin.ModelAdmin): + list_display = ["session", "status", "time", "by"] + raw_id_fields = ["session", "by"] + ordering = ["-id"] + +admin.site.register(SchedulingEvent, SchedulingEventAdmin) + class ScheduleAdmin(admin.ModelAdmin): list_display = ["name", "meeting", "owner", "visible", "public", "badness"] list_filter = ["meeting", ] diff --git a/ietf/meeting/ajax.py b/ietf/meeting/ajax.py index a7aae7acb..1ad694423 100644 --- a/ietf/meeting/ajax.py +++ b/ietf/meeting/ajax.py @@ -10,7 +10,9 @@ from django.views.decorators.http import require_POST from ietf.ietfauth.utils import role_required, has_role from ietf.meeting.helpers import get_meeting, get_schedule, schedule_permissions, get_person_by_email, get_schedule_by_name from ietf.meeting.models import TimeSlot, Session, Schedule, Room, Constraint, SchedTimeSessAssignment, ResourceAssociation -from ietf.meeting.views import edit_timeslots, edit_schedule +from ietf.meeting.views import edit_timeslots, edit_schedule +from ietf.meeting.utils import only_sessions_that_can_meet +from ietf.meeting.utils import add_event_info_to_session_qs import debug # pyflakes:ignore @@ -431,7 +433,11 @@ def session_json(request, num, sessionid): def sessions_json(request, num): meeting = get_meeting(num) - sessions = meeting.sessions_that_can_meet.all() + sessions = add_event_info_to_session_qs( + only_sessions_that_can_meet(meeting.session_set), + requested_time=True, + requested_by=True, + ) sess1_dict = [ x.json_dict(request.build_absolute_uri('/')) for x in sessions ] return HttpResponse(json.dumps(sess1_dict, sort_keys=True, indent=2), diff --git a/ietf/meeting/factories.py b/ietf/meeting/factories.py index 0da2c2320..e72303151 100644 --- a/ietf/meeting/factories.py +++ b/ietf/meeting/factories.py @@ -10,7 +10,8 @@ import datetime from django.core.files.base import ContentFile -from ietf.meeting.models import Meeting, Session, Schedule, TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission +from ietf.meeting.models import Meeting, Session, SchedulingEvent, Schedule, TimeSlot, SessionPresentation, FloorPlan, Room, SlideSubmission +from ietf.name.models import SessionStatusName from ietf.group.factories import GroupFactory from ietf.person.factories import PersonFactory @@ -83,9 +84,28 @@ class SessionFactory(factory.DjangoModelFactory): meeting = factory.SubFactory(MeetingFactory) type_id='session' group = factory.SubFactory(GroupFactory) - requested_by = factory.SubFactory(PersonFactory) - status_id='sched' + @factory.post_generation + def status_id(obj, create, extracted, **kwargs): + if create: + if not extracted: + extracted = 'sched' + + if extracted not in ['apprw', 'schedw']: + # requested event + SchedulingEvent.objects.create( + session=obj, + status=SessionStatusName.objects.get(slug='schedw'), + by=PersonFactory(), + ) + + # actual state event + SchedulingEvent.objects.create( + session=obj, + status=SessionStatusName.objects.get(slug=extracted), + by=PersonFactory(), + ) + @factory.post_generation def add_to_schedule(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument ''' diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index a79eb29b7..c30e72560 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -21,7 +21,7 @@ from ietf.group.models import Group from ietf.ietfauth.utils import has_role from ietf.meeting.models import Session, Meeting, Schedule, countries, timezones from ietf.meeting.helpers import get_next_interim_number, make_materials_directories -from ietf.meeting.helpers import is_meeting_approved, get_next_agenda_name +from ietf.meeting.helpers import is_interim_meeting_approved, get_next_agenda_name from ietf.message.models import Message from ietf.person.models import Person from ietf.utils.fields import DatepickerDateField, DurationField, MultiEmailField @@ -132,7 +132,7 @@ class InterimMeetingModelForm(forms.ModelForm): self.fields['group'].widget.attrs['disabled'] = True if self.instance.city or self.instance.country: self.fields['in_person'].initial = True - if is_meeting_approved(self.instance): + if is_interim_meeting_approved(self.instance): self.fields['approved'].initial = True else: self.fields['approved'].initial = False @@ -244,15 +244,8 @@ class InterimSessionModelForm(forms.ModelForm): """NOTE: as the baseform of an inlineformset self.save(commit=True) never gets called""" session = super(InterimSessionModelForm, self).save(commit=kwargs.get('commit', True)) - if self.is_approved_or_virtual: - session.status_id = 'scheda' - else: - session.status_id = 'apprw' session.group = self.group session.type_id = 'session' - if not self.instance.pk: - session.requested_by = self.user.person - return session def save_agenda(self): diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 0c15b4446..0206ac355 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -27,7 +27,8 @@ from ietf.ietfauth.utils import has_role, user_is_person from ietf.liaisons.utils import get_person_for_user from ietf.mailtrigger.utils import gather_address_lists from ietf.person.models import Person -from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment, ImportantDate +from ietf.meeting.models import Meeting, Schedule, TimeSlot, SchedTimeSessAssignment, ImportantDate, SchedulingEvent +from ietf.meeting.utils import session_requested_by from ietf.name.models import ImportantDateName from ietf.utils.history import find_history_active_at, find_history_replacements_active_at from ietf.utils.mail import send_mail @@ -205,6 +206,11 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting): 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) + # add current session status + sessions = {a.session_id: a.session for a in assignments if a.session} + for e in SchedulingEvent.objects.filter(session__in=sessions.keys()).order_by('time', 'id').iterator(): + sessions[e.session_id].current_status = e.status_id + return assignments def read_session_file(type, num, doc): @@ -435,12 +441,9 @@ def get_earliest_session_date(formset): return sorted([f.cleaned_data['date'] for f in formset.forms if f.cleaned_data.get('date')])[0] -def is_meeting_approved(meeting): - """Returns True if the meeting is approved""" - if meeting.session_set.first().status.slug == 'apprw': - return False - else: - return True +def is_interim_meeting_approved(meeting): + from ietf.meeting.utils import add_event_info_to_session_qs + return add_event_info_to_session_qs(meeting.session_set.all()).first().current_status == 'apprw' def get_next_interim_number(acronym,date): ''' @@ -493,8 +496,9 @@ def send_interim_approval_request(meetings): """Sends an email to the secretariat, group chairs, and responsible area director or the IRTF chair noting that approval has been requested for a new interim meeting. Takes a list of one or more meetings.""" - group = meetings[0].session_set.first().group - requester = meetings[0].session_set.first().requested_by + first_session = meetings[0].session_set.first() + group = first_session.group + requester = session_requested_by(first_session) (to_email, cc_list) = gather_address_lists('session_requested',group=group,person=requester) from_email = (settings.SESSION_REQUEST_FROM_EMAIL) subject = '{group} - New Interim Meeting Request'.format(group=group.acronym) @@ -524,8 +528,9 @@ def send_interim_approval_request(meetings): def send_interim_announcement_request(meeting): """Sends an email to the secretariat that an interim meeting is ready for announcement, includes the link to send the official announcement""" - group = meeting.session_set.first().group - requester = meeting.session_set.first().requested_by + first_session = meeting.session_set.first() + group = first_session.group + requester = session_requested_by(first_session) (to_email, cc_list) = gather_address_lists('interim_approved') from_email = (settings.SESSION_REQUEST_FROM_EMAIL) subject = '{group} - interim meeting ready for announcement'.format(group=group.acronym) @@ -553,10 +558,8 @@ def send_interim_cancellation_notice(meeting): date=meeting.date.strftime('%Y-%m-%d')) start_time = session.official_timeslotassignment().timeslot.time end_time = start_time + session.requested_duration - if meeting.session_set.filter(status='sched').count() > 1: - is_multi_day = True - else: - is_multi_day = False + from ietf.meeting.utils import add_event_info_to_session_qs + is_multi_day = add_event_info_to_session_qs(meeting.session_set.all()).filter(current_status='sched').count() > 1 template = 'meeting/interim_cancellation_notice.txt' context = locals() send_mail(None, @@ -590,12 +593,24 @@ def send_interim_minutes_reminder(meeting): cc=cc_list) -def sessions_post_save(forms): +def sessions_post_save(request, forms): """Helper function to perform various post save operations on each form of a InterimSessionModelForm formset""" for form in forms: if not form.has_changed(): continue + + if form.instance.pk is not None and not SchedulingEvent.objects.filter(session=form.instance).exists(): + if form.is_approved_or_virtual: + status_id = 'scheda' + else: + status_id = 'apprw' + SchedulingEvent.objects.create( + session=form.instance, + status_id=status_id, + by=request.user.person, + ) + if ('date' in form.changed_data) or ('time' in form.changed_data): update_interim_session_assignment(form) if 'agenda' in form.changed_data: diff --git a/ietf/meeting/migrations/0022_schedulingevent.py b/ietf/meeting/migrations/0022_schedulingevent.py new file mode 100644 index 000000000..4ea0091ac --- /dev/null +++ b/ietf/meeting/migrations/0022_schedulingevent.py @@ -0,0 +1,31 @@ +# Copyright The IETF Trust 2019, All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2019-11-19 02:41 +from __future__ import unicode_literals + +import datetime +from django.db import migrations, models +import django.db.models.deletion +import ietf.utils.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('person', '0009_auto_20190118_0725'), + ('name', '0007_fix_m2m_slug_id_length'), + ('meeting', '0021_rename_meeting_agenda_to_schedule'), + ] + + operations = [ + migrations.CreateModel( + name='SchedulingEvent', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('time', models.DateTimeField(default=datetime.datetime.now, help_text='When the event happened')), + ('by', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')), + ('session', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='meeting.Session')), + ('status', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.SessionStatusName')), + ], + ), + ] diff --git a/ietf/meeting/migrations/0023_create_scheduling_events.py b/ietf/meeting/migrations/0023_create_scheduling_events.py new file mode 100644 index 000000000..d1d07d149 --- /dev/null +++ b/ietf/meeting/migrations/0023_create_scheduling_events.py @@ -0,0 +1,67 @@ +# Copyright The IETF Trust 2019, All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2019-11-19 02:42 +from __future__ import unicode_literals + +from django.db import migrations + +import datetime + +def create_scheduling_events(apps, schema_editor): + Session = apps.get_model('meeting', 'Session') + SchedulingEvent = apps.get_model('meeting', 'SchedulingEvent') + Person = apps.get_model('person', 'Person') + SessionStatusName = apps.get_model('name', 'SessionStatusName') + + system_person = Person.objects.get(name='(System)') + session_status_names = { n.slug: n for n in SessionStatusName.objects.all() } + + epoch_time = datetime.datetime(1970, 1, 1, 0, 0, 0) + + for s in Session.objects.select_related('requested_by').filter(schedulingevent=None).iterator(): + # temporarily fix up weird timestamps for the migration + if s.requested == epoch_time: + s.requested = s.modified + + requested_event = SchedulingEvent() + requested_event.session = s + requested_event.time = s.requested + requested_event.by = s.requested_by + requested_event.status = session_status_names[s.status_id if s.status_id == 'apprw' or (s.status_id == 'notmeet' and not s.scheduled) else 'schedw'] + requested_event.save() + + scheduled_event = None + if s.status_id != requested_event.status_id: + if s.scheduled or s.status_id in ['sched', 'scheda']: + scheduled_event = SchedulingEvent() + scheduled_event.session = s + if s.scheduled: + scheduled_event.time = s.scheduled + else: + # we don't know when this happened + scheduled_event.time = s.modified + scheduled_event.by = system_person # we don't know who did it + scheduled_event.status = session_status_names[s.status_id if s.status_id == 'scheda' else 'sched'] + scheduled_event.save() + + final_event = None + if s.status_id not in ['apprw', 'schedw', 'notmeet', 'sched', 'scheda']: + final_event = SchedulingEvent() + final_event.session = s + final_event.time = s.modified + final_event.by = system_person # we don't know who did it + final_event.status = session_status_names[s.status_id] + final_event.save() + +def noop(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0022_schedulingevent'), + ] + + operations = [ + migrations.RunPython(create_scheduling_events, noop), + ] diff --git a/ietf/meeting/migrations/0024_auto_20191204_1731.py b/ietf/meeting/migrations/0024_auto_20191204_1731.py new file mode 100644 index 000000000..7f7454137 --- /dev/null +++ b/ietf/meeting/migrations/0024_auto_20191204_1731.py @@ -0,0 +1,28 @@ +# Copyright The IETF Trust 2019, All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2019-12-04 17:31 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0023_create_scheduling_events'), + ] + + operations = [ + migrations.RemoveField( + model_name='session', + name='requested', + ), + migrations.RemoveField( + model_name='session', + name='requested_by', + ), + migrations.RemoveField( + model_name='session', + name='status', + ), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index 04a0d4c3a..ef9b58574 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -200,13 +200,6 @@ class Meeting(models.Model): else: return None - @property - def sessions_that_can_meet(self): - qs = self.session_set.exclude(status__slug='notmeet').exclude(status__slug='disappr').exclude(status__slug='deleted').exclude(status__slug='apprw') - # Restrict graphical scheduling to meeting requests (Sessions) of type 'session' for now - qs = qs.filter(type__slug='session') - return qs - def json_url(self): return "/meeting/%s/json" % (self.number, ) @@ -705,12 +698,6 @@ class Schedule(models.Model): def qs_assignments_with_sessions(self): return self.assignments.filter(session__isnull=False) - @property - def sessions_that_can_meet(self): - if not hasattr(self, "_cached_sessions_that_can_meet"): - self._cached_sessions_that_can_meet = self.meeting.sessions_that_can_meet.all() - return self._cached_sessions_that_can_meet - def delete_schedule(self): self.assignments.all().delete() self.delete() @@ -900,11 +887,8 @@ class Session(models.Model): group = ForeignKey(Group) # The group type historically determined the session type. BOFs also need to be added as a group. Note that not all meeting requests have a natural group to associate with. attendees = models.IntegerField(null=True, blank=True) agenda_note = models.CharField(blank=True, max_length=255) - requested = models.DateTimeField(default=datetime.datetime.now) - requested_by = ForeignKey(Person) requested_duration = models.DurationField(default=datetime.timedelta(0)) comments = models.TextField(blank=True) - status = ForeignKey(SessionStatusName) scheduled = models.DateTimeField(null=True, blank=True) modified = models.DateTimeField(auto_now=True) remote_instructions = models.CharField(blank=True,max_length=1024) @@ -914,9 +898,6 @@ class Session(models.Model): unique_constraints_dict = None - def not_meeting(self): - return self.status_id == 'notmeet' - # Should work on how materials are captured so that deleted things are no longer associated with the session # (We can keep the information about something being added to and removed from a session in the document's history) def get_material(self, material_type, only_one): @@ -960,17 +941,17 @@ class Session(models.Model): return list(self.materials.filter(type='draft')) def all_meeting_sessions_for_group(self): + from ietf.meeting.utils import add_event_info_to_session_qs if self.group.type_id in ['wg','rg','ag']: if not hasattr(self, "_all_meeting_sessions_for_group_cache"): - sessions = [s for s in self.meeting.session_set.filter(group=self.group,type=self.type) if s.official_timeslotassignment()] + sessions = [s for s in add_event_info_to_session_qs(self.meeting.session_set.filter(group=self.group,type=self.type)) if s.official_timeslotassignment()] self._all_meeting_sessions_for_group_cache = sorted(sessions, key = lambda x: x.official_timeslotassignment().timeslot.time) return self._all_meeting_sessions_for_group_cache else: return [self] def all_meeting_sessions_cancelled(self): - states = set([s.status_id for s in self.all_meeting_sessions_for_group()]) - return 'canceled' in states and len(states) == 1 + return set(s.current_status for s in self.all_meeting_sessions_for_group()) == {'canceled'} def all_meeting_recordings(self): recordings = [] # These are not sets because we need to preserve relative ordering or redo the ordering work later @@ -1028,13 +1009,21 @@ class Session(models.Model): if self.meeting.type_id == "interim": return self.meeting.number - if self.status.slug in ('canceled','disappr','notmeet','deleted'): - ss0name = "(%s)" % self.status.name + status_id = None + if hasattr(self, 'current_status'): + status_id = self.current_status + elif self.pk is not None: + latest_event = SchedulingEvent.objects.filter(session=self.pk).order_by('-time', '-id').first() + if latest_event: + status_id = latest_event.status_id + + if status_id in ('canceled','disappr','notmeet','deleted'): + ss0name = "(%s)" % SessionStatusName.objects.get(slug=status_id).name else: ss0name = "(unscheduled)" ss = self.timeslotassignments.filter(schedule=self.meeting.schedule).order_by('timeslot__time') if ss: - ss0name = ','.join([x.timeslot.time.strftime("%a-%H%M") for x in 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) @property @@ -1110,16 +1099,47 @@ class Session(models.Model): sess1['bof'] = str(self.group.is_bof()) sess1['agenda_note'] = self.agenda_note sess1['attendees'] = str(self.attendees) - sess1['status'] = self.status.name + + # fish out scheduling information - eventually, we should pick + # this out in the caller instead + latest_event = None + first_event = None + + if self.pk is not None: + if not hasattr(self, 'current_status') or not hasattr(self, 'requested_time'): + events = list(SchedulingEvent.objects.filter(session=self.pk).order_by('time', 'id')) + if events: + first_event = events[0] + latest_event = events[-1] + + status_id = None + if hasattr(self, 'current_status'): + status_id = self.current_status + elif latest_event: + status_id = latest_event.status_id + + sess1['status'] = SessionStatusName.objects.get(slug=status_id).name if status_id else None if self.comments is not None: sess1['comments'] = self.comments - sess1['requested_time'] = self.requested.strftime("%Y-%m-%d") - # the related person object sometimes does not exist in the dataset. - try: - if self.requested_by is not None: - sess1['requested_by'] = str(self.requested_by) - except Person.DoesNotExist: - pass + + requested_time = None + if hasattr(self, 'requested_time'): + requested_time = self.requested_time + elif first_event: + requested_time = first_event.time + sess1['requested_time'] = requested_time.strftime("%Y-%m-%d") if requested_time else None + + + requested_by = None + if hasattr(self, 'requested_by'): + requested_by = self.requested_by + elif first_event: + requested_by = first_event.by_id + + if requested_by is not None: + requested_by_person = Person.objects.filter(pk=requested_by).first() + if requested_by_person: + sess1['requested_by'] = str(requested_by_person) sess1['requested_duration']= "%.1f" % (float(self.requested_duration.seconds) / 3600) sess1['special_request'] = str(self.special_request_token) @@ -1137,12 +1157,6 @@ class Session(models.Model): else: return "The agenda has not been uploaded yet." - def ical_status(self): - if self.status.slug == 'canceled': # sic - return "CANCELLED" - else: - return "CONFIRMED" - def agenda_file(self): if not hasattr(self, '_agenda_file'): self._agenda_file = "" @@ -1164,6 +1178,16 @@ class Session(models.Model): else: return self.group.acronym +@python_2_unicode_compatible +class SchedulingEvent(models.Model): + session = ForeignKey(Session) + time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened") + status = ForeignKey(SessionStatusName) + by = ForeignKey(Person) + + def __str__(self): + return u'%s : %s : %s : %s' % (self.session, self.status, self.time, self.by) + @python_2_unicode_compatible class ImportantDate(models.Model): meeting = ForeignKey(Meeting) diff --git a/ietf/meeting/resources.py b/ietf/meeting/resources.py index 6bcdf33cb..049196413 100644 --- a/ietf/meeting/resources.py +++ b/ietf/meeting/resources.py @@ -13,7 +13,7 @@ from ietf import api from ietf.meeting.models import ( Meeting, ResourceAssociation, Constraint, Room, Schedule, Session, TimeSlot, SchedTimeSessAssignment, SessionPresentation, FloorPlan, - UrlResource, ImportantDate, SlideSubmission ) + UrlResource, ImportantDate, SlideSubmission, SchedulingEvent ) from ietf.name.resources import MeetingTypeNameResource class MeetingResource(ModelResource): @@ -163,14 +163,12 @@ api.meeting.register(ScheduleResource()) from ietf.group.resources import GroupResource from ietf.doc.resources import DocumentResource -from ietf.name.resources import TimeSlotTypeNameResource, SessionStatusNameResource +from ietf.name.resources import TimeSlotTypeNameResource from ietf.person.resources import PersonResource class SessionResource(ModelResource): meeting = ToOneField(MeetingResource, 'meeting') type = ToOneField(TimeSlotTypeNameResource, 'type') group = ToOneField(GroupResource, 'group') - requested_by = ToOneField(PersonResource, 'requested_by') - status = ToOneField(SessionStatusNameResource, 'status') materials = ToManyField(DocumentResource, 'materials', null=True) resources = ToManyField(ResourceAssociationResource, 'resources', null=True) assignments = ToManyField('ietf.meeting.resources.SchedTimeSessAssignmentResource', 'timeslotassignments', null=True) @@ -203,6 +201,26 @@ class SessionResource(ModelResource): } api.meeting.register(SessionResource()) +from ietf.name.resources import SessionStatusNameResource +class SchedulingEventResource(ModelResource): + session = ToOneField(SessionResource, 'session') + status = ToOneField(SessionStatusNameResource, 'status') + by = ToOneField(PersonResource, 'location') + class Meta: + cache = SimpleCache() + queryset = SchedulingEvent.objects.all() + serializer = api.Serializer() + ordering = ['id', 'time', 'modified', 'meeting',] + filtering = { + "id": ALL, + "time": ALL, + "session": ALL_WITH_RELATIONS, + "by": ALL_WITH_RELATIONS, + } +api.meeting.register(SchedulingEventResource()) + + + from ietf.name.resources import TimeSlotTypeNameResource class TimeSlotResource(ModelResource): meeting = ToOneField(MeetingResource, 'meeting') diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index e311c4be3..d8508567a 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -14,7 +14,7 @@ import debug # pyflakes:ignore from ietf.doc.factories import DocumentFactory from ietf.group.models import Group from ietf.meeting.models import (Meeting, Room, TimeSlot, Session, Schedule, SchedTimeSessAssignment, - ResourceAssociation, SessionPresentation, UrlResource) + ResourceAssociation, SessionPresentation, UrlResource, SchedulingEvent) from ietf.meeting.helpers import create_interim_meeting from ietf.name.models import RoomResourceName from ietf.person.models import Person @@ -25,10 +25,11 @@ def make_interim_meeting(group,date,status='sched'): time = datetime.datetime.combine(date, datetime.time(9)) meeting = create_interim_meeting(group=group,date=date) session = Session.objects.create(meeting=meeting, group=group, - attendees=10, requested_by=system_person, status_id=status, + attendees=10, requested_duration=datetime.timedelta(minutes=20), remote_instructions='http://webex.com', - scheduled=datetime.datetime.now(),type_id="session") + type_id="session") + SchedulingEvent.objects.create(session=session, status_id=status, by=system_person) slot = TimeSlot.objects.create( meeting=meeting, type_id="session", @@ -117,43 +118,44 @@ def make_meeting_test_data(meeting=None): # mars WG mars = Group.objects.get(acronym='mars') mars_session = Session.objects.create(meeting=meeting, group=mars, - attendees=10, requested_by=system_person, status_id="schedw", - requested_duration=datetime.timedelta(minutes=20), - scheduled=datetime.datetime.now(),type_id="session") + attendees=10, requested_duration=datetime.timedelta(minutes=20), + type_id="session") + SchedulingEvent.objects.create(session=mars_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=slot1, session=mars_session, schedule=schedule) SchedTimeSessAssignment.objects.create(timeslot=slot2, session=mars_session, schedule=unofficial_schedule) # ames WG ames_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ames"), - attendees=10, requested_by=system_person, status_id="schedw", + attendees=10, requested_duration=datetime.timedelta(minutes=20), - scheduled=datetime.datetime.now(),type_id="session") + type_id="session") + SchedulingEvent.objects.create(session=ames_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=slot2, session=ames_session, schedule=schedule) SchedTimeSessAssignment.objects.create(timeslot=slot1, session=ames_session, schedule=unofficial_schedule) # IESG breakfast iesg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="iesg"), name="IESG Breakfast", attendees=25, - requested_by=system_person, status_id="schedw", requested_duration=datetime.timedelta(minutes=20), - scheduled=datetime.datetime.now(),type_id="lead") + type_id="lead") + SchedulingEvent.objects.create(session=iesg_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=breakfast_slot, session=iesg_session, schedule=schedule) # No breakfast on unofficial schedule # Registration reg_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"), name="Registration", attendees=250, - requested_by=system_person, status_id="schedw", requested_duration=datetime.timedelta(minutes=480), - scheduled=datetime.datetime.now(),type_id="reg") + 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) # Break break_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="secretariat"), name="Morning Break", attendees=250, - requested_by=system_person, status_id="schedw", requested_duration=datetime.timedelta(minutes=30), - scheduled=datetime.datetime.now(),type_id="break") + 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) meeting.schedule = schedule diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index 7c23c3301..f27216250 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -28,13 +28,16 @@ import debug # pyflakes:ignore from ietf.doc.models import Document from ietf.group.models import Group, Role +from ietf.person.models import Person from ietf.meeting.helpers import can_approve_interim_request, can_view_interim_request from ietf.meeting.helpers import send_interim_approval_request from ietf.meeting.helpers import send_interim_cancellation_notice from ietf.meeting.helpers import send_interim_minutes_reminder, populate_important_dates, update_important_dates -from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission +from ietf.meeting.models import Session, TimeSlot, Meeting, SchedTimeSessAssignment, Schedule, SessionPresentation, SlideSubmission, SchedulingEvent from ietf.meeting.test_data import make_meeting_test_data, make_interim_meeting from ietf.meeting.utils import finalize +from ietf.meeting.utils import add_event_info_to_session_qs +from ietf.meeting.utils import current_session_status from ietf.name.models import SessionStatusName, ImportantDateName from ietf.utils.decorators import skip_coverage from ietf.utils.mail import outbox, empty_outbox @@ -195,8 +198,11 @@ class MeetingTests(TestCase): self.assertContains(r, slot.location.name) # week view with a cancelled session - session.status_id='canceled' - session.save() + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='canceled'), + by=Person.objects.get(name='(System)') + ) r = self.client.get(urlreverse("ietf.meeting.views.week_view", kwargs=dict(num=meeting.number))) self.assertContains(r, 'CANCELLED') self.assertContains(r, session.group.acronym) @@ -874,8 +880,11 @@ class InterimTests(TestCase): url = urlreverse("ietf.meeting.views.interim_announce") meeting = Meeting.objects.filter(type='interim', session__group__acronym='mars').first() session = meeting.session_set.first() - session.status = SessionStatusName.objects.get(slug='scheda') - session.save() + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='scheda'), + by=Person.objects.get(name='(System)') + ) login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) self.assertContains(r, meeting.number) @@ -894,12 +903,12 @@ class InterimTests(TestCase): len_before = len(outbox) r = self.client.post(url) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_announce')) - self.assertEqual(meeting.session_set.first().status.slug,'sched') + self.assertEqual(add_event_info_to_session_qs(meeting.session_set).first().current_status, 'sched') self.assertEqual(len(outbox), len_before) def test_interim_send_announcement(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting url = urlreverse("ietf.meeting.views.interim_send_announcement", kwargs={'number': meeting.number}) login_testing_unauthorized(self, "secretary", url) r = self.client.get(url) @@ -914,26 +923,26 @@ class InterimTests(TestCase): def test_interim_approve_by_ad(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) length_before = len(outbox) login_testing_unauthorized(self, "ad", url) r = self.client.post(url, {'approve': 'approve'}) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending')) - for session in meeting.session_set.all(): - self.assertEqual(session.status.slug, 'scheda') + for session in add_event_info_to_session_qs(meeting.session_set.all()): + self.assertEqual(session.current_status, 'scheda') self.assertEqual(len(outbox), length_before + 1) self.assertIn('ready for announcement', outbox[-1]['Subject']) def test_interim_approve_by_secretariat(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) login_testing_unauthorized(self, "secretary", url) r = self.client.post(url, {'approve': 'approve'}) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_send_announcement', kwargs={'number': meeting.number})) - for session in meeting.session_set.all(): - self.assertEqual(session.status.slug, 'scheda') + for session in add_event_info_to_session_qs(meeting.session_set.all()): + self.assertEqual(session.current_status, 'scheda') def test_past(self): today = datetime.date.today() @@ -951,8 +960,9 @@ class InterimTests(TestCase): make_meeting_test_data() url = urlreverse("ietf.meeting.views.upcoming") today = datetime.date.today() - mars_interim = Meeting.objects.filter(date__gt=today, type='interim', session__group__acronym='mars', session__status='sched').first() - ames_interim = Meeting.objects.filter(date__gt=today, type='interim', session__group__acronym='ames', session__status='canceled').first() + add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first() + mars_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='mars')).filter(current_status='sched').first().meeting + ames_interim = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', meeting__date__gt=today, group__acronym='ames')).filter(current_status='canceled').first().meeting r = self.client.get(url) self.assertContains(r, mars_interim.number) self.assertContains(r, ames_interim.number) @@ -1078,7 +1088,7 @@ class InterimTests(TestCase): session = meeting.session_set.first() self.assertEqual(session.remote_instructions,remote_instructions) self.assertEqual(session.agenda_note,agenda_note) - self.assertEqual(session.status.slug,'scheda') + self.assertEqual(current_session_status(session).slug,'scheda') timeslot = session.official_timeslotassignment().timeslot self.assertEqual(timeslot.time,dt) self.assertEqual(timeslot.duration,duration) @@ -1322,7 +1332,7 @@ class InterimTests(TestCase): def test_interim_pending(self): make_meeting_test_data() url = urlreverse('ietf.meeting.views.interim_pending') - count = Meeting.objects.filter(type='interim',session__status='apprw').distinct().count() + count = len(set(s.meeting_id for s in add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim')).filter(current_status='apprw'))) # unpriviledged user login_testing_unauthorized(self,"plain",url) @@ -1343,7 +1353,7 @@ class InterimTests(TestCase): # unprivileged user user = User.objects.get(username='plain') group = Group.objects.get(acronym='mars') - meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group=group).first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group=group)).filter(current_status='apprw').first().meeting self.assertFalse(can_approve_interim_request(meeting=meeting,user=user)) # Secretariat user = User.objects.get(username='secretary') @@ -1363,7 +1373,7 @@ class InterimTests(TestCase): # unprivileged user user = User.objects.get(username='plain') group = Group.objects.get(acronym='mars') - meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group=group).first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group=group)).filter(current_status='apprw').first().meeting self.assertFalse(can_view_interim_request(meeting=meeting,user=user)) # Secretariat user = User.objects.get(username='secretary') @@ -1383,7 +1393,7 @@ class InterimTests(TestCase): def test_interim_request_details(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.get(url) @@ -1413,17 +1423,17 @@ class InterimTests(TestCase): def test_interim_request_disapprove(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) login_testing_unauthorized(self,"secretary",url) r = self.client.post(url,{'disapprove':'Disapprove'}) self.assertRedirects(r, urlreverse('ietf.meeting.views.interim_pending')) - for session in meeting.session_set.all(): - self.assertEqual(session.status_id,'disappr') + for session in add_event_info_to_session_qs(meeting.session_set.all()): + self.assertEqual(session.current_status,'disappr') def test_interim_request_cancel(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details', kwargs={'number': meeting.number}) # ensure no cancel button for unauthorized user self.client.login(username="ameschairman", password="ameschairman+password") @@ -1448,17 +1458,17 @@ class InterimTests(TestCase): length_before = len(outbox) r = self.client.post(url, {'comments': comments}) self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming')) - for session in meeting.session_set.all(): - self.assertEqual(session.status_id, 'canceledpa') + for session in add_event_info_to_session_qs(meeting.session_set.all()): + self.assertEqual(session.current_status,'canceledpa') self.assertEqual(session.agenda_note, comments) self.assertEqual(len(outbox), length_before) # no email notice # test cancelling after announcement - meeting = Meeting.objects.filter(type='interim', session__status='sched', session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting url = urlreverse('ietf.meeting.views.interim_request_cancel', kwargs={'number': meeting.number}) r = self.client.post(url, {'comments': comments}) self.assertRedirects(r, urlreverse('ietf.meeting.views.upcoming')) - for session in meeting.session_set.all(): - self.assertEqual(session.status_id, 'canceled') + for session in add_event_info_to_session_qs(meeting.session_set.all()): + self.assertEqual(session.current_status,'canceled') self.assertEqual(session.agenda_note, comments) self.assertEqual(len(outbox), length_before + 1) self.assertIn('Interim Meeting Cancelled', outbox[-1]['Subject']) @@ -1466,7 +1476,7 @@ class InterimTests(TestCase): def test_interim_request_edit_no_notice(self): '''Edit a request. No notice should go out if it hasn't been announced yet''' make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim', session__status='apprw', session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting group = meeting.session_set.first().group url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number}) # test unauthorized access @@ -1504,7 +1514,7 @@ class InterimTests(TestCase): def test_interim_request_edit(self): '''Edit request. Send notice of change''' make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim', session__status='sched', session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting group = meeting.session_set.first().group url = urlreverse('ietf.meeting.views.interim_request_edit', kwargs={'number': meeting.number}) # test unauthorized access @@ -1550,7 +1560,7 @@ class InterimTests(TestCase): def test_interim_request_details_permissions(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting url = urlreverse('ietf.meeting.views.interim_request_details',kwargs={'number':meeting.number}) # unprivileged user @@ -1560,7 +1570,7 @@ class InterimTests(TestCase): def test_send_interim_approval_request(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim',session__status='apprw',session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='apprw').first().meeting length_before = len(outbox) send_interim_approval_request(meetings=[meeting]) self.assertEqual(len(outbox),length_before+1) @@ -1568,7 +1578,7 @@ class InterimTests(TestCase): def test_send_interim_cancellation_notice(self): make_meeting_test_data() - meeting = Meeting.objects.filter(type='interim',session__status='sched',session__group__acronym='mars').first() + meeting = add_event_info_to_session_qs(Session.objects.filter(meeting__type='interim', group__acronym='mars')).filter(current_status='sched').first().meeting length_before = len(outbox) send_interim_cancellation_notice(meeting=meeting) self.assertEqual(len(outbox),length_before+1) diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 022100488..5b1944fd0 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -11,28 +11,49 @@ import six.moves.urllib.request from six.moves.urllib.error import HTTPError from django.conf import settings from django.template.loader import render_to_string +from django.db.models.expressions import Subquery, OuterRef import debug # pyflakes:ignore from ietf.dbtemplate.models import DBTemplate -from ietf.meeting.models import Session, Meeting +from ietf.meeting.models import Session, Meeting, SchedulingEvent, TimeSlot +from ietf.group.models import Group from ietf.group.utils import can_manage_materials from ietf.person.models import 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() + if official_timeslot: + return official_timeslot.time + elif use_meeting_date and session.meeting.date: + return datetime.datetime.combine(session.meeting.date, datetime.time.min) + else: + first_event = SchedulingEvent.objects.filter(session=session).order_by('time', 'id').first() + if first_event: + return first_event.time + else: + return datetime.datetime.min + +def session_requested_by(session): + first_event = SchedulingEvent.objects.filter(session=session).order_by('time', 'id').first() + if first_event: + return first_event.by + + return None + +def current_session_status(session): + last_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first() + if last_event: + return last_event.status + + return None + + def group_sessions(sessions): - def sort_key(session): - official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule) - if official_sessions: - return official_sessions.first().timeslot.time - elif session.meeting.date: - return datetime.datetime.combine(session.meeting.date,datetime.datetime.min.time()) - else: - return session.requested - for s in sessions: - s.time=sort_key(s) + s.time = session_time_for_sorting(s, use_meeting_date=True) sessions = sorted(sessions,key=lambda s:s.time) @@ -65,29 +86,24 @@ def get_upcoming_manageable_sessions(user): # This notion of searching by end-of-meeting is also present in Document.future_presentations. # It would be nice to make it easier to use querysets to talk about meeting endings wthout a heuristic like this one - candidate_sessions = Session.objects.exclude(status__in=['canceled','disappr','notmeet','deleted']).filter(meeting__date__gte=datetime.date.today()-datetime.timedelta(days=15)) - refined_candidates = [ sess for sess in candidate_sessions if sess.meeting.end_date()>=datetime.date.today()] + # We can in fact do that with something like + # .filter(date__gte=today - F('days')), but unfortunately, it + # doesn't work correctly with Django 1.11 and MySQL/SQLite - return [ sess for sess in refined_candidates if can_manage_materials(user, sess.group) ] + today = datetime.date.today() + + candidate_sessions = add_event_info_to_session_qs( + Session.objects.filter(meeting__date__gte=today - datetime.timedelta(days=15)) + ).exclude( + current_status__in=['canceled', 'disappr', 'notmeet', 'deleted'] + ).prefetch_related('meeting') + + return [ + sess for sess in candidate_sessions if sess.meeting.end_date() >= today and can_manage_materials(user, sess.group) + ] def sort_sessions(sessions): - - # Python sorts are stable since version 2,2, so this series results in a list sorted first - # by the meeting 'number', then by session's group acronym, then by scheduled time - # (or the time of the session request if the session isn't scheduled). - - def time_sort_key(session): - official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule) - if official_sessions: - return official_sessions.first().timeslot.time - else: - return session.requested - - time_sorted = sorted(sessions,key=time_sort_key) - acronym_sorted = sorted(time_sorted,key=lambda x: x.group.acronym) - meeting_sorted = sorted(acronym_sorted,key=lambda x: x.meeting.number) - - return meeting_sorted + return sorted(sessions, key=lambda s: (s.meeting.number, s.group.acronym, session_time_for_sorting(s, use_meeting_date=False))) def create_proceedings_templates(meeting): '''Create DBTemplates for meeting proceedings''' @@ -178,3 +194,100 @@ def sort_accept_tuple(accept): tup.append((keys[0], q)) return sorted(tup, key = lambda x: float(x[1]), reverse = True) return tup + +def add_event_info_to_session_qs(qs, current_status=True, requested_by=False, requested_time=False): + """Take a session queryset and add attributes computed from the + scheduling events. A queryset is returned and the added attributes + can be further filtered on.""" + from django.db.models import TextField, Value + from django.db.models.functions import Coalesce + if current_status: + qs = qs.annotate( + # coalesce with '' to avoid nulls which give funny + # results, e.g. .exclude(current_status='canceled') also + # skips rows with null in them + current_status=Coalesce(Subquery(SchedulingEvent.objects.filter(session=OuterRef('pk')).order_by('-time', '-id').values('status')[:1]), Value(''), output_field=TextField()), + ) + + if requested_by: + qs = qs.annotate( + requested_by=Subquery(SchedulingEvent.objects.filter(session=OuterRef('pk')).order_by('time', 'id').values('by')[:1]), + ) + + if requested_time: + qs = qs.annotate( + requested_time=Subquery(SchedulingEvent.objects.filter(session=OuterRef('pk')).order_by('time', 'id').values('time')[:1]), + ) + + return qs + +def only_sessions_that_can_meet(session_qs): + qs = add_event_info_to_session_qs(session_qs).exclude(current_status__in=['notmeet', 'disappr', 'deleted', 'apprw']) + + # Restrict graphical scheduling to meeting requests (Sessions) of type 'session' for now + qs = qs.filter(type__slug='session') + + return qs + +def data_for_meetings_overview(meetings, interim_status=None): + """Return filtered meetings with sessions and group hierarchy (for the + interim menu).""" + + # extract sessions + for m in meetings: + m.sessions = [] + + sessions = add_event_info_to_session_qs( + Session.objects.filter(meeting__in=meetings).order_by('meeting', 'pk') + ).select_related('group', 'group__parent') + + meeting_dict = {m.pk: m for m in meetings} + for s in sessions.iterator(): + meeting_dict[s.meeting_id].sessions.append(s) + + # filter + if interim_status == 'apprw': + meetings = [ + m for m in meetings + if not m.type_id == 'interim' or any(s.current_status == 'apprw' for s in m.sessions) + ] + + elif interim_status == 'scheda': + meetings = [ + m for m in meetings + if not m.type_id == 'interim' or any(s.current_status == 'scheda' for s in m.sessions) + ] + + else: + meetings = [ + m for m in meetings + if not m.type_id == 'interim' or not all(s.current_status in ['apprw', 'scheda', 'canceledpa'] for s in m.sessions) + ] + + # group hierarchy + ietf_group = Group.objects.get(acronym='ietf') + + group_hierarchy = [ietf_group] + + parents = {} + for m in meetings: + if m.type_id == 'interim' and m.sessions: + for s in m.sessions: + parent = parents.get(s.group.parent_id) + if not parent: + parent = s.group.parent + parent.group_list = [] + group_hierarchy.append(parent) + + parent.group_list.append(s.group) + + for p in parents.values(): + p.group_list.sort(key=lambda g: g.acronym) + + # set some useful attributes + for m in meetings: + m.end = m.date + datetime.timedelta(days=m.days) + m.responsible_group = (m.sessions[0].group if m.sessions else None) if m.type_id == 'interim' else ietf_group + m.interim_meeting_cancelled = m.type_id == 'interim' and all(s.current_status == 'canceled' for s in m.sessions) + + return meetings, group_hierarchy diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 6b1b6f1ce..b1278ecf8 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -8,6 +8,7 @@ import csv import datetime import glob import io +import itertools import json import os import pytz @@ -34,7 +35,7 @@ from django.contrib.auth.decorators import login_required from django.core.exceptions import ValidationError from django.core.validators import URLValidator from django.urls import reverse,reverse_lazy -from django.db.models import Min, Max, Q +from django.db.models import Min, Max, Q, F from django.forms.models import modelform_factory, inlineformset_factory from django.template import TemplateDoesNotExist from django.template.loader import render_to_string @@ -49,9 +50,10 @@ from ietf.doc.fields import SearchableDocumentsField from ietf.doc.models import Document, State, DocEvent, NewRevisionDocEvent, DocAlias from ietf.group.models import Group from ietf.group.utils import can_manage_materials +from ietf.person.models import Person from ietf.ietfauth.utils import role_required, has_role from ietf.mailtrigger.utils import gather_address_lists -from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission +from ietf.meeting.models import Meeting, Session, Schedule, FloorPlan, SessionPresentation, TimeSlot, SlideSubmission, SessionStatusName, SchedulingEvent, SchedTimeSessAssignment from ietf.meeting.helpers import get_areas, get_person_by_email, get_schedule_by_name from ietf.meeting.helpers import build_all_agenda_slices, get_wg_name_list from ietf.meeting.helpers import get_all_assignments_from_schedule @@ -63,12 +65,17 @@ from ietf.meeting.helpers import convert_draft_to_pdf, get_earliest_session_date from ietf.meeting.helpers import can_view_interim_request, can_approve_interim_request from ietf.meeting.helpers import can_edit_interim_request from ietf.meeting.helpers import can_request_interim_meeting, get_announcement_initial -from ietf.meeting.helpers import sessions_post_save, is_meeting_approved +from ietf.meeting.helpers import sessions_post_save, is_interim_meeting_approved from ietf.meeting.helpers import send_interim_cancellation_notice from ietf.meeting.helpers import send_interim_approval_request from ietf.meeting.helpers import send_interim_announcement_request from ietf.meeting.utils import finalize from ietf.meeting.utils import sort_accept_tuple +from ietf.meeting.utils import add_event_info_to_session_qs +from ietf.meeting.utils import session_time_for_sorting +from ietf.meeting.utils import session_requested_by +from ietf.meeting.utils import current_session_status +from ietf.meeting.utils import data_for_meetings_overview from ietf.message.utils import infer_message from ietf.secr.proceedings.utils import handle_upload_file from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files, @@ -85,7 +92,7 @@ from .forms import (InterimMeetingModelForm, InterimAnnounceForm, InterimSession InterimCancelForm, InterimSessionInlineFormSet, FileUploadForm, RequestMinutesForm,) -def get_menu_entries(request): +def get_interim_menu_entries(request): '''Setup menu entries for interim meeting view tabs''' entries = [] if has_role(request.user, ('Area Director','Secretariat','IRTF Chair','WG Chair', 'RG Chair')): @@ -129,22 +136,21 @@ def materials(request, num=None): past_cutoff_date = datetime.date.today() > meeting.get_submission_correction_date() - #sessions = Session.objects.filter(meeting__number=meeting.number, timeslot__isnull=False) schedule = get_schedule(meeting, None) - sessions = ( Session.objects - .filter(meeting__number=meeting.number, timeslotassignments__schedule=schedule) - .select_related('meeting__schedule','status','group__state','group__parent', ) - ) - for session in sessions: - session.past_cutoff_date = past_cutoff_date + + sessions = Session.objects.filter( + meeting__number=meeting.number, + timeslotassignments__schedule=schedule + ).distinct().select_related('meeting__schedule', 'group__state', 'group__parent') + plenaries = sessions.filter(name__icontains='plenary') ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu') irtf = sessions.filter(group__parent__acronym = 'irtf') training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['session', 'other', ]) iab = sessions.filter(group__parent__acronym = 'iab') - other = sessions.filter(type_id__in=['session'], group__type__features__has_meetings = True) - for ss in [plenaries, ietf, irtf, training, iab]: - other = other.exclude(pk__in=[s.pk for s in ss]) + + session_pks = [s.pk for ss in [plenaries, ietf, irtf, training, iab] for s in ss] + other = sessions.filter(type_id__in=['session'], group__type__features__has_meetings=True).exclude(pk__in=session_pks) for topic in [plenaries, ietf, training, irtf, iab]: for event in topic: @@ -152,6 +158,10 @@ def materials(request, num=None): for slide_event in event.all_meeting_slides(): date_list.append(slide_event.time) for agenda_event in event.all_meeting_agendas(): date_list.append(agenda_event.time) if date_list: setattr(event, 'last_update', sorted(date_list, reverse=True)[0]) + + for session_list in [plenaries, ietf, training, irtf, iab, other]: + for session in session_list: + session.past_cutoff_date = past_cutoff_date return render(request, "meeting/materials.html", { 'meeting': meeting, @@ -819,7 +829,7 @@ def week_view(request, num=None, name=None, owner=None): if a.session and a.session.agenda(): item["agenda"] = a.session.agenda().href() - if a.session.status_id=='canceled': + if a.session.current_status == 'canceled': item["name"] = "CANCELLED - " + item["name"] items.append(item) @@ -887,6 +897,12 @@ def room_view(request, num=None, name=None, owner=None): template = "meeting/room-view.html" return render(request, template,{"meeting":meeting,"schedule":schedule,"unavailable":unavailable,"assignments":assignments,"rooms":rooms,"days":days}) +def ical_session_status(session_with_current_status): + if session_with_current_status == 'canceled': + return "CANCELLED" + else: + return "CONFIRMED" + def ical_agenda(request, num=None, name=None, acronym=None, session_id=None): meeting = get_meeting(num, type_in=None) schedule = get_schedule(meeting, name) @@ -933,6 +949,10 @@ def ical_agenda(request, num=None, name=None, acronym=None, session_id=None): elif session_id: assignments = [ a for a in assignments if a.session_id == int(session_id) ] + for a in assignments: + if a.session: + a.session.ical_status = ical_session_status(a.session) + return render(request, "meeting/agenda.ics", { "schedule": schedule, "assignments": assignments, @@ -1007,7 +1027,7 @@ def json_agenda(request, num=None ): rev_docevent = doc.latest_event(NewRevisionDocEvent,'new_revision') modified = max(modified, (rev_docevent and rev_docevent.time) or modified) sessdict['modified'] = modified - sessdict['status'] = asgn.session.status_id + sessdict['status'] = asgn.session.current_status sessions.append(sessdict) rooms = [] @@ -1060,7 +1080,27 @@ def json_agenda(request, num=None ): def meeting_requests(request, num=None): meeting = get_meeting(num) - sessions = Session.objects.filter(meeting__number=meeting.number, type__slug='session', group__parent__isnull = False).exclude(requested_by=0).order_by("group__parent__acronym","status__slug","group__acronym").prefetch_related("group","group__ad_role__person","requested_by") + sessions = add_event_info_to_session_qs( + Session.objects.filter( + meeting__number=meeting.number, + type__slug='session', + group__parent__isnull=False + ), + requested_by=True, + ).exclude( + requested_by=0 + ).order_by( + "group__parent__acronym", "current_status", "group__acronym" + ).prefetch_related( + "group","group__ad_role__person" + ) + + status_names = {n.slug: n.name for n in SessionStatusName.objects.all()} + session_requesters = {p.pk: p for p in Person.objects.filter(pk__in=[s.requested_by for s in sessions if s.requested_by is not None])} + + for s in sessions: + s.current_status_name = status_names.get(s.current_status, s.current_status) + s.requested_by_person = session_requesters.get(s.requested_by) groups_not_meeting = Group.objects.filter(state='Active',type__in=['wg','rg','ag','bof']).exclude(acronym__in = [session.group.acronym for session in sessions]).order_by("parent__acronym","acronym").prefetch_related("parent") @@ -1075,36 +1115,32 @@ def get_sessions(num, acronym): if not sessions: sessions = Session.objects.filter(meeting=meeting,short=acronym,type__in=['session','plenary','other']) - def sort_key(session): - official_sessions = session.timeslotassignments.filter(schedule=session.meeting.schedule) - if official_sessions: - return official_sessions.first().timeslot.time - else: - return session.requested + sessions = add_event_info_to_session_qs(sessions) - return sorted(sessions,key=sort_key) + return sorted(sessions, key=lambda s: session_time_for_sorting(s, use_meeting_date=False)) -def session_details(request, num, acronym ): +def session_details(request, num, acronym): meeting = get_meeting(num=num,type_in=None) sessions = get_sessions(num, acronym) if not sessions: raise Http404 + status_names = {n.slug: n.name for n in SessionStatusName.objects.all()} for session in sessions: session.type_counter = Counter() ss = session.timeslotassignments.filter(schedule=meeting.schedule).order_by('timeslot__time') if ss: session.time = ', '.join(x.timeslot.time.strftime("%A %b-%d-%Y %H%M") for x in ss) - if session.status.slug == 'canceled': + if session.current_status == 'canceled': session.time += " CANCELLED" elif session.meeting.type_id=='interim': session.time = session.meeting.date.strftime("%A %b-%d-%Y") - if session.status.slug == 'canceled': + if session.current_status == 'canceled': session.time += " CANCELLED" else: - session.time = session.status.name + session.time = status_names.get(session.current_status, session.current_status) session.filtered_artifacts = list(session.sessionpresentation_set.filter(document__type__slug__in=['agenda','minutes','bluesheets'])) session.filtered_artifacts.sort(key=lambda d:['agenda','minutes','bluesheets'].index(d.document.type.slug)) @@ -1115,12 +1151,12 @@ def session_details(request, num, acronym ): qs = [p for p in qs if p.document.get_state_slug(p.document.type_id)!='deleted'] session.type_counter.update([p.document.type.slug for p in qs]) - # we somewhat arbitrarily use the group of the last session wet get from + # we somewhat arbitrarily use the group of the last session we get from # get_sessions() above when checking can_manage_materials() can_manage = can_manage_materials(request.user, session.group) - scheduled_sessions=[s for s in sessions if s.status_id=='sched'] - unscheduled_sessions = [s for s in sessions if s.status_id!='sched'] + scheduled_sessions = [s for s in sessions if s.current_status == 'sched'] + unscheduled_sessions = [s for s in sessions if s.current_status != 'sched'] pending_suggestions = None if request.user.is_authenticated: @@ -1790,8 +1826,8 @@ def ajax_get_utc(request): @role_required('Secretariat',) def interim_announce(request): '''View which shows interim meeting requests awaiting announcement''' - meetings = Meeting.objects.filter(type='interim', session__status='scheda').distinct() - menu_entries = get_menu_entries(request) + meetings, _ = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='scheda') + menu_entries = get_interim_menu_entries(request) selected_menu_entry = 'announce' return render(request, "meeting/interim_announce.html", { @@ -1812,7 +1848,12 @@ def interim_send_announcement(request, number): if form.is_valid(): message = form.save(user=request.user) message.related_groups.add(group) - meeting.session_set.update(status_id='sched') + for session in meeting.session_set.all(): + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='sched'), + by=request.user.person, + ) send_mail_message(request, message) messages.success(request, 'Interim meeting announcement sent') return redirect(interim_announce) @@ -1832,7 +1873,12 @@ def interim_skip_announcement(request, number): meeting = get_object_or_404(Meeting, number=number) if request.method == 'POST': - meeting.session_set.update(status_id='sched') + for session in meeting.session_set.all(): + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='sched'), + by=request.user.person, + ) messages.success(request, 'Interim meeting scheduled. No announcement sent.') return redirect(interim_announce) @@ -1843,12 +1889,12 @@ def interim_skip_announcement(request, number): @role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair') def interim_pending(request): '''View which shows interim meeting requests pending approval''' - meetings = Meeting.objects.filter(type='interim', session__status='apprw').distinct().order_by('date') - menu_entries = get_menu_entries(request) + meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(type='interim').order_by('date'), interim_status='apprw') + + menu_entries = get_interim_menu_entries(request) selected_menu_entry = 'pending' - meetings = [m for m in meetings if can_view_interim_request( - m, request.user)] + meetings = [m for m in meetings if can_view_interim_request(m, request.user)] for meeting in meetings: if can_approve_interim_request(meeting, request.user): meeting.can_approve = True @@ -1891,7 +1937,7 @@ def interim_request(request): formset = SessionFormset(instance=meeting, data=request.POST) formset.is_valid() formset.save() - sessions_post_save(formset) + sessions_post_save(request, formset) if not (is_approved or is_virtual): send_interim_approval_request(meetings=[meeting]) @@ -1922,7 +1968,7 @@ def interim_request(request): session.meeting = meeting session.save() series.append(meeting) - sessions_post_save([session_form]) + sessions_post_save(request, [session_form]) if not (is_approved or is_virtual): send_interim_approval_request(meetings=series) @@ -1947,7 +1993,9 @@ def interim_request(request): def interim_request_cancel(request, number): '''View for cancelling an interim meeting request''' meeting = get_object_or_404(Meeting, number=number) - group = meeting.session_set.first().group + first_session = meeting.session_set.first() + session_status = current_session_status(first_session) + group = first_session.group if not can_view_interim_request(meeting, request.user): return HttpResponseForbidden("You do not have permissions to cancel this meeting request") @@ -1956,11 +2004,20 @@ def interim_request_cancel(request, number): if form.is_valid(): if 'comments' in form.changed_data: meeting.session_set.update(agenda_note=form.cleaned_data.get('comments')) - if meeting.session_set.first().status.slug == 'sched': - meeting.session_set.update(status_id='canceled') + + was_scheduled = session_status.slug == 'sched' + + result_status = SessionStatusName.objects.get(slug='canceled' if was_scheduled else 'canceledpa') + for session in meeting.session_set.all(): + SchedulingEvent.objects.create( + session=first_session, + status=result_status, + by=request.user.person, + ) + + if was_scheduled: send_interim_cancellation_notice(meeting) - else: - meeting.session_set.update(status_id='canceledpa') + messages.success(request, 'Interim meeting cancelled') return redirect(upcoming) else: @@ -1968,7 +2025,9 @@ def interim_request_cancel(request, number): return render(request, "meeting/interim_request_cancel.html", { "form": form, - "meeting": meeting}) + "meeting": meeting, + "session_status": session_status, + }) @role_required('Area Director', 'Secretariat', 'IRTF Chair', 'WG Chair', 'RG Chair') @@ -1981,7 +2040,12 @@ def interim_request_details(request, number): if request.method == 'POST': if request.POST.get('approve') and can_approve_interim_request(meeting, request.user): - meeting.session_set.update(status_id='scheda') + for session in meeting.session_set.all(): + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='scheda'), + by=request.user.person, + ) messages.success(request, 'Interim meeting approved') if has_role(request.user, 'Secretariat'): return redirect(interim_send_announcement, number=number) @@ -1989,13 +2053,23 @@ def interim_request_details(request, number): send_interim_announcement_request(meeting) return redirect(interim_pending) if request.POST.get('disapprove') and can_approve_interim_request(meeting, request.user): - meeting.session_set.update(status_id='disappr') + for session in meeting.session_set.all(): + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='disappr'), + by=request.user.person, + ) messages.success(request, 'Interim meeting disapproved') return redirect(interim_pending) + first_session = sessions.first() + return render(request, "meeting/interim_request_details.html", { "meeting": meeting, "sessions": sessions, + "group": first_session.group, + "requester": session_requested_by(first_session), + "session_status": current_session_status(first_session), "can_edit": can_edit, "can_approve": can_approve}) @@ -2018,7 +2092,7 @@ def interim_request_edit(request, number): form = InterimMeetingModelForm(request=request, instance=meeting, data=request.POST) group = Group.objects.get(pk=form.data['group']) - is_approved = is_meeting_approved(meeting) + is_approved = is_interim_meeting_approved(meeting) SessionFormset.form.__init__ = curry( InterimSessionModelForm.__init__, @@ -2031,10 +2105,11 @@ def interim_request_edit(request, number): if form.is_valid() and formset.is_valid(): meeting = form.save(date=get_earliest_session_date(formset)) formset.save() - sessions_post_save(formset) + sessions_post_save(request, formset) message = 'Interim meeting request saved' - if (form.has_changed() or formset.has_changed()) and meeting.session_set.filter(status='sched'): + meeting_is_scheduled = add_event_info_to_session_qs(meeting.session_set).filter(current_status='sched').exists() + if (form.has_changed() or formset.has_changed()) and meeting_is_scheduled: send_interim_change_notice(request, meeting) message = message + ' and change announcement sent' messages.success(request, message) @@ -2053,32 +2128,8 @@ def interim_request_edit(request, number): def past(request): '''List of past meetings''' today = datetime.datetime.today() - meetings = ( Meeting.objects.filter(date__lte=today) - .exclude(session__status__in=('apprw', 'scheda', 'canceledpa')) - .order_by('-date') - .select_related('type') - .prefetch_related('session_set__status','session_set__group',) - ) - # extract groups hierarchy for display filter - seen = set() - groups = [m.session_set.first().group for m - in meetings.filter(type='interim')] - group_parents = [ Group.objects.get(acronym='ietf') ] - for g in groups: - if g.parent and g.parent.acronym not in seen: - group_parents.append(g.parent) - seen.add(g.parent.acronym) - - seen = set() - for p in group_parents: - p.group_list = [] - for g in groups: - if g.acronym not in seen and g.parent == p: - p.group_list.append(g) - seen.add(g.acronym) - - p.group_list.sort(key=lambda g: g.acronym) + meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(date__lte=today).order_by('-date')) return render(request, 'meeting/past.html', { 'meetings': meetings, @@ -2087,35 +2138,11 @@ def past(request): def upcoming(request): '''List of upcoming meetings''' today = datetime.datetime.today() - meetings = Meeting.objects.filter(date__gte=today).exclude( - session__status__in=('apprw', 'scheda', 'canceledpa')).order_by('date') - # extract groups hierarchy for display filter - seen = set() - groups = [m.session_set.first().group for m - in meetings.filter(type='interim')] - group_parents = [] - for g in groups: - if g.parent.acronym not in seen: - group_parents.append(g.parent) - seen.add(g.parent.acronym) - - seen = set() - for p in group_parents: - p.group_list = [] - for g in groups: - if g.acronym not in seen and g.parent == p: - p.group_list.append(g) - seen.add(g.acronym) - - p.group_list.sort(key=lambda g: g.acronym) - - meetings = list(meetings) - for m in meetings: - m.end = m.date+datetime.timedelta(days=m.days) + meetings, group_parents = data_for_meetings_overview(Meeting.objects.filter(date__gte=today).order_by('date')) # add menu entries - menu_entries = get_menu_entries(request) + menu_entries = get_interim_menu_entries(request) selected_menu_entry = 'upcoming' # add menu actions @@ -2138,14 +2165,17 @@ def upcoming_ical(request): '''Return Upcoming meetings in iCalendar file''' filters = request.GET.getlist('filters') today = datetime.datetime.today() - meetings = Meeting.objects.filter(date__gte=today).exclude( - session__status__in=('apprw', 'schedpa')).order_by('date') - assignments = [] - for meeting in meetings: - items = meeting.schedule.assignments.order_by( - 'session__type__slug', 'timeslot__time') - assignments.extend(items) + meetings, _ = data_for_meetings_overview(Meeting.objects.filter(date__gte=today).order_by('date')) + + assignments = list(SchedTimeSessAssignment.objects.filter( + schedule__meeting__schedule=F('schedule'), + session__in=[s.pk for m in meetings for s in m.sessions] + ).order_by( + 'schedule__meeting__date', 'session__type', 'timeslot__time' + ).select_related( + 'session__group', 'session__group__parent', 'timeslot', 'schedule', 'schedule__meeting' + ).distinct()) # apply filters if filters: @@ -2155,6 +2185,14 @@ def upcoming_ical(request): a.session.group.parent and a.session.group.parent.acronym in filters ) ) ] + + # we already collected sessions with current_status, so reuse those + sessions = {s.pk: s for m in meetings for s in m.sessions} + for a in assignments: + if a.session_id is not None: + a.session = sessions.get(a.session_id) or a.session + a.session.ical_status = ical_session_status(a.session.current_status) + # gather vtimezones vtimezones = set() for meeting in meetings: @@ -2198,17 +2236,36 @@ def proceedings(request, num=None): now = datetime.date.today() schedule = get_schedule(meeting, None) - sessions = Session.objects.filter(meeting__number=meeting.number).filter(Q(timeslotassignments__schedule=schedule)|Q(status='notmeet')).select_related().order_by('-status_id') - plenaries = sessions.filter(name__icontains='plenary').exclude(status='notmeet') + sessions = add_event_info_to_session_qs( + Session.objects.filter(meeting__number=meeting.number) + ).filter( + Q(timeslotassignments__schedule=schedule) | Q(current_status='notmeet') + ).select_related().order_by('-current_status') + plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet') ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu') irtf = sessions.filter(group__parent__acronym = 'irtf') - training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['session', 'other', ]).exclude(status='notmeet') - iab = sessions.filter(group__parent__acronym = 'iab').exclude(status='notmeet') + training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['session', 'other', ]).exclude(current_status='notmeet') + iab = sessions.filter(group__parent__acronym = 'iab').exclude(current_status='notmeet') cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"] + + ietf_areas = [] + for area, sessions in itertools.groupby(sorted(ietf, key=lambda s: (s.group.parent.acronym, s.group.acronym)), key=lambda s: s.group.parent): + sessions = list(sessions) + meeting_groups = set(s.group_id for s in sessions if s.current_status != 'notmeet') + meeting_sessions = [] + not_meeting_sessions = [] + for s in sessions: + if s.current_status == 'notmeet' and s.group_id not in meeting_groups: + not_meeting_sessions.append(s) + else: + meeting_sessions.append(s) + ietf_areas.append((area, meeting_sessions, not_meeting_sessions)) + return render(request, "meeting/proceedings.html", { 'meeting': meeting, 'plenaries': plenaries, 'ietf': ietf, 'training': training, 'irtf': irtf, 'iab': iab, + 'ietf_areas': ietf_areas, 'cut_off_date': cut_off_date, 'cor_cut_off_date': cor_cut_off_date, 'submission_started': now > begin_date, @@ -2424,12 +2481,19 @@ def request_minutes(request, num=None): ) return HttpResponseRedirect(reverse('ietf.meeting.views.materials',kwargs={'num':num})) else: - needs_minutes = set() - for a in meeting.schedule.assignments.filter(session__group__type_id__in=('wg','rg','ag')).exclude(session__status='canceled'): - if not a.session.all_meeting_minutes(): - group = a.session.group + needs_minutes = set() + session_qs = add_event_info_to_session_qs( + Session.objects.filter( + timeslotassignments__schedule__meeting=meeting, + timeslotassignments__schedule__meeting__schedule=F('timeslotassignments__schedule'), + group__type__in=['wg','rg','ag'], + ) + ).filter(~Q(current_status='canceled')).select_related('group', 'group__parent') + for session in session_qs: + if not session.all_meeting_minutes(): + group = session.group if group.parent and group.parent.type_id in ('area','irtf'): - needs_minutes.add(a.session.group) + needs_minutes.add(group) needs_minutes = list(needs_minutes) needs_minutes.sort(key=lambda g: ('zzz' if g.parent.acronym == 'irtf' else g.parent.acronym)+":"+g.acronym) body_context = {'meeting':meeting, diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index f9f762e2f..f826be015 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -16,7 +16,7 @@ from django.conf import settings from django.urls import reverse from ietf.group.models import Group, GroupEvent -from ietf.meeting.models import Meeting, Room, TimeSlot, SchedTimeSessAssignment, Session +from ietf.meeting.models import Meeting, Room, TimeSlot, SchedTimeSessAssignment, Session, SchedulingEvent from ietf.meeting.test_data import make_meeting_test_data from ietf.name.models import SessionStatusName from ietf.person.models import Person @@ -152,10 +152,9 @@ class SecrMeetingTestCase(TestCase): mars_group = Group.objects.get(acronym='mars') ames_group = Group.objects.get(acronym='ames') ames_stsa = meeting.schedule.assignments.get(session__group=ames_group) - assert ames_stsa.session.status_id == 'schedw' + assert SchedulingEvent.objects.filter(session=ames_stsa.session).order_by('-id')[0].status_id == 'schedw' mars_stsa = meeting.schedule.assignments.get(session__group=mars_group) - mars_stsa.session.status = SessionStatusName.objects.get(slug='appr') - mars_stsa.session.save() + SchedulingEvent.objects.create(session=mars_stsa.session, status=SessionStatusName.objects.get(slug='appr'), by=Person.objects.get(name="(System)")) url = reverse('ietf.secr.meetings.views.notifications',kwargs={'meeting_id':72}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) @@ -185,9 +184,9 @@ class SecrMeetingTestCase(TestCase): self.assertEqual(response.status_code, 302) self.assertEqual(len(outbox), mailbox_before + 1) ames_stsa = meeting.schedule.assignments.get(session__group=ames_group) - assert ames_stsa.session.status_id == 'sched' + self.assertEqual(SchedulingEvent.objects.filter(session=ames_stsa.session).order_by('-id')[0].status_id, 'sched') mars_stsa = meeting.schedule.assignments.get(session__group=mars_group) - assert mars_stsa.session.status_id == 'sched' + self.assertEqual(SchedulingEvent.objects.filter(session=mars_stsa.session).order_by('-id')[0].status_id, 'sched') def test_meetings_rooms(self): meeting = make_meeting_test_data() @@ -363,7 +362,7 @@ class SecrMeetingTestCase(TestCase): response = self.client.post(url, {'post':'yes'}) self.assertRedirects(response, redirect_url) session = slot.sessionassignments.filter(schedule=meeting.schedule).first().session - self.assertEqual(session.status_id, 'canceled') + self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'canceled') def test_meetings_session_edit(self): meeting = make_meeting_test_data() diff --git a/ietf/secr/meetings/views.py b/ietf/secr/meetings/views.py index 23d646c10..baea035e3 100644 --- a/ietf/secr/meetings/views.py +++ b/ietf/secr/meetings/views.py @@ -17,7 +17,9 @@ from ietf.ietfauth.utils import role_required from ietf.utils.mail import send_mail from ietf.meeting.forms import duration_string from ietf.meeting.helpers import get_meeting, make_materials_directories, populate_important_dates -from ietf.meeting.models import Meeting, Session, Room, TimeSlot, SchedTimeSessAssignment, Schedule +from ietf.meeting.models import Meeting, Session, Room, TimeSlot, SchedTimeSessAssignment, Schedule, SchedulingEvent +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 @@ -35,17 +37,6 @@ from ietf.mailtrigger.utils import gather_address_lists # -------------------------------------------------- # Helper Functions # -------------------------------------------------- -def assign(session,timeslot,meeting,schedule=None): - ''' - Robust function to assign a session to a timeslot. Much simplyfied 2014-03-26. - ''' - if schedule == None: - schedule = meeting.schedule - SchedTimeSessAssignment.objects.create(schedule=schedule, - session=session, - timeslot=timeslot) - session.status_id = 'sched' - session.save() def build_timeslots(meeting,room=None): ''' @@ -155,16 +146,21 @@ def send_notifications(meeting, groups, person): items[i]['period'] = '%s-%s' % (t.time.strftime('%H%M'),(t.time + t.duration).strftime('%H%M')) # send email + first_event = SchedulingEvent.objects.filter(session=sessions[0]).select_related('by').order_by('time', 'id').first() + requested_by = None + if first_event and first_event.status_id in ['appw', 'schedw']: + requested_by = first_event.by + context = { 'items': items, 'meeting': meeting, 'baseurl': settings.IDTRACKER_BASE_URL, } - context['to_name'] = sessions[0].requested_by + context['to_name'] = str(requested_by) or "Requester" context['agenda_note'] = sessions[0].agenda_note context['session'] = get_initial_session(sessions) context['group'] = group - context['login'] = sessions[0].requested_by + context['login'] = requested_by send_mail(None, addrs.to, @@ -411,16 +407,18 @@ def non_session(request, meeting_id, schedule_name): group = Group.objects.get(acronym='secretariat') # create associated Session object - session = Session(meeting=meeting, - name=name, - short=short, - group=group, - requested_by=Person.objects.get(name='(System)'), - status_id='sched', - type=type, - ) - session.save() - + session = Session.objects.create(meeting=meeting, + name=name, + short=short, + group=group, + type=type) + + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='sched'), + by=request.user.person, + ) + # create association SchedTimeSessAssignment.objects.create(timeslot=timeslot, session=session, @@ -434,6 +432,14 @@ def non_session(request, meeting_id, schedule_name): if TimeSlot.objects.filter(meeting=meeting,type='other',location__isnull=True): messages.warning(request, 'There are non-session items which do not have a room assigned') + session_statuses = { + e.session_id: e.status_id + for e in SchedulingEvent.objects.filter(session__in=[a.session_id for a in assignments]).order_by('time', 'id') + } + + for a in assignments: + a.current_session_status = session_statuses.get(a.session_id) + return render(request, 'meetings/non_session.html', { 'assignments': assignments, 'form': form, @@ -453,8 +459,12 @@ def non_session_cancel(request, meeting_id, schedule_name, slot_id): schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name) if request.method == 'POST' and request.POST['post'] == 'yes': - assignments = slot.sessionassignments.filter(schedule=schedule) - Session.objects.filter(pk__in=[x.session.pk for x in assignments]).update(status_id='canceled') + for session in Session.objects.filter(timeslotassignments__schedule=schedule, timeslotassignments__timeslot=slot): + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='canceled'), + by=request.user.person, + ) messages.success(request, 'The session was cancelled successfully') return redirect('ietf.secr.meetings.views.non_session', meeting_id=meeting_id, schedule_name=schedule_name) @@ -569,12 +579,13 @@ def notifications(request, meeting_id): if request.method == "POST": # ensure session state is scheduled - for ss in meeting.schedule.assignments.all(): - session = ss.session - if session.status.slug in ["schedw", "appr"]: - session.status_id = "sched" - session.scheduled = datetime.datetime.now() - session.save() + sessions = add_event_info_to_session_qs(Session.objects.filter(timeslotassignments__schedule=meeting.schedule_id)).filter(current_status__in=["schedw", "appr"]) + for session in sessions: + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='sched'), + by=request.user.person, + ) send_notifications(meeting,groups,request.user.person) messages.success(request, "Notifications Sent") @@ -638,16 +649,27 @@ def sessions(request, meeting_id, schedule_name): ''' meeting = get_object_or_404(Meeting, number=meeting_id) schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name) - sessions = schedule.sessions_that_can_meet.order_by('group__acronym') - + + sessions = add_event_info_to_session_qs( + only_sessions_that_can_meet(schedule.meeting.session_set) + ).order_by('group__acronym') + if request.method == 'POST': if 'cancel' in request.POST: pk = request.POST.get('pk') - session = Session.objects.get(pk=pk) - session.status = SessionStatusName.objects.get(slug='canceled') - session.save() + 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') + 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) + return render(request, 'meetings/sessions.html', { 'meeting': meeting, 'schedule': schedule, @@ -664,7 +686,7 @@ def session_edit(request, meeting_id, schedule_name, session_id): meeting = get_object_or_404(Meeting, number=meeting_id) schedule = get_object_or_404(Schedule, meeting=meeting, name=schedule_name) session = get_object_or_404(Session, id=session_id) - assignment = SchedTimeSessAssignment.objects.get(schedule=schedule,session=session) + assignment = SchedTimeSessAssignment.objects.filter(schedule=schedule, session=session).first() if request.method == 'POST': form = SessionEditForm(request.POST, instance=session) @@ -676,11 +698,17 @@ def session_edit(request, meeting_id, schedule_name, session_id): else: form = SessionEditForm(instance=session) + current_status_name = None + latest_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first() + if latest_event: + current_status_name = latest_event.status.name + return render(request, 'meetings/session_edit.html', { 'meeting': meeting, 'schedule': schedule, 'session': session, - 'timeslot': assignment.timeslot, + 'timeslot': assignment.timeslot if assignment else None, + 'current_status_name': current_status_name, 'form': form}, ) @@ -821,8 +849,13 @@ def times_delete(request, meeting_id, schedule_name, time): for assignment in slot.sessionassignments.all(): if assignment.session: session = assignment.session - session.status = status - session.save() + latest_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first() + if not latest_event or latest_event.status_id != 'schedw': + SchedulingEvent.objects.create( + session=session, + status=status, + by=request.user.person, + ) assignment.delete() slot.delete() messages.success(request, 'The entry was deleted successfully') diff --git a/ietf/secr/proceedings/forms.py b/ietf/secr/proceedings/forms.py index 7243d4e10..dc3b49846 100644 --- a/ietf/secr/proceedings/forms.py +++ b/ietf/secr/proceedings/forms.py @@ -1,8 +1,10 @@ +# Copyright The IETF Trust 2007-2019, All Rights Reserved from django import forms from ietf.doc.models import Document from ietf.meeting.models import Session +from ietf.meeting.utils import add_event_info_to_session_qs # --------------------------------------------- @@ -25,8 +27,9 @@ class RecordingForm(forms.Form): def __init__(self, *args, **kwargs): self.meeting = kwargs.pop('meeting') super(RecordingForm, self).__init__(*args,**kwargs) - self.fields['session'].queryset = Session.objects.filter(meeting=self.meeting, - type__in=('session','plenary','other'),status='sched').order_by('group__acronym') + self.fields['session'].queryset = add_event_info_to_session_qs( + Session.objects.filter(meeting=self.meeting, type__in=('session','plenary','other')) + ).filter(current_status='sched').order_by('group__acronym') class RecordingEditForm(forms.ModelForm): class Meta: diff --git a/ietf/secr/proceedings/proc_utils.py b/ietf/secr/proceedings/proc_utils.py index 14c3efef5..c8cd6323b 100644 --- a/ietf/secr/proceedings/proc_utils.py +++ b/ietf/secr/proceedings/proc_utils.py @@ -22,7 +22,7 @@ from django.core.exceptions import ObjectDoesNotExist from ietf.doc.models import Document, DocAlias, DocEvent, NewRevisionDocEvent, State from ietf.group.models import Group -from ietf.meeting.models import Meeting, SessionPresentation, TimeSlot, SchedTimeSessAssignment +from ietf.meeting.models import Meeting, SessionPresentation, TimeSlot, SchedTimeSessAssignment, Session from ietf.person.models import Person from ietf.utils.log import log from ietf.utils.mail import send_mail @@ -65,6 +65,8 @@ def import_audio_files(meeting): Example: ietf90-salonb-20140721-1710.mp3 ''' + from ietf.meeting.utils import add_event_info_to_session_qs + unmatched_files = [] path = os.path.join(settings.MEETING_RECORDINGS_DIR, meeting.type.slug + meeting.number) if not os.path.exists(path): @@ -72,15 +74,18 @@ def import_audio_files(meeting): for filename in os.listdir(path): timeslot = get_timeslot_for_filename(filename) if timeslot: - sessionassignments = timeslot.sessionassignments.filter( - schedule=timeslot.meeting.schedule, - session__status='sched', - ).exclude(session__agenda_note__icontains='canceled').order_by('timeslot__time') - if not sessionassignments: + sessions = add_event_info_to_session_qs(Session.objects.filter( + timeslotassignments__schedule=timeslot.meeting.schedule_id, + ).exclude( + agenda_note__icontains='canceled' + )).filter( + current_status='sched', + ).order_by('timeslotassignments__timeslot__time') + if not sessions: continue url = settings.IETF_AUDIO_URL + 'ietf{}/{}'.format(meeting.number, filename) - doc = get_or_create_recording_document(url,sessionassignments[0].session) - attach_recording(doc, [ x.session for x in sessionassignments ]) + doc = get_or_create_recording_document(url, sessions[0]) + attach_recording(doc, sessions) else: # use for reconciliation email unmatched_files.append(filename) @@ -92,6 +97,8 @@ def get_timeslot_for_filename(filename): '''Returns a timeslot matching the filename given. NOTE: currently only works with ietfNN prefix (regular meetings) ''' + from ietf.meeting.utils import add_event_info_to_session_qs + basename, _ = os.path.splitext(filename) match = AUDIO_FILE_RE.match(basename) if match: @@ -104,9 +111,10 @@ def get_timeslot_for_filename(filename): location__name=room_mapping[match.groupdict()['room']], time=time, sessionassignments__schedule=meeting.schedule, - ).exclude(sessions__status_id='canceled').distinct() - return slots.get() - except (ObjectDoesNotExist, KeyError): + ).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] + except (ObjectDoesNotExist, KeyError, IndexError): return None def attach_recording(doc, sessions): diff --git a/ietf/secr/proceedings/tests.py b/ietf/secr/proceedings/tests.py index 432cf46cb..2004b88f6 100644 --- a/ietf/secr/proceedings/tests.py +++ b/ietf/secr/proceedings/tests.py @@ -15,8 +15,9 @@ from django.urls import reverse from ietf.doc.models import Document from ietf.group.factories import RoleFactory -from ietf.meeting.models import SchedTimeSessAssignment +from ietf.meeting.models import SchedTimeSessAssignment, SchedulingEvent from ietf.meeting.factories import MeetingFactory, SessionFactory +from ietf.person.models import Person from ietf.name.models import SessionStatusName from ietf.utils.test_utils import TestCase from ietf.utils.mail import outbox @@ -129,10 +130,16 @@ class RecordingTestCase(TestCase): mars_session = SessionFactory(meeting=meeting,status_id='sched',group__acronym='mars') ames_session = SessionFactory(meeting=meeting,status_id='sched',group__acronym='ames') scheduled = SessionStatusName.objects.get(slug='sched') - mars_session.status = scheduled - mars_session.save() - ames_session.status = scheduled - ames_session.save() + SchedulingEvent.objects.create( + session=mars_session, + status=scheduled, + by=Person.objects.get(name='(System)') + ) + SchedulingEvent.objects.create( + session=ames_session, + status=scheduled, + by=Person.objects.get(name='(System)') + ) timeslot = mars_session.official_timeslotassignment().timeslot SchedTimeSessAssignment.objects.create(timeslot=timeslot,session=ames_session,schedule=meeting.schedule) self.create_audio_file_for_timeslot(timeslot) diff --git a/ietf/secr/proceedings/views.py b/ietf/secr/proceedings/views.py index ceb1f28c4..a51a22531 100644 --- a/ietf/secr/proceedings/views.py +++ b/ietf/secr/proceedings/views.py @@ -25,6 +25,7 @@ from ietf.doc.models import Document, DocEvent from ietf.person.models import Person from ietf.ietfauth.utils import has_role, role_required from ietf.meeting.models import Meeting, Session +from ietf.meeting.utils import add_event_info_to_session_qs from ietf.secr.proceedings.forms import RecordingForm, RecordingEditForm from ietf.secr.proceedings.proc_utils import (create_recording) @@ -170,7 +171,8 @@ def main(request): meetings = [m for m in Meeting.objects.filter(type='ietf').order_by('-number') if m.get_submission_correction_date()>=today] groups = get_my_groups(request.user) - interim_meetings = Meeting.objects.filter(type='interim',session__group__in=groups,session__status='sched').order_by('-date') + interim_sessions = add_event_info_to_session_qs(Session.objects.filter(group__in=groups, meeting__type='interim')).filter(current_status='sched').select_related('meeting') + interim_meetings = sorted({s.meeting for s in interim_sessions}, key=lambda m: m.date, reverse=True) # tac on group for use in templates for m in interim_meetings: m.group = m.session_set.first().group diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index 3d01dc0c5..668faa7a2 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -13,7 +13,7 @@ import debug # pyflakes:ignore from ietf.utils.test_utils import TestCase from ietf.group.factories import GroupFactory, RoleFactory -from ietf.meeting.models import Session, ResourceAssociation +from ietf.meeting.models import Session, ResourceAssociation, SchedulingEvent from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.person.models import Person from ietf.utils.mail import outbox, empty_outbox @@ -42,16 +42,16 @@ class SessionRequestTestCase(TestCase): def test_main(self): meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) SessionFactory.create_batch(2, meeting=meeting, status_id='sched') - SessionFactory.create_batch(2, meeting=meeting, status_id='unsched') + SessionFactory.create_batch(2, meeting=meeting, status_id='disappr') # An additional unscheduled group comes from make_immutable_base_data url = reverse('ietf.secr.sreq.views.main') self.client.login(username="secretary", password="secretary+password") r = self.client.get(url) self.assertEqual(r.status_code, 200) sched = r.context['scheduled_groups'] + self.assertEqual(len(sched), 2) unsched = r.context['unscheduled_groups'] - self.assertEqual(len(unsched),8) - self.assertEqual(len(sched),2) + self.assertEqual(len(unsched), 8) def test_approve(self): meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) @@ -64,25 +64,23 @@ class SessionRequestTestCase(TestCase): self.client.login(username="ad", password="ad+password") r = self.client.get(url) self.assertRedirects(r,reverse('ietf.secr.sreq.views.view', kwargs={'acronym':'mars'})) - session = Session.objects.get(pk=session.pk) - self.assertEqual(session.status_id,'appr') + self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'appr') def test_cancel(self): meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) ad = Person.objects.get(user__username='ad') area = RoleFactory(name_id='ad', person=ad, group__type_id='area').group - mars = SessionFactory(meeting=meeting, group__parent=area, group__acronym='mars', status_id='sched').group + session = SessionFactory(meeting=meeting, group__parent=area, group__acronym='mars', status_id='sched') url = reverse('ietf.secr.sreq.views.cancel', kwargs={'acronym':'mars'}) self.client.login(username="ad", password="ad+password") r = self.client.get(url) self.assertRedirects(r,reverse('ietf.secr.sreq.views.main')) - sessions = Session.objects.filter(meeting=meeting, group=mars) - self.assertEqual(sessions[0].status_id,'deleted') - + self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'deleted') + def test_edit(self): meeting = MeetingFactory(type_id='ietf', date=datetime.date.today()) mars = RoleFactory(name_id='chair', person__user__username='marschairman', group__acronym='mars').group - SessionFactory(meeting=meeting,group=mars,status_id='sched',scheduled=datetime.datetime.now()) + SessionFactory(meeting=meeting,group=mars,status_id='sched') url = reverse('ietf.secr.sreq.views.edit', kwargs={'acronym':'mars'}) self.client.login(username="marschairman", password="marschairman+password") diff --git a/ietf/secr/sreq/urls.py b/ietf/secr/sreq/urls.py index c8337e4fc..7e0db8117 100644 --- a/ietf/secr/sreq/urls.py +++ b/ietf/secr/sreq/urls.py @@ -1,3 +1,5 @@ +# Copyright The IETF Trust 2007-2019, All Rights Reserved + from django.conf import settings from ietf.secr.sreq import views @@ -14,5 +16,5 @@ urlpatterns = [ url(r'^%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit), url(r'^%(acronym)s/new/$' % settings.URL_REGEXPS, views.new), url(r'^%(acronym)s/no_session/$' % settings.URL_REGEXPS, views.no_session), - url(r'^(?P[A-Za-z0-9_\-\+]+)/%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit_mtg), + url(r'^(?P[A-Za-z0-9_\-\+]+)/%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit), ] diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index 8ddd68ee5..69ffdb8b7 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -5,22 +5,25 @@ from __future__ import absolute_import, print_function, unicode_literals import datetime +from collections import defaultdict from django.conf import settings from django.contrib import messages from django.db.models import Q from django.shortcuts import render, get_object_or_404, redirect +from django.http import Http404 import debug # pyflakes:ignore -from ietf.group.models import Group +from ietf.group.models import Group, GroupFeatures from ietf.ietfauth.utils import has_role, role_required -from ietf.meeting.models import Meeting, Session, Constraint, ResourceAssociation +from ietf.meeting.models import Meeting, Session, Constraint, ResourceAssociation, SchedulingEvent from ietf.meeting.helpers import get_meeting +from ietf.meeting.utils import add_event_info_to_session_qs from ietf.name.models import SessionStatusName, ConstraintName from ietf.secr.sreq.forms import SessionForm, ToolStatusForm from ietf.secr.utils.decorators import check_permissions -from ietf.secr.utils.group import groups_by_session +from ietf.secr.utils.group import get_my_groups from ietf.utils.mail import send_mail from ietf.person.models import Person from ietf.mailtrigger.utils import gather_address_lists @@ -171,11 +174,18 @@ def approve(request, acronym): ''' meeting = get_meeting() group = get_object_or_404(Group, acronym=acronym) - session = Session.objects.get(meeting=meeting,group=group,status='apprw') + + session = add_event_info_to_session_qs(Session.objects.filter(meeting=meeting, group=group)).filter(current_status='apprw').first() + if session is None: + raise Http404 if has_role(request.user,'Secretariat') or group.parent.role_set.filter(name='ad',person=request.user.person): - session.status = SessionStatusName.objects.get(slug='appr') - session_save(session) + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='appr'), + by=request.user.person, + ) + session_changed(session) messages.success(request, 'Third session approved') return redirect('ietf.secr.sreq.views.view', acronym=acronym) @@ -205,8 +215,12 @@ def cancel(request, acronym): # mark sessions as deleted for session in sessions: - session.status_id = 'deleted' - session_save(session) + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='deleted'), + by=request.user.person, + ) + session_changed(session) # clear schedule assignments if already scheduled session.timeslotassignments.all().delete() @@ -236,7 +250,7 @@ def confirm(request, acronym): login = request.user.person # check if request already exists for this group - if Session.objects.filter(group=group,meeting=meeting).exclude(status__in=('deleted','notmeet')): + if add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(Q(current_status__isnull=True) | ~Q(current_status__in=['deleted', 'notmeet'])): messages.warning(request, 'Sessions for working group %s have already been requested once.' % group.acronym) return redirect('ietf.secr.sreq.views.main') @@ -258,7 +272,7 @@ def confirm(request, acronym): if request.method == 'POST' and button_text == 'Submit': # delete any existing session records with status = canceled or notmeet - Session.objects.filter(group=group,meeting=meeting,status__in=('canceled','notmeet')).delete() + add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status__in=['canceled', 'notmeet']).delete() # create new session records count = 0 @@ -268,19 +282,22 @@ def confirm(request, acronym): count += 1 if duration: slug = 'apprw' if count == 3 else 'schedw' - new_session = Session(meeting=meeting, - group=group, - attendees=form.data['attendees'], - requested=datetime.datetime.now(), - requested_by=login, - requested_duration=datetime.timedelta(0,int(duration)), - comments=form.data['comments'], - status=SessionStatusName.objects.get(slug=slug), - type_id='session', - ) - session_save(new_session) + new_session = Session.objects.create( + meeting=meeting, + group=group, + attendees=form.data['attendees'], + requested_duration=datetime.timedelta(0,int(duration)), + comments=form.data['comments'], + type_id='session', + ) + SchedulingEvent.objects.create( + session=new_session, + status=SessionStatusName.objects.get(slug=slug), + by=login, + ) if 'resources' in form.data: new_session.resources.set(session_data['resources']) + session_changed(new_session) # write constraint records save_conflicts(group,meeting,form.data.get('conflict1',''),'conflict') @@ -293,7 +310,7 @@ def confirm(request, acronym): Constraint.objects.create(name=bethere_cn, source=group, person=p, meeting=new_session.meeting) # clear not meeting - Session.objects.filter(group=group,meeting=meeting,status='notmeet').delete() + add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status='notmeet').delete() # send notification send_notification(group,meeting,login,session_data,'new') @@ -322,23 +339,21 @@ def add_essential_people(group,initial): initial['bethere'] = list(people) -def edit(request, *args, **kwargs): - return edit_mtg(request, None, *args, **kwargs) +def session_changed(session): + latest_event = SchedulingEvent.objects.filter(session=session).order_by('-time', '-id').first() -def session_save(session): - session.save() - if session.status_id == "schedw" and session.meeting.schedule != None: + if latest_event and latest_event.status_id == "schedw" and session.meeting.schedule != None: # send an email to iesg-secretariat to alert to change pass @check_permissions -def edit_mtg(request, num, acronym): +def edit(request, acronym, num=None): ''' This view allows the user to edit details of the session request ''' meeting = get_meeting(num) group = get_object_or_404(Group, acronym=acronym) - sessions = Session.objects.filter(meeting=meeting,group=group).exclude(status__in=('deleted','notmeet')).order_by('id') + sessions = add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(Q(current_status__isnull=True) | ~Q(current_status__in=['canceled', 'notmeet'])).order_by('id') sessions_count = sessions.count() initial = get_initial_session(sessions) if 'resources' in initial: @@ -370,7 +385,8 @@ def edit_mtg(request, num, acronym): if 'length_session1' in form.changed_data: session = sessions[0] session.requested_duration = datetime.timedelta(0,int(form.cleaned_data['length_session1'])) - session_save(session) + session.save() + session_changed(session) # session 2 if 'length_session2' in form.changed_data: @@ -379,22 +395,24 @@ def edit_mtg(request, num, acronym): sessions[1].delete() elif sessions_count < 2: duration = datetime.timedelta(0,int(form.cleaned_data['length_session2'])) - new_session = Session(meeting=meeting, - group=group, - attendees=form.cleaned_data['attendees'], - requested=datetime.datetime.now(), - requested_by=login, - requested_duration=duration, - comments=form.cleaned_data['comments'], - status=SessionStatusName.objects.get(slug='schedw'), - type_id='session', - ) - new_session.save() + new_session = Session.objects.create( + meeting=meeting, + group=group, + attendees=form.cleaned_data['attendees'], + requested_duration=duration, + comments=form.cleaned_data['comments'], + type_id='session', + ) + SchedulingEvent.objects.create( + session=new_session, + status=SessionStatusName.objects.get(slug='schedw'), + by=request.user.person, + ) else: duration = datetime.timedelta(0,int(form.cleaned_data['length_session2'])) session = sessions[1] session.requested_duration = duration - session_save(session) + session.save() # session 3 if 'length_session3' in form.changed_data: @@ -403,22 +421,25 @@ def edit_mtg(request, num, acronym): sessions[2].delete() elif sessions_count < 3: duration = datetime.timedelta(0,int(form.cleaned_data['length_session3'])) - new_session = Session(meeting=meeting, - group=group, - attendees=form.cleaned_data['attendees'], - requested=datetime.datetime.now(), - requested_by=login, - requested_duration=duration, - comments=form.cleaned_data['comments'], - status=SessionStatusName.objects.get(slug='apprw'), - type_id='session', - ) - new_session.save() + new_session = Session.objects.create( + meeting=meeting, + group=group, + attendees=form.cleaned_data['attendees'], + requested_duration=duration, + comments=form.cleaned_data['comments'], + type_id='session', + ) + SchedulingEvent.objects.create( + session=new_session, + status=SessionStatusName.objects.get(slug='apprw'), + by=request.user.person, + ) else: duration = datetime.timedelta(0,int(form.cleaned_data['length_session3'])) session = sessions[2] session.requested_duration = duration - session_save(session) + session.save() + session_changed(session) if 'attendees' in form.changed_data: @@ -495,7 +516,28 @@ def main(request): meeting = get_meeting() - scheduled_groups, unscheduled_groups = groups_by_session(request.user, meeting) + scheduled_groups = [] + unscheduled_groups = [] + + group_types = GroupFeatures.objects.filter(has_meetings=True).values_list('type', flat=True) + + my_groups = [g for g in get_my_groups(request.user, conclude=True) if g.type_id in group_types] + + sessions_by_group = defaultdict(list) + for s in add_event_info_to_session_qs(Session.objects.filter(meeting=meeting, group__in=my_groups)).filter(current_status__in=['schedw', 'apprw', 'appr', 'sched']): + sessions_by_group[s.group_id].append(s) + + for group in my_groups: + group.meeting_sessions = sessions_by_group.get(group.pk, []) + + if group.pk in sessions_by_group: + # include even if concluded as we need to to see that the + # sessions are there + scheduled_groups.append(group) + else: + if group.state_id not in ['conclude', 'bof-conc']: + # too late for unscheduled if concluded + unscheduled_groups.append(group) # warn if there are no associated groups if not scheduled_groups and not unscheduled_groups: @@ -503,15 +545,14 @@ def main(request): # add session status messages for use in template for group in scheduled_groups: - sessions = group.session_set.filter(meeting=meeting) - if sessions.count() < 3: - group.status_message = sessions[0].status + if len(group.meeting_sessions) < 3: + group.status_message = group.meeting_sessions[0].current_status else: - group.status_message = 'First two sessions: %s, Third session: %s' % (sessions[0].status,sessions[2].status) + group.status_message = 'First two sessions: %s, Third session: %s' % (group.meeting_sessions[0].current_status, group.meeting_sessions[2].current_status) # add not meeting indicators for use in template for group in unscheduled_groups: - if group.session_set.filter(meeting=meeting,status='notmeet'): + if any(s.current_status == 'notmeet' for s in group.meeting_sessions): group.not_meeting = True return render(request, 'sreq/main.html', { @@ -550,7 +591,7 @@ def new(request, acronym): # pre-populated with data from last meeeting's session request elif request.method == 'GET' and 'previous' in request.GET: previous_meeting = Meeting.objects.get(number=str(int(meeting.number) - 1)) - previous_sessions = Session.objects.filter(meeting=previous_meeting,group=group).exclude(status__in=('notmeet','deleted')).order_by('id') + previous_sessions = add_event_info_to_session_qs(Session.objects.filter(meeting=previous_meeting, group=group)).filter(current_status__in=['notmeet', 'deleted']).order_by('id') if not previous_sessions: messages.warning(request, 'This group did not meet at %s' % previous_meeting) return redirect('ietf.secr.sreq.views.new', acronym=acronym) @@ -586,22 +627,25 @@ def no_session(request, acronym): login = request.user.person # delete canceled record if there is one - Session.objects.filter(group=group,meeting=meeting,status='canceled').delete() + add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status='canceled').delete() # skip if state is already notmeet - if Session.objects.filter(group=group,meeting=meeting,status='notmeet'): + if add_event_info_to_session_qs(Session.objects.filter(group=group, meeting=meeting)).filter(current_status='notmeet'): messages.info(request, 'The group %s is already marked as not meeting' % group.acronym) return redirect('ietf.secr.sreq.views.main') - session = Session(group=group, - meeting=meeting, - requested=datetime.datetime.now(), - requested_by=login, - requested_duration=datetime.timedelta(0), - status=SessionStatusName.objects.get(slug='notmeet'), - type_id='session', - ) - session_save(session) + session = Session.objects.create( + group=group, + meeting=meeting, + requested_duration=datetime.timedelta(0), + type_id='session', + ) + SchedulingEvent.objects.create( + session=session, + status=SessionStatusName.objects.get(slug='notmeet'), + by=login, + ) + session_changed(session) # send notification (to_email, cc_list) = gather_address_lists('session_request_not_meeting',group=group,person=login) @@ -669,7 +713,7 @@ def view(request, acronym, num = None): ''' meeting = get_meeting(num) group = get_object_or_404(Group, acronym=acronym) - sessions = Session.objects.filter(~Q(status__in=('canceled','notmeet','deleted')),meeting=meeting,group=group).order_by('id') + sessions = add_event_info_to_session_qs(Session.objects.filter(meeting=meeting, group=group)).filter(Q(current_status__isnull=True) | ~Q(current_status__in=('canceled','notmeet','deleted'))).order_by('id') # check if app is locked is_locked = check_app_locked() @@ -683,16 +727,12 @@ def view(request, acronym, num = None): else: return redirect('ietf.secr.sreq.views.new', acronym=acronym) - # TODO simulate activity records - activities = [{'act_date':sessions[0].requested.strftime('%b %d, %Y'), - 'act_time':sessions[0].requested.strftime('%H:%M:%S'), - 'activity':'New session was requested', - 'act_by':sessions[0].requested_by}] - if sessions[0].scheduled: - activities.append({'act_date':sessions[0].scheduled.strftime('%b %d, %Y'), - 'act_time':sessions[0].scheduled.strftime('%H:%M:%S'), - 'activity':'Session was scheduled', - 'act_by':'Secretariat'}) + activities = [{ + 'act_date': e.time.strftime('%b %d, %Y'), + 'act_time': e.time.strftime('%H:%M:%S'), + 'activity': e.status.name, + 'act_by': e.by, + } for e in sessions[0].schedulingevent_set.select_related('status', 'by')] # other groups that list this group in their conflicts session_conflicts = session_conflicts_as_string(group, meeting) @@ -700,7 +740,7 @@ def view(request, acronym, num = None): # if sessions include a 3rd session waiting approval and the user is a secretariat or AD of the group # display approve button - if sessions.filter(status='apprw'): + if any(s.current_status == 'apprw' for s in sessions): if has_role(request.user,'Secretariat') or group.parent.role_set.filter(name='ad',person=request.user.person): show_approve_button = True diff --git a/ietf/secr/templates/meetings/non_session.html b/ietf/secr/templates/meetings/non_session.html index adf0ce8c7..bf5418c8c 100644 --- a/ietf/secr/templates/meetings/non_session.html +++ b/ietf/secr/templates/meetings/non_session.html @@ -24,7 +24,7 @@ {% for assignment in assignments %} - + {{ assignment.timeslot.time|date:"D" }} {{ assignment.timeslot.time|date:"H:i" }}-{{ assignment.timeslot.end_time|date:"H:i" }} {{ assignment.timeslot.name }} diff --git a/ietf/secr/templates/meetings/session_edit.html b/ietf/secr/templates/meetings/session_edit.html index 8837448d8..7d9cd6025 100644 --- a/ietf/secr/templates/meetings/session_edit.html +++ b/ietf/secr/templates/meetings/session_edit.html @@ -23,19 +23,19 @@ Day: - {{ timeslot.time|date:"l" }} + {% if timeslot %}{{ timeslot.time|date:"l" }}{% endif %} Time: - {{ timeslot.time|time:"H:i" }} + {% if timeslot %}{{ timeslot.time|time:"H:i" }}{% endif %} Room: - {{ timeslot.location.name }} + {% if timeslot %}{{ timeslot.location.name }}{% endif %} Status: - {{ session.status }} + {{ current_status_name }} {{ form }} diff --git a/ietf/secr/templates/meetings/sessions.html b/ietf/secr/templates/meetings/sessions.html index ecd57c8c9..a6de5fa08 100644 --- a/ietf/secr/templates/meetings/sessions.html +++ b/ietf/secr/templates/meetings/sessions.html @@ -33,7 +33,7 @@ {% endif %} {{ session.agenda_note }} - {{ session.status }} + {{ session.current_status_name }} Edit
        diff --git a/ietf/secr/templates/sreq/view.html b/ietf/secr/templates/sreq/view.html index b5b116dbd..838b1f99e 100644 --- a/ietf/secr/templates/sreq/view.html +++ b/ietf/secr/templates/sreq/view.html @@ -29,7 +29,7 @@
          -
        • +
        • {% if show_approve_button %}
        • {% endif %} diff --git a/ietf/secr/utils/group.py b/ietf/secr/utils/group.py index 6fe69a56b..3895367ec 100644 --- a/ietf/secr/utils/group.py +++ b/ietf/secr/utils/group.py @@ -13,8 +13,7 @@ from django.conf import settings from django.core.exceptions import ObjectDoesNotExist # Datatracker imports -from ietf.group.models import Group, GroupFeatures -from ietf.meeting.models import Session +from ietf.group.models import Group from ietf.ietfauth.utils import has_role @@ -75,35 +74,3 @@ def get_my_groups(user,conclude=False): continue return list(my_groups) - -def groups_by_session(user, meeting, types=None): - ''' - Takes a Django User object, Meeting object and optionally string of meeting types to - include. Returns a tuple scheduled_groups, unscheduled groups. sorted lists of those - groups that the user has access to, secretariat defaults to all groups - If user=None than all groups are returned. - - For groups with a session, we must include "concluded" groups because we still want to know - who had a session at a particular meeting even if they are concluded after. This is not true - for groups without a session because this function is often used to build select lists (ie. - Session Request Tool) and you don't want concluded groups appearing as options. - ''' - groups_session = [] - groups_no_session = [] - my_groups = get_my_groups(user,conclude=True) - sessions = Session.objects.filter(meeting=meeting,status__in=('schedw','apprw','appr','sched')) - groups_with_sessions = [ s.group for s in sessions ] - for group in my_groups: - if group in groups_with_sessions: - groups_session.append(group) - else: - if group.state_id not in ('conclude','bof-conc'): - groups_no_session.append(group) - - if not types: - types = GroupFeatures.objects.filter(has_meetings=True).values_list('type', flat=True) - - groups_session = [x for x in groups_session if x.type_id in types] - groups_no_session = [x for x in groups_no_session if x.type_id in types] - - return groups_session, groups_no_session diff --git a/ietf/settings.py b/ietf/settings.py index d9faba0d1..fb74794d4 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -377,7 +377,7 @@ MIDDLEWARE = [ 'django_referrer_policy.middleware.ReferrerPolicyMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', - 'csp.middleware.CSPMiddleware', + # 'csp.middleware.CSPMiddleware', 'ietf.middleware.unicode_nfkc_normalization_middleware', ] diff --git a/ietf/templates/doc/material/presentations-row.html b/ietf/templates/doc/material/presentations-row.html index 0516a3558..e16b04815 100644 --- a/ietf/templates/doc/material/presentations-row.html +++ b/ietf/templates/doc/material/presentations-row.html @@ -21,10 +21,10 @@ {% ifchanged s.meeting %}{% if s.meeting.type.slug == 'ietf' %}IETF{% endif %}{{s.meeting.number}}{% endifchanged %} {% if s.name %}{{ s.name }}
          {% else %}{{ s.group.acronym }} - {% endif %} - {% if s.status.slug == "sched" %} + {% if s.current_status == "sched" %} {% if s.meeting.type.slug == 'ietf' %}{{s.time|date:"D M d, Y Hi"}}{% else %}{{s.time|date:"D M d, Y"}}{% endif %} {% else %} - {{s.status}} + {{s.current_status_name}} {% endif %} {% if s.agenda %}Agenda{% endif %} diff --git a/ietf/templates/meeting/agenda.html b/ietf/templates/meeting/agenda.html index 1dbc3cea1..b2f11286d 100644 --- a/ietf/templates/meeting/agenda.html +++ b/ietf/templates/meeting/agenda.html @@ -223,7 +223,7 @@ {{item.timeslot.name}} {% endif %} - {% if item.session.status.slug == 'canceled' %} + {% if item.session.current_status == 'canceled' %} CANCELLED {% endif %} @@ -316,7 +316,7 @@ BOF {% endif %} - {% if item.session.status.slug == 'canceled' %} + {% if item.session.current_status == 'canceled' %} CANCELLED {% endif %} diff --git a/ietf/templates/meeting/agenda.txt b/ietf/templates/meeting/agenda.txt index 96cf3dd20..241b87711 100644 --- a/ietf/templates/meeting/agenda.txt +++ b/ietf/templates/meeting/agenda.txt @@ -22,7 +22,7 @@ {% endif %}{% if item.timeslot.type.slug == "session" %}{% 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.status.slug == '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 ***{% 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 %} diff --git a/ietf/templates/meeting/interim_announce.html b/ietf/templates/meeting/interim_announce.html index b9b8ee5f4..280e11cfa 100644 --- a/ietf/templates/meeting/interim_announce.html +++ b/ietf/templates/meeting/interim_announce.html @@ -35,17 +35,9 @@ {% for meeting in meetings %} - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} + {{ meeting.date }} - {% if meeting.type.slug == 'interim' %} - {{ meeting.session_set.all.0.group.acronym }} - {% else %} - ietf - {% endif %} + {{ meeting.responsible_group.acronym }} {{ meeting.number }} diff --git a/ietf/templates/meeting/interim_pending.html b/ietf/templates/meeting/interim_pending.html index a4e003da7..0bba822cf 100644 --- a/ietf/templates/meeting/interim_pending.html +++ b/ietf/templates/meeting/interim_pending.html @@ -14,54 +14,46 @@ {% origin %}

          Pending Interim Meetings

          - {% if menu_entries %} - - {% endif %} + {% if menu_entries %} + + {% endif %} - {% if meetings %} - - - - - - - - - - - {% for meeting in meetings %} - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} - - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} - + + + {% endfor %} + +
          DateGroupName
          {{ meeting.date }}{{ meeting.session_set.all.0.group.acronym }}ietf - {% if meeting.type.slug == "interim" %} - {{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %} -- CANCELLED --{% endif %} + {% if meetings %} + + + + + + + + + + + {% for meeting in meetings %} + + + + - - - {% endfor %} - -
          DateGroupName
          {{ meeting.date }}{{ meeting.responsible_group.acronym }} + {% if meeting.type_id == "interim" %} + {{ meeting.number }}{% if meeting.interim_meeting_cancelled %}  CANCELLED{% endif %} {% else %} IETF - {{ meeting.number }} {% endif %} - {% if meeting.can_approve %}can be approved{% endif %}
          - {% else %} -

          No pending interim meetings

          - {% endif %} +
          {% if meeting.can_approve %}can be approved{% endif %}
          + {% else %} +

          No pending interim meetings

          + {% endif %} {% endblock %} diff --git a/ietf/templates/meeting/interim_request_cancel.html b/ietf/templates/meeting/interim_request_cancel.html index 908a0cf7e..da93af2d9 100644 --- a/ietf/templates/meeting/interim_request_cancel.html +++ b/ietf/templates/meeting/interim_request_cancel.html @@ -3,7 +3,7 @@ {% load origin %} {% load staticfiles bootstrap3 widget_tweaks %} -{% block title %}Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}{% endblock %} + {% block pagehead %} @@ -13,7 +13,7 @@ {% block content %} {% origin %} -

          Cancel Interim Meeting {% if meeting.session_set.first.status.slug != "sched" %}Request{% endif %}

          +

          {% block title %}Cancel Interim Meeting {% if session_status != "sched" %}Request{% endif %}{% endblock %}

          {% csrf_token %} diff --git a/ietf/templates/meeting/interim_request_details.html b/ietf/templates/meeting/interim_request_details.html index 8e8ed82f9..9d3aaf0ac 100644 --- a/ietf/templates/meeting/interim_request_details.html +++ b/ietf/templates/meeting/interim_request_details.html @@ -15,11 +15,11 @@

          Interim Meeting Request Details

          Group
          -
          {{ sessions.0.group.acronym }} +
          {{ group.acronym }}
          Requested By
          -
          {{ sessions.0.requested_by }} +
          {{ requester }}
          Status
          -
          {{ sessions.0.status }}
          +
          {{ session_status.name }}
          City
          {{ meeting.city }}
          Country
          @@ -46,24 +46,24 @@ {% if can_edit %} Edit {% endif %} - {% if can_approve and sessions.0.status.slug == 'apprw' %} + {% if can_approve and session_status.slug == 'apprw' %} {% endif %} - {% if user|has_role:"Secretariat" and sessions.0.status.slug == 'scheda' %} + {% if user|has_role:"Secretariat" and session_status.slug == 'scheda' %} Announce Skip Announcement {% endif %} {% if can_edit %} - {% if sessions.0.status.slug == 'apprw' or sessions.0.status.slug == 'scheda' or sessions.0.status.slug == 'sched' %} + {% if session_status.slug == 'apprw' or session_status.slug == 'scheda' or session_status.slug == 'sched' %} Cancel Meeting {% endif %} {% endif %} - {% if sessions.0.status.slug == "apprw" %} + {% if session_status.slug == "apprw" %} Back - {% elif sessions.0.status.slug == "scheda" %} + {% elif session_status.slug == "scheda" %} Back - {% elif sessions.0.status.slug == "sched" %} + {% elif session_status.slug == "sched" %} Back {% else %} Back diff --git a/ietf/templates/meeting/past.html b/ietf/templates/meeting/past.html index c0e88174b..00d8d3918 100644 --- a/ietf/templates/meeting/past.html +++ b/ietf/templates/meeting/past.html @@ -5,7 +5,7 @@ {% load ietf_filters staticfiles %} {% block pagehead %} - + {% endblock %} {% block bodyAttrs %}data-spy="scroll" data-target="#affix"{% endblock %} @@ -75,47 +75,43 @@ {% else %}
          No past meetings are available.
          {% endif %} - +
      • - {% if meetings %} -

        - - - - - - - - - - {% for meeting in meetings %} - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} - - {% if meeting.type.slug == 'interim' %} - + {% if meetings %} +

        +
        DateGroupName
        {{ meeting.date }} - {{ meeting.session_set.all.0.group.acronym }} -
        + + + + + + + + + {% for meeting in meetings %} + + + + {{ meeting.responsible_group.acronym }} {% endif %} - + + - - + {% endifchanged %} @@ -73,7 +73,7 @@ {% for meeting in meetings %} - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} + - {% if meeting.type.slug == 'interim' %} - - {% else %} - - {% endif %} +
        DateGroupName
        {{ meeting.date }} + {% if meeting.responsible_group.type_id != 'ietf' %} + {{ meeting.responsible_group.acronym }} {% else %} - ietf - {% if meeting.type.slug == "interim" %} - {{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %}  CANCELLED{% endif %} + + {% if meeting.type_id == "interim" %} + {{ meeting.number }}{% if meeting.interim_meeting_cancelled %}  CANCELLED{% endif %} {% else %} IETF - {{ meeting.number }} {% endif %} - - {% if meeting.type.slug == "interim" %} + + {% if meeting.type_id == "interim" %} {% else %} {% if meeting.get_number > 97 %} Important dates diff --git a/ietf/templates/meeting/proceedings.html b/ietf/templates/meeting/proceedings.html index 10260515a..4700aae74 100644 --- a/ietf/templates/meeting/proceedings.html +++ b/ietf/templates/meeting/proceedings.html @@ -68,60 +68,58 @@ {% endif %} - {% regroup ietf|dictsort:"group.parent.acronym" by group.parent.name as areas %} - {% for sessions in areas %} -

        {{sessions.list.0.group.parent.acronym|upper}} {{ sessions.grouper }}

        - {% regroup sessions.list by not_meeting as meet_or_not %} - {% for batch in meet_or_not %} - {% if not batch.grouper %} - - - - - - - - - - - - {% for session in batch.list|dictsort:"group.acronym" %} - {% ifchanged session.group.acronym %} - {% include "meeting/group_proceedings.html" %} - {% endifchanged %} - {% endfor %} - -
        GroupArtifactsRecordingsSlidesDrafts
        - {% else %} -

        {{sessions.grouper }} groups not meeting: - {% for session in batch.list|dictsort:"group.acronym" %} - {% ifchanged session.group.acronym %} - {{session.group.acronym}}{% if not forloop.last %},{% endif %} - {% endifchanged %} - {% endfor %} -

        - - - - - - - - - - - - {% for session in batch.list|dictsort:"group.acronym" %} - {% ifchanged session.group.acronym %} - {% if session.sessionpresentation_set.exists %} - {% include "meeting/group_proceedings.html" %} - {% endif %} - {% endifchanged %} - {% endfor %} - -
             
        - {% endif %} - {% endfor %} + {% for area, meeting_sessions, not_meeting_sessions in ietf_areas %} +

        {{ area.acronym|upper }} {{ area.name }}

        + {% if meeting_sessions %} + + + + + + + + + + + + {% for session in meeting_sessions %} + {% ifchanged session.group.acronym %} + {% include "meeting/group_proceedings.html" %} + {% endifchanged %} + {% endfor %} + +
        GroupArtifactsRecordingsSlidesDrafts
        + {% endif %} + + {% if not_meeting_sessions %} +

        {{ area.name }} groups not meeting: + {% for session in not_meeting_sessions %} + {% ifchanged session.group.acronym %} + {{ session.group.acronym }}{% if not forloop.last %},{% endif %} + {% endifchanged %} + {% endfor %} +

        + + + + + + + + + + + + {% for session in not_meeting_sessions %} + {% ifchanged session.group.acronym %} + {% if session.sessionpresentation_set.exists %} + {% include "meeting/group_proceedings.html" %} + {% endif %} + {% endifchanged %} + {% endfor %} + +
             
        + {% endif %} {% endfor %} diff --git a/ietf/templates/meeting/requests.html b/ietf/templates/meeting/requests.html index a37ce500e..de5e4a28a 100644 --- a/ietf/templates/meeting/requests.html +++ b/ietf/templates/meeting/requests.html @@ -52,14 +52,14 @@ {% ifchanged %}
        {{session.status|capfirst}}
        {{session.current_status_name|capfirst}}
        - + {{session.group.acronym}} {{session.attendees|default:""}} - {{session.requested_by}} + {{session.requested_by_person}} diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index fcbbf0a3c..63fa639a8 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -9,9 +9,9 @@ {% if session.agenda_note %}

        {{session.agenda_note}}

        {% endif %} {% if can_manage_materials %} - {% if session.status.slug == 'sched' or session.status.slug == 'schedw' %} + {% if session.current_status == 'sched' or session.current_status == 'schedw' %}
        - {% if meeting.type.slug == 'interim' and user|has_role:"Secretariat" %} + {% if meeting.type_id == 'interim' and user|has_role:"Secretariat" %} Meeting Details {% endif %}
        diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html index 3a6618ad5..c280030a1 100644 --- a/ietf/templates/meeting/upcoming.html +++ b/ietf/templates/meeting/upcoming.html @@ -114,20 +114,12 @@
        {{ meeting.date }}{{ meeting.session_set.all.0.group.acronym }}ietf{{ meeting.responsible_group.acronym }} - {% if meeting.type.slug == "interim" %} - {{ meeting.number }}{% if meeting.session_set.all.0.status.slug == "canceled" %}  CANCELLED{% endif %} + {% if meeting.type_id == "interim" %} + {{ meeting.number }}{% if meeting.interim_meeting_cancelled %}  CANCELLED{% endif %} {% else %} IETF - {{ meeting.number }} {% endif %} From defc50caf54a624962124b0ee9e7a1650cba341f Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 5 Dec 2019 18:11:24 +0000 Subject: [PATCH 3/9] Fix bug in materials page introduced in previous commit with non-WG groups - Legacy-Id: 17124 --- ietf/meeting/models.py | 4 ++++ ietf/meeting/views.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index ef9b58574..c962438fb 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -940,6 +940,10 @@ class Session(models.Model): def drafts(self): return list(self.materials.filter(type='draft')) + # The utilities below are used in the proceedings and materials + # templates, and should be moved there - then we could also query + # out the needed information in a few passes and speed up those + # pages. def all_meeting_sessions_for_group(self): from ietf.meeting.utils import add_event_info_to_session_qs if self.group.type_id in ['wg','rg','ag']: diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index b1278ecf8..8f0bf5f67 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -138,10 +138,10 @@ def materials(request, num=None): schedule = get_schedule(meeting, None) - sessions = Session.objects.filter( + sessions = add_event_info_to_session_qs(Session.objects.filter( meeting__number=meeting.number, timeslotassignments__schedule=schedule - ).distinct().select_related('meeting__schedule', 'group__state', 'group__parent') + ).distinct().select_related('meeting__schedule', 'group__state', 'group__parent')) plenaries = sessions.filter(name__icontains='plenary') ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu') From 6920c179cd0e2f0e8b8fc82a0934a19db737fe6b Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 6 Dec 2019 19:56:03 +0000 Subject: [PATCH 4/9] Fix problem with missing names - Legacy-Id: 17127 --- ietf/meeting/tests_js.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ietf/meeting/tests_js.py b/ietf/meeting/tests_js.py index 50ec81424..bf756ddcd 100644 --- a/ietf/meeting/tests_js.py +++ b/ietf/meeting/tests_js.py @@ -20,6 +20,7 @@ from ietf.group import colors from ietf.meeting.factories import SessionFactory from ietf.meeting.test_data import make_meeting_test_data from ietf.meeting.models import SchedTimeSessAssignment +from ietf.name.models import SessionStatusName from ietf.utils.test_runner import set_coverage_checking from ietf.utils.pipe import pipe from ietf import settings @@ -118,7 +119,11 @@ class SlideReorderTests(StaticLiveServerTestCase): def setUp(self): self.driver = webdriver.PhantomJS(port=0, service_log_path=settings.TEST_GHOSTDRIVER_LOG_PATH) self.driver.set_window_size(1024,768) - self.session = SessionFactory(meeting__type_id='ietf') + # this is a temporary fix - we should have these name in the + # database already at this point + SessionStatusName.objects.get_or_create(slug='schedw') + SessionStatusName.objects.get_or_create(slug='sched') + self.session = SessionFactory(meeting__type_id='ietf', status_id='sched') self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='one'),order=1) self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='two'),order=2) self.session.sessionpresentation_set.create(document=DocumentFactory(type_id='slides',name='three'),order=3) From 568670c06092d70cc708489902b3a023ddac757c Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Fri, 6 Dec 2019 20:02:26 +0000 Subject: [PATCH 5/9] Turn sessions into regular sessions and non-sessions into misc. sessions in the UI and code to avoid ambiguity. This doesn't change the data in the DB except for uses of TimeSlotTypeName where 'session' is now 'regular'. - Legacy-Id: 17128 --- ietf/doc/tests.py | 2 +- ietf/doc/tests_material.py | 2 +- ietf/doc/views_doc.py | 2 +- ietf/group/views.py | 2 +- ietf/meeting/ajax.py | 8 +- ietf/meeting/factories.py | 4 +- ietf/meeting/forms.py | 2 +- ietf/meeting/helpers.py | 10 +- .../0025_rename_type_session_to_regular.py | 38 ++++++++ ietf/meeting/models.py | 2 +- ietf/meeting/test_data.py | 14 +-- ietf/meeting/tests_api.py | 16 ++-- ietf/meeting/tests_views.py | 4 +- ietf/meeting/utils.py | 4 +- ietf/meeting/views.py | 38 ++++---- ietf/name/fixtures/names.json | 2 +- ietf/name/models.py | 2 +- ietf/secr/meetings/forms.py | 12 +-- ietf/secr/meetings/tests.py | 46 +++++----- ietf/secr/meetings/urls.py | 13 +-- ietf/secr/meetings/views.py | 91 ++++++++++--------- ietf/secr/proceedings/forms.py | 2 +- ietf/secr/proceedings/views.py | 2 +- ietf/secr/sreq/views.py | 6 +- ietf/secr/static/secr/css/custom.css | 2 +- .../templates/meetings/base_rooms_times.html | 4 +- ...ssion_edit.html => misc_session_edit.html} | 2 +- .../{non_session.html => misc_sessions.html} | 8 +- ...on_edit.html => regular_session_edit.html} | 2 +- ietf/secr/templates/meetings/sessions.html | 2 +- ietf/static/ietf/js/agenda/agenda_helpers.js | 2 +- ietf/static/ietf/js/agenda/timeslot_edit.js | 6 +- ietf/templates/meeting/agenda.html | 4 +- ietf/templates/meeting/agenda.txt | 2 +- ietf/templates/meeting/agenda_by_type.html | 2 +- ietf/templates/meeting/group_materials.html | 2 +- 36 files changed, 202 insertions(+), 160 deletions(-) create mode 100644 ietf/meeting/migrations/0025_rename_type_session_to_regular.py rename ietf/secr/templates/meetings/{non_session_edit.html => misc_session_edit.html} (84%) rename ietf/secr/templates/meetings/{non_session.html => misc_sessions.html} (78%) rename ietf/secr/templates/meetings/{session_edit.html => regular_session_edit.html} (93%) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index 0f5a29736..7005cd6ba 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -713,7 +713,7 @@ class DocTestCase(TestCase): meeting = Meeting.objects.get(number='72'), group = Group.objects.get(acronym='mars'), modified = datetime.datetime.now(), - type_id = "session", + type_id = 'regular', ) SchedulingEvent.objects.create( session=session, diff --git a/ietf/doc/tests_material.py b/ietf/doc/tests_material.py index c67525cdd..4c1d1978a 100644 --- a/ietf/doc/tests_material.py +++ b/ietf/doc/tests_material.py @@ -159,7 +159,7 @@ class GroupMaterialTests(TestCase): meeting = Meeting.objects.get(number='42'), group = Group.objects.get(acronym='mars'), modified = datetime.datetime.now(), - type_id="session", + type_id='regular', ) SchedulingEvent.objects.create( session=session, diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index f14b963c0..6578d1571 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -1346,7 +1346,7 @@ def all_presentations(request, name): doc = get_object_or_404(Document, name=name) sessions = add_event_info_to_session_qs( - doc.session_set.filter(type__in=['session','plenary','other']) + doc.session_set.filter(type__in=['regular','plenary','other']) ).filter(current_status__in=['sched','schedw','appr','canceled']) status_names = {n.slug: n.name for n in SessionStatusName.objects.all()} diff --git a/ietf/group/views.py b/ietf/group/views.py index a93d71930..cba3df21e 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -753,7 +753,7 @@ def meetings(request, acronym=None, group_type=None): sessions = add_event_info_to_session_qs( group.session_set.filter( meeting__date__gt=four_years_ago, - type__in=['session','plenary','other'] + type__in=['regular','plenary','other'] ) ).filter( current_status__in=['sched','schedw','appr','canceled'], diff --git a/ietf/meeting/ajax.py b/ietf/meeting/ajax.py index 1ad694423..44e838deb 100644 --- a/ietf/meeting/ajax.py +++ b/ietf/meeting/ajax.py @@ -161,8 +161,8 @@ AddSlotForm = modelform_factory(TimeSlot, exclude=('meeting','name','location',' # no authorization required to list. def timeslot_slotlist(request, mtg): slots = mtg.timeslot_set.all() - # Restrict graphical editing to slots of type 'session' for now - slots = slots.filter(type__slug='session') + # Restrict graphical editing to slots of type 'regular' for now + slots = slots.filter(type__slug='regular') json_array=[] for slot in slots: json_array.append(slot.json_dict(request.build_absolute_uri('/'))) @@ -209,14 +209,14 @@ def timeslot_updslot(request, meeting, slotid): slot.save() # WORKAROUND: Right now, if there are sessions scheduled in this timeslot - # when it is marked unavailable (or any other value besides "session") they + # when it is marked unavailable (or any other value besides 'regular') they # become unreachable from the editing screen. The session is listed in the # "unscheduled" block incorrectly, and drag-dropping it onto the a new # timeslot produces erroneous results. To avoid this, we will silently # unschedule any sessions in the timeslot that has just been made # unavailable. - if slot.type_id != 'session': + if slot.type_id != 'regular': slot.sessionassignments.all().delete() # ENDWORKAROUND diff --git a/ietf/meeting/factories.py b/ietf/meeting/factories.py index e72303151..20bbf8530 100644 --- a/ietf/meeting/factories.py +++ b/ietf/meeting/factories.py @@ -82,7 +82,7 @@ class SessionFactory(factory.DjangoModelFactory): model = Session meeting = factory.SubFactory(MeetingFactory) - type_id='session' + type_id='regular' group = factory.SubFactory(GroupFactory) @factory.post_generation @@ -139,7 +139,7 @@ class TimeSlotFactory(factory.DjangoModelFactory): model = TimeSlot meeting = factory.SubFactory(MeetingFactory) - type_id = 'session' + type_id = 'regular' @factory.post_generation def location(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py index c30e72560..9028a3eaa 100644 --- a/ietf/meeting/forms.py +++ b/ietf/meeting/forms.py @@ -245,7 +245,7 @@ class InterimSessionModelForm(forms.ModelForm): never gets called""" session = super(InterimSessionModelForm, self).save(commit=kwargs.get('commit', True)) session.group = self.group - session.type_id = 'session' + session.type_id = 'regular' return session def save_agenda(self): diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 0206ac355..a329ac371 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -73,7 +73,7 @@ def get_areas(): # get list of areas that are referenced. def get_area_list_from_sessions(assignments, num): - return assignments.filter(timeslot__type = 'Session', + return assignments.filter(timeslot__type = 'regular', session__group__parent__isnull = False).order_by( 'session__group__parent__acronym').distinct().values_list( 'session__group__parent__acronym',flat=True) @@ -82,7 +82,7 @@ def build_all_agenda_slices(meeting): time_slices = [] date_slices = {} - for ts in meeting.timeslot_set.filter(type__in=['session',]).order_by('time','name'): + for ts in meeting.timeslot_set.filter(type__in=['regular',]).order_by('time','name'): ymd = ts.time.date() if ymd not in date_slices and ts.location != None: @@ -98,7 +98,7 @@ def build_all_agenda_slices(meeting): def get_all_assignments_from_schedule(schedule): ss = schedule.assignments.filter(timeslot__location__isnull = False) - ss = ss.filter(session__type__slug='session') + ss = ss.filter(session__type__slug='regular') ss = ss.order_by('timeslot__time','timeslot__name') return ss @@ -107,7 +107,7 @@ def get_modified_from_assignments(assignments): return assignments.aggregate(Max('timeslot__modified'))['timeslot__modified__max'] def get_wg_name_list(assignments): - return assignments.filter(timeslot__type = 'Session', + return assignments.filter(timeslot__type = 'regular', session__group__isnull = False, session__group__parent__isnull = False).order_by( 'session__group__acronym').distinct().values_list( @@ -631,7 +631,7 @@ def update_interim_session_assignment(form): else: slot = TimeSlot.objects.create( meeting=session.meeting, - type_id="session", + type_id='regular', duration=session.requested_duration, time=time) SchedTimeSessAssignment.objects.create( diff --git a/ietf/meeting/migrations/0025_rename_type_session_to_regular.py b/ietf/meeting/migrations/0025_rename_type_session_to_regular.py new file mode 100644 index 000000000..5b2c04918 --- /dev/null +++ b/ietf/meeting/migrations/0025_rename_type_session_to_regular.py @@ -0,0 +1,38 @@ +# Copyright The IETF Trust 2019, All Rights Reserved +# -*- coding: utf-8 -*- +# Generated by Django 1.11.26 on 2019-12-06 11:13 +from __future__ import unicode_literals + +from django.db import migrations + +def rename_session_to_request(apps, schema_editor): + Session = apps.get_model('meeting', 'Session') + TimeSlot = apps.get_model('meeting', 'TimeSlot') + Room = apps.get_model('meeting', 'Room') + TimeSlotTypeName = apps.get_model('name', 'TimeSlotTypeName') + + TimeSlotTypeName.objects.create( + slug='regular', + name='Regular', + used=True, + order=0, + ) + + Session.objects.filter(type='session').update(type='regular') + TimeSlot.objects.filter(type='session').update(type='regular') + Room.session_types.through.objects.filter(timeslottypename='session').update(timeslottypename='regular') + + TimeSlotTypeName.objects.filter(slug='session').delete() + +def noop(apps, schema_editor): + pass + +class Migration(migrations.Migration): + + dependencies = [ + ('meeting', '0024_auto_20191204_1731'), + ] + + operations = [ + migrations.RunPython(rename_session_to_request, noop), + ] diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index c962438fb..b94592a22 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -794,7 +794,7 @@ class SchedTimeSessAssignment(models.Model): components.append(g.acronym) components.append(slugify(self.session.name)) - if self.timeslot.type_id in ('session', 'plenary'): + if self.timeslot.type_id in ('regular', 'plenary'): if self.timeslot.type_id == "plenary": components.append("1plenary") else: diff --git a/ietf/meeting/test_data.py b/ietf/meeting/test_data.py index d8508567a..84a754003 100644 --- a/ietf/meeting/test_data.py +++ b/ietf/meeting/test_data.py @@ -28,11 +28,11 @@ def make_interim_meeting(group,date,status='sched'): attendees=10, requested_duration=datetime.timedelta(minutes=20), remote_instructions='http://webex.com', - type_id="session") + type_id='regular') SchedulingEvent.objects.create(session=session, status_id=status, by=system_person) slot = TimeSlot.objects.create( meeting=meeting, - type_id="session", + type_id='regular', duration=session.requested_duration, time=time) SchedTimeSessAssignment.objects.create( @@ -85,7 +85,7 @@ def make_meeting_test_data(meeting=None): pname = RoomResourceName.objects.create(name='projector',slug='proj') projector = ResourceAssociation.objects.create(name=pname,icon="notfound.png",desc="Basic projector") room = Room.objects.create(meeting=meeting, name="Test Room", capacity=123, functional_name="Testing Ground") - room.session_types.add("session") + room.session_types.add('regular') room.resources.add(projector) asname = RoomResourceName.objects.get(slug='audiostream') UrlResource.objects.create(name=asname, room=room, url='http://ietf{number}streaming.dnsalias.net/ietf/ietf{number}1.m3u'.format(number=meeting.number)) @@ -100,10 +100,10 @@ def make_meeting_test_data(meeting=None): # slots session_date = meeting.date + datetime.timedelta(days=1) - slot1 = TimeSlot.objects.create(meeting=meeting, type_id="session", location=room, + slot1 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=30), time=datetime.datetime.combine(session_date, datetime.time(9, 30))) - slot2 = TimeSlot.objects.create(meeting=meeting, type_id="session", location=room, + slot2 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room, duration=datetime.timedelta(minutes=30), time=datetime.datetime.combine(session_date, datetime.time(10, 30))) breakfast_slot = TimeSlot.objects.create(meeting=meeting, type_id="lead", location=breakfast_room, @@ -119,7 +119,7 @@ def make_meeting_test_data(meeting=None): mars = Group.objects.get(acronym='mars') mars_session = Session.objects.create(meeting=meeting, group=mars, attendees=10, requested_duration=datetime.timedelta(minutes=20), - type_id="session") + type_id='regular') SchedulingEvent.objects.create(session=mars_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=slot1, session=mars_session, schedule=schedule) SchedTimeSessAssignment.objects.create(timeslot=slot2, session=mars_session, schedule=unofficial_schedule) @@ -128,7 +128,7 @@ def make_meeting_test_data(meeting=None): ames_session = Session.objects.create(meeting=meeting, group=Group.objects.get(acronym="ames"), attendees=10, requested_duration=datetime.timedelta(minutes=20), - type_id="session") + type_id='regular') SchedulingEvent.objects.create(session=ames_session, status_id='schedw', by=system_person) SchedTimeSessAssignment.objects.create(timeslot=slot2, session=ames_session, schedule=schedule) SchedTimeSessAssignment.objects.create(timeslot=slot1, session=ames_session, schedule=unofficial_schedule) diff --git a/ietf/meeting/tests_api.py b/ietf/meeting/tests_api.py index 06f267da3..5840e5ea7 100644 --- a/ietf/meeting/tests_api.py +++ b/ietf/meeting/tests_api.py @@ -143,10 +143,10 @@ class ApiTests(TestCase): def test_create_new_room(self): meeting = make_meeting_test_data() - timeslots_before = meeting.timeslot_set.filter(type='session').count() + timeslots_before = meeting.timeslot_set.filter(type='regular').count() url = urlreverse("ietf.meeting.ajax.timeslot_roomsurl", kwargs=dict(num=meeting.number)) - post_data = { "name": "new room", "capacity": "50" , "resources": [], "session_types":["session"]} + post_data = { "name": "new room", "capacity": "50" , "resources": [], "session_types":['regular']} # unauthorized post r = self.client.post(url, post_data) @@ -159,7 +159,7 @@ class ApiTests(TestCase): self.assertEqual(r.status_code, 302) self.assertTrue(meeting.room_set.filter(name="new room")) - timeslots_after = meeting.timeslot_set.filter(type='session').count() + timeslots_after = meeting.timeslot_set.filter(type='regular').count() # It's not clear that what that ajax function is doing is the right thing to do, # but it currently makes a new timeslot for any existing timeslot. # The condition tested below relies on the timeslots before this test all having different start and end times @@ -212,7 +212,7 @@ class ApiTests(TestCase): r = self.client.get(url) self.assertEqual(r.status_code, 200) info = r.json() - self.assertEqual(set([x['short_name'] for x in info]),set([s.session.short_name for s in meeting.schedule.assignments.filter(session__type_id='session')])) + self.assertEqual(set([x['short_name'] for x in info]),set([s.session.short_name for s in meeting.schedule.assignments.filter(session__type_id='regular')])) schedule = meeting.schedule url = urlreverse("ietf.meeting.ajax.assignments_json", @@ -466,7 +466,7 @@ class TimeSlotEditingApiTests(TestCase): def test_manipulate_timeslot(self): meeting = make_meeting_test_data() slot = meeting.timeslot_set.all()[0] - self.assertEqual(TimeSlot.objects.get(pk=slot.pk).type.name,'Session') + self.assertEqual(TimeSlot.objects.get(pk=slot.pk).type_id,'regular') url = urlreverse("ietf.meeting.ajax.timeslot_sloturl", kwargs=dict(num=meeting.number, slotid=slot.pk)) @@ -479,10 +479,12 @@ class TimeSlotEditingApiTests(TestCase): self.client.login(username="plain", password="plain+password") r = self.client.post(url, modify_post_data) self.assertEqual(r.status_code, 403) - self.assertEqual(TimeSlot.objects.get(pk=slot.pk).type.name,'Session') + slot.refresh_from_db() + self.assertEqual(slot.type_id, 'regular') # Successful change of purpose self.client.login(username="secretary", password="secretary+password") r = self.client.post(url, modify_post_data) self.assertEqual(r.status_code, 200) - self.assertEqual(TimeSlot.objects.get(pk=slot.pk).type.name,'Plenary') + slot.refresh_from_db() + self.assertEqual(slot.type_id, 'plenary') diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index f27216250..6cc9e54e2 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -241,7 +241,7 @@ class MeetingTests(TestCase): self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room',]])) self.assertNotContains(r, 'IESG Breakfast') - url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,type='session')) + url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,type='regular')) r = self.client.get(url) self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room']])) self.assertFalse(any([x in unicontent(r) for x in ['IESG Breakfast','Breakfast Room']])) @@ -576,7 +576,7 @@ class MeetingTests(TestCase): self.assertEqual(response.get('Content-Type'), 'text/calendar') def test_edit_slide_order(self): - session=SessionFactory(meeting__type_id='iestf',type_id='session') + session=SessionFactory(meeting__type_id='iestf',type_id='regular') slides = DocumentFactory(type_id='slides') session.sessionpresentation_set.create(document=slides,order=0) url = urlreverse('ietf.meeting.views.set_slide_order',kwargs={'session_id':session.id,'num':session.meeting.number,'name':slides.name}) diff --git a/ietf/meeting/utils.py b/ietf/meeting/utils.py index 5b1944fd0..286f043b9 100644 --- a/ietf/meeting/utils.py +++ b/ietf/meeting/utils.py @@ -224,8 +224,8 @@ def add_event_info_to_session_qs(qs, current_status=True, requested_by=False, re def only_sessions_that_can_meet(session_qs): qs = add_event_info_to_session_qs(session_qs).exclude(current_status__in=['notmeet', 'disappr', 'deleted', 'apprw']) - # Restrict graphical scheduling to meeting requests (Sessions) of type 'session' for now - qs = qs.filter(type__slug='session') + # Restrict graphical scheduling to meeting requests (Sessions) of type 'regular' for now + qs = qs.filter(type__slug='regular') return qs diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 8f0bf5f67..bd2f8d108 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -146,11 +146,11 @@ def materials(request, num=None): plenaries = sessions.filter(name__icontains='plenary') ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu') irtf = sessions.filter(group__parent__acronym = 'irtf') - training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['session', 'other', ]) + training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other', ]) iab = sessions.filter(group__parent__acronym = 'iab') session_pks = [s.pk for ss in [plenaries, ietf, irtf, training, iab] for s in ss] - other = sessions.filter(type_id__in=['session'], group__type__features__has_meetings=True).exclude(pk__in=session_pks) + other = sessions.filter(type__in=['regular'], group__type__features__has_meetings=True).exclude(pk__in=session_pks) for topic in [plenaries, ietf, training, irtf, iab]: for event in topic: @@ -372,7 +372,7 @@ def edit_schedule(request, num=None, owner=None, name=None): meeting_base_url = request.build_absolute_uri(meeting.base_url()) site_base_url = request.build_absolute_uri('/')[:-1] # skip the trailing slash - rooms = meeting.room_set.filter(session_types__slug='session').distinct().order_by("capacity") + rooms = meeting.room_set.filter(session_types__slug='regular').distinct().order_by("capacity") saveas = SaveAsForm() saveasurl=reverse(edit_schedule, args=[meeting.number, schedule.owner_email(), schedule.name]) @@ -615,7 +615,7 @@ def agenda_csv(schedule, filtered_assignments): row.append(item.session.pk) row.append(agenda_field(item)) row.append(slides_field(item)) - elif item.timeslot.type_id == "session": + elif item.timeslot.type_id == 'regular': row.append(item.timeslot.name) row.append(item.timeslot.location.name if item.timeslot.location else "") row.append(item.session.historic_group.historic_parent.acronym.upper() if item.session.historic_group.historic_parent else "") @@ -1083,7 +1083,7 @@ def meeting_requests(request, num=None): sessions = add_event_info_to_session_qs( Session.objects.filter( meeting__number=meeting.number, - type__slug='session', + type__slug='regular', group__parent__isnull=False ), requested_by=True, @@ -1110,10 +1110,10 @@ def meeting_requests(request, num=None): def get_sessions(num, acronym): meeting = get_meeting(num=num,type_in=None) - sessions = Session.objects.filter(meeting=meeting,group__acronym=acronym,type__in=['session','plenary','other']) + sessions = Session.objects.filter(meeting=meeting,group__acronym=acronym,type__in=['regular','plenary','other']) if not sessions: - sessions = Session.objects.filter(meeting=meeting,short=acronym,type__in=['session','plenary','other']) + sessions = Session.objects.filter(meeting=meeting,short=acronym,type__in=['regular','plenary','other']) sessions = add_event_info_to_session_qs(sessions) @@ -1322,7 +1322,7 @@ def upload_session_minutes(request, session_id, num): session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) - show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'session' else False + show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'regular' else False if len(sessions) > 1: session_number = 1 + sessions.index(session) @@ -1333,7 +1333,7 @@ def upload_session_minutes(request, session_id, num): if form.is_valid(): file = request.FILES['file'] _, ext = os.path.splitext(file.name) - apply_to_all = session.type_id == 'session' + apply_to_all = session.type_id == 'regular' if show_apply_to_all_checkbox: apply_to_all = form.cleaned_data['apply_to_all'] if minutes_sp: @@ -1422,7 +1422,7 @@ def upload_session_agenda(request, session_id, num): session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) - show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'session' else False + show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'regular' else False if len(sessions) > 1: session_number = 1 + sessions.index(session) @@ -1433,7 +1433,7 @@ def upload_session_agenda(request, session_id, num): if form.is_valid(): file = request.FILES['file'] _, ext = os.path.splitext(file.name) - apply_to_all = session.type_id == 'session' + apply_to_all = session.type_id == 'regular' if show_apply_to_all_checkbox: apply_to_all = form.cleaned_data['apply_to_all'] if agenda_sp: @@ -1495,7 +1495,7 @@ def upload_session_agenda(request, session_id, num): doc.save_with_history([e]) return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym) else: - form = UploadAgendaForm(show_apply_to_all_checkbox, initial={'apply_to_all':session.type_id=='session'}) + form = UploadAgendaForm(show_apply_to_all_checkbox, initial={'apply_to_all':session.type_id=='regular'}) return render(request, "meeting/upload_session_agenda.html", {'session': session, @@ -1533,7 +1533,7 @@ def upload_session_slides(request, session_id, num, name): session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) - show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'session' else False + show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'regular' else False if len(sessions) > 1: session_number = 1 + sessions.index(session) @@ -1550,7 +1550,7 @@ def upload_session_slides(request, session_id, num, name): if form.is_valid(): file = request.FILES['file'] _, ext = os.path.splitext(file.name) - apply_to_all = session.type_id == 'session' + apply_to_all = session.type_id == 'regular' if show_apply_to_all_checkbox: apply_to_all = form.cleaned_data['apply_to_all'] if slides_sp: @@ -1627,7 +1627,7 @@ def propose_session_slides(request, session_id, num): session_number = None sessions = get_sessions(session.meeting.number,session.group.acronym) - show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'session' else False + show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'regular' else False if len(sessions) > 1: session_number = 1 + sessions.index(session) @@ -1637,7 +1637,7 @@ def propose_session_slides(request, session_id, num): if form.is_valid(): file = request.FILES['file'] _, ext = os.path.splitext(file.name) - apply_to_all = session.type_id == 'session' + apply_to_all = session.type_id == 'regular' if show_apply_to_all_checkbox: apply_to_all = form.cleaned_data['apply_to_all'] title = form.cleaned_data['title'] @@ -2244,7 +2244,7 @@ def proceedings(request, num=None): plenaries = sessions.filter(name__icontains='plenary').exclude(current_status='notmeet') ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu') irtf = sessions.filter(group__parent__acronym = 'irtf') - training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['session', 'other', ]).exclude(current_status='notmeet') + training = sessions.filter(group__acronym__in=['edu','iaoc'], type_id__in=['regular', 'other', ]).exclude(current_status='notmeet') iab = sessions.filter(group__parent__acronym = 'iab').exclude(current_status='notmeet') cache_version = Document.objects.filter(session__meeting__number=meeting.number).aggregate(Max('time'))["time__max"] @@ -2529,7 +2529,7 @@ def approve_proposed_slides(request, slidesubmission_id, num): session_number = None sessions = get_sessions(submission.session.meeting.number,submission.session.group.acronym) - show_apply_to_all_checkbox = len(sessions) > 1 if submission.session.type_id == 'session' else False + show_apply_to_all_checkbox = len(sessions) > 1 if submission.session.type_id == 'regular' else False if len(sessions) > 1: session_number = 1 + sessions.index(submission.session) name, _ = os.path.splitext(submission.filename) @@ -2538,7 +2538,7 @@ def approve_proposed_slides(request, slidesubmission_id, num): if request.method == 'POST': form = ApproveSlidesForm(show_apply_to_all_checkbox, request.POST) if form.is_valid(): - apply_to_all = submission.session.type_id == 'session' + apply_to_all = submission.session.type_id == 'regular' if show_apply_to_all_checkbox: apply_to_all = form.cleaned_data['apply_to_all'] if request.POST.get('approve'): diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json index 86592b5f1..4e9b0560f 100644 --- a/ietf/name/fixtures/names.json +++ b/ietf/name/fixtures/names.json @@ -11622,7 +11622,7 @@ "used": true }, "model": "name.timeslottypename", - "pk": "session" + "pk": "regular" }, { "fields": { diff --git a/ietf/name/models.py b/ietf/name/models.py index 453b7042e..f950d71a9 100644 --- a/ietf/name/models.py +++ b/ietf/name/models.py @@ -68,7 +68,7 @@ class AgendaTypeName(NameModel): class SessionStatusName(NameModel): """Waiting for Approval, Approved, Waiting for Scheduling, Scheduled, Cancelled, Disapproved""" class TimeSlotTypeName(NameModel): - """Session, Break, Registration, Other(Non-Session), Reserved, unavail""" + """Session, Break, Registration, Other, Reserved, unavail""" class ConstraintName(NameModel): """Conflict""" penalty = models.IntegerField(default=0, help_text="The penalty for violating this kind of constraint; for instance 10 (small penalty) or 10000 (large penalty)") diff --git a/ietf/secr/meetings/forms.py b/ietf/secr/meetings/forms.py index 242118df4..4aa1dd36b 100644 --- a/ietf/secr/meetings/forms.py +++ b/ietf/secr/meetings/forms.py @@ -49,7 +49,7 @@ def get_times(meeting,day): The label is [start_time]-[end_time]. ''' # pick a random room - rooms = Room.objects.filter(meeting=meeting,session_types='session') + rooms = Room.objects.filter(meeting=meeting,session_types='regular') if rooms: room = rooms[0] else: @@ -148,9 +148,9 @@ class TimeSlotForm(forms.Form): raise forms.ValidationError('{} value has an invalid format. It must be in HH:MM format'.format(duration)) return self.cleaned_data['duration'] -class NonSessionForm(TimeSlotForm): +class MiscSessionForm(TimeSlotForm): short = forms.CharField(max_length=32,label='Short Name',help_text='Enter an abbreviated session name (used for material file names)',required=False) - type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True).exclude(slug__in=('session',)),empty_label=None) + type = forms.ModelChoiceField(queryset=TimeSlotTypeName.objects.filter(used=True).exclude(slug__in=('regular',)),empty_label=None) group = forms.ModelChoiceField( queryset=Group.objects.filter(type__in=['ietf','team'],state='active'), help_text='''Select a group to associate with this session. For example:
        @@ -166,11 +166,11 @@ class NonSessionForm(TimeSlotForm): self.meeting = kwargs.pop('meeting') if 'session' in kwargs: self.session = kwargs.pop('session') - super(NonSessionForm, self).__init__(*args,**kwargs) + super(MiscSessionForm, self).__init__(*args,**kwargs) self.fields['location'].queryset = Room.objects.filter(meeting=self.meeting) def clean(self): - super(NonSessionForm, self).clean() + super(MiscSessionForm, self).clean() if any(self.errors): return cleaned_data = self.cleaned_data @@ -199,7 +199,7 @@ class UploadBlueSheetForm(forms.Form): raise forms.ValidationError('Incorrect filename format') return file -class SessionEditForm(forms.ModelForm): +class RegularSessionEditForm(forms.ModelForm): class Meta: model = Session fields = ['agenda_note'] diff --git a/ietf/secr/meetings/tests.py b/ietf/secr/meetings/tests.py index f826be015..91ad389e2 100644 --- a/ietf/secr/meetings/tests.py +++ b/ietf/secr/meetings/tests.py @@ -231,7 +231,7 @@ class SecrMeetingTestCase(TestCase): def test_meetings_times_delete(self): meeting = make_meeting_test_data() - qs = TimeSlot.objects.filter(meeting=meeting,type='session') + qs = TimeSlot.objects.filter(meeting=meeting,type='regular') before = qs.count() expected_deletion_count = qs.filter(time=qs.first().time).count() url = reverse('ietf.secr.meetings.views.times_delete',kwargs={ @@ -248,12 +248,12 @@ class SecrMeetingTestCase(TestCase): self.assertEqual(response.status_code, 200) response = self.client.post(url, {'post':'yes'}) self.assertRedirects(response, redirect_url) - after = TimeSlot.objects.filter(meeting=meeting,type='session').count() + after = TimeSlot.objects.filter(meeting=meeting,type='regular').count() self.assertEqual(after,before - expected_deletion_count) def test_meetings_times_edit(self): meeting = make_meeting_test_data() - timeslot = TimeSlot.objects.filter(meeting=meeting,type='session').first() + timeslot = TimeSlot.objects.filter(meeting=meeting,type='regular').first() url = reverse('ietf.secr.meetings.views.times_edit',kwargs={ 'meeting_id':72, 'schedule_name':'test-schedule', @@ -269,18 +269,18 @@ class SecrMeetingTestCase(TestCase): self.assertEqual(response.status_code, 302) self.assertTrue(TimeSlot.objects.filter(meeting=meeting,name='Testing')) - def test_meetings_nonsession(self): + def test_meetings_misc_sessions(self): make_meeting_test_data() - url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) + url = reverse('ietf.secr.meetings.views.misc_sessions',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) - def test_meetings_nonsession_add_valid(self): + def test_meetings_misc_session_add_valid(self): meeting = make_meeting_test_data() room = meeting.room_set.first() group = Group.objects.get(acronym='secretariat') - url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) + url = reverse('ietf.secr.meetings.views.misc_sessions',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) self.client.login(username="secretary", password="secretary+password") response = self.client.post(url, { 'day':'1', @@ -298,10 +298,10 @@ class SecrMeetingTestCase(TestCase): self.assertEqual(session.timeslotassignments.first().timeslot.location, room) - def test_meetings_nonsession_add_invalid(self): + def test_meetings_misc_session_add_invalid(self): make_meeting_test_data() group = Group.objects.get(acronym='secretariat') - url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) + url = reverse('ietf.secr.meetings.views.misc_sessions',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) self.client.login(username="secretary", password="secretary+password") response = self.client.post(url, { 'day':'1', @@ -315,12 +315,12 @@ class SecrMeetingTestCase(TestCase): self.assertEqual(response.status_code, 200) self.assertContains(response, 'invalid format') - def test_meetings_nonsession_edit(self): + def test_meetings_misc_session_edit(self): meeting = make_meeting_test_data() - session = meeting.session_set.exclude(name='').first() # get first non-session session + session = meeting.session_set.exclude(name='').first() # get first misc session timeslot = session.official_timeslotassignment().timeslot - url = reverse('ietf.secr.meetings.views.non_session_edit',kwargs={'meeting_id':72,'schedule_name':meeting.schedule.name,'slot_id':timeslot.pk}) - redirect_url = reverse('ietf.secr.meetings.views.non_session',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) + url = reverse('ietf.secr.meetings.views.misc_session_edit',kwargs={'meeting_id':72,'schedule_name':meeting.schedule.name,'slot_id':timeslot.pk}) + redirect_url = reverse('ietf.secr.meetings.views.misc_sessions',kwargs={'meeting_id':72,'schedule_name':'test-schedule'}) new_time = timeslot.time + datetime.timedelta(days=1) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) @@ -339,11 +339,11 @@ class SecrMeetingTestCase(TestCase): timeslot = session.official_timeslotassignment().timeslot self.assertEqual(timeslot.time,new_time) - def test_meetings_non_session_delete(self): + 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.non_session_delete', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id}) - target = reverse('ietf.secr.meetings.views.non_session', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) + 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}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -351,11 +351,11 @@ class SecrMeetingTestCase(TestCase): self.assertRedirects(response, target) self.assertFalse(meeting.schedule.assignments.filter(timeslot=slot)) - def test_meetings_non_session_cancel(self): + 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.non_session_cancel', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'slot_id':slot.id}) - redirect_url = reverse('ietf.secr.meetings.views.non_session', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) + 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}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -364,11 +364,11 @@ class SecrMeetingTestCase(TestCase): session = slot.sessionassignments.filter(schedule=meeting.schedule).first().session self.assertEqual(SchedulingEvent.objects.filter(session=session).order_by('-id')[0].status_id, 'canceled') - def test_meetings_session_edit(self): + def test_meetings_regular_session_edit(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting,group__acronym='mars').first() - url = reverse('ietf.secr.meetings.views.session_edit', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'session_id':session.id}) - redirect_url = reverse('ietf.secr.meetings.views.sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) + url = reverse('ietf.secr.meetings.views.regular_session_edit', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name,'session_id':session.id}) + redirect_url = reverse('ietf.secr.meetings.views.regular_sessions', kwargs={'meeting_id':meeting.number,'schedule_name':meeting.schedule.name}) self.client.login(username="secretary", password="secretary+password") response = self.client.get(url) self.assertEqual(response.status_code, 200) @@ -382,7 +382,7 @@ class SecrMeetingTestCase(TestCase): # ----------------------- def test_get_times(self): meeting = make_meeting_test_data() - timeslot = meeting.timeslot_set.filter(type='session').first() + timeslot = meeting.timeslot_set.filter(type='regular').first() day = (timeslot.time.weekday() + 1) % 7 + 1 # fix up to match django __week_day filter times = get_times(meeting,day) values = [ x[0] for x in times ] diff --git a/ietf/secr/meetings/urls.py b/ietf/secr/meetings/urls.py index 7aa9a473a..96c61d47b 100644 --- a/ietf/secr/meetings/urls.py +++ b/ietf/secr/meetings/urls.py @@ -1,3 +1,4 @@ +# Copyright The IETF Trust 2007-2019, All Rights Reserved from ietf.secr.meetings import views from ietf.utils.urls import url @@ -13,14 +14,14 @@ urlpatterns = [ url(r'^(?P\d{1,6})/edit/$', views.edit_meeting), url(r'^(?P\d{1,6})/notifications/$', views.notifications), url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/$', views.rooms), - url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/non_session/$', views.non_session), - url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/non_session/cancel/(?P\d{1,6})/$', views.non_session_cancel), - url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/non_session/edit/(?P\d{1,6})/$', views.non_session_edit), - url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/non_session/delete/(?P\d{1,6})/$', views.non_session_delete), + url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/miscsessions/$', views.misc_sessions), + url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/miscsessions/cancel/(?P\d{1,6})/$', views.misc_session_cancel), + url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/miscsessions/edit/(?P\d{1,6})/$', views.misc_session_edit), + url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/miscsessions/delete/(?P\d{1,6})/$', views.misc_session_delete), url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/rooms/$', views.rooms), url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/times/$', views.times), - url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/sessions/$', views.sessions), + url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/regularsessions/$', views.regular_sessions), url(r'^(?P\d{1,6})/(?P[A-Za-z0-9_\-]+)/times/delete/(?P
        - {% if s.name %}{{ s.name }}
        {% endif %} - {% if s.status.slug == "sched" %} - {{s.time|date:"D"}}   {{s.time|date:"Y-m-d"}} + {% if s.name %}{{ s.name }}
        {% endif %} + {% if s.current_status == "sched" %} + {{s.time|date:"D"}}   {{s.time|date:"Y-m-d"}} {% else %} - {{s.status}} + {{s.current_status_name}} {% endif %} - {% if show_request and s.meeting.type.slug == 'ietf' %} + {% if show_request and s.meeting.type_id == 'ietf' %} {% if can_edit %}
        Edit Session Request @@ -37,7 +37,7 @@
        {% if show_ical %} - {% if s.meeting.type.slug == 'ietf' %} + {% if s.meeting.type_id == 'ietf' %} {{s.time|date:"H:i"}}   {% else %} From 60a48d46388987f377792acd92383605da0fa2f9 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 9 Dec 2019 18:09:54 +0000 Subject: [PATCH 7/9] Fix bug introduced in previous commit (exclude became filter) - Legacy-Id: 17143 --- ietf/secr/sreq/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index 272b896df..db4f93e36 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -591,7 +591,7 @@ def new(request, acronym): # pre-populated with data from last meeeting's session request elif request.method == 'GET' and 'previous' in request.GET: previous_meeting = Meeting.objects.get(number=str(int(meeting.number) - 1)) - previous_sessions = add_event_info_to_session_qs(Session.objects.filter(meeting=previous_meeting, group=group)).filter(current_status__in=['notmeet', 'deleted']).order_by('id') + previous_sessions = add_event_info_to_session_qs(Session.objects.filter(meeting=previous_meeting, group=group)).exclude(current_status__in=['notmeet', 'deleted']).order_by('id') if not previous_sessions: messages.warning(request, 'This group did not meet at %s' % previous_meeting) return redirect('ietf.secr.sreq.views.new', acronym=acronym) From 173b242e375d76ce6045cbf43073ba4c113f4bd1 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Mon, 9 Dec 2019 18:20:27 +0000 Subject: [PATCH 8/9] Request regular sessions with type session, fix the request test to check the type - Legacy-Id: 17144 --- ietf/secr/sreq/tests.py | 8 ++++---- ietf/secr/sreq/views.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ietf/secr/sreq/tests.py b/ietf/secr/sreq/tests.py index 668faa7a2..949df471f 100644 --- a/ietf/secr/sreq/tests.py +++ b/ietf/secr/sreq/tests.py @@ -127,14 +127,14 @@ class SubmitRequestCase(TestCase): post_data['submit'] = 'Submit' r = self.client.post(confirm_url,post_data) self.assertRedirects(r, main_url) - session_count_after = Session.objects.filter(meeting=meeting, group=group).count() - self.assertTrue(session_count_after == session_count_before + 1) + session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count() + self.assertEqual(session_count_after, session_count_before + 1) # test that second confirm does not add sessions r = self.client.post(confirm_url,post_data) self.assertRedirects(r, main_url) - session_count_after = Session.objects.filter(meeting=meeting, group=group).count() - self.assertTrue(session_count_after == session_count_before + 1) + session_count_after = Session.objects.filter(meeting=meeting, group=group, type='regular').count() + self.assertEqual(session_count_after, session_count_before + 1) def test_submit_request_invalid(self): MeetingFactory(type_id='ietf', date=datetime.date.today()) diff --git a/ietf/secr/sreq/views.py b/ietf/secr/sreq/views.py index db4f93e36..255146233 100644 --- a/ietf/secr/sreq/views.py +++ b/ietf/secr/sreq/views.py @@ -288,7 +288,7 @@ def confirm(request, acronym): attendees=form.data['attendees'], requested_duration=datetime.timedelta(0,int(duration)), comments=form.data['comments'], - type_id='session', + type_id='regular', ) SchedulingEvent.objects.create( session=new_session, From db232ff71c81083d8b63a34b402b2257a4d700a0 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Wed, 11 Dec 2019 17:09:08 +0000 Subject: [PATCH 9/9] Fix a few missing uses of session (instead of regular) embedded in CSS classes - Legacy-Id: 17146 --- ietf/static/ietf/css/agenda/agenda.css | 2 +- ietf/static/ietf/js/agenda/timeslot_edit.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ietf/static/ietf/css/agenda/agenda.css b/ietf/static/ietf/css/agenda/agenda.css index 51c6c53b1..ff06231f5 100644 --- a/ietf/static/ietf/css/agenda/agenda.css +++ b/ietf/static/ietf/css/agenda/agenda.css @@ -907,7 +907,7 @@ td.ourconflicts, td.theirconflicts { background-image: url('images/orange_hatch.png'); } -.agenda_slot_session { +.agenda_slot_regular { background-color: #E0E0E0; } diff --git a/ietf/static/ietf/js/agenda/timeslot_edit.js b/ietf/static/ietf/js/agenda/timeslot_edit.js index 4e07bc914..a7aa1167c 100644 --- a/ietf/static/ietf/js/agenda/timeslot_edit.js +++ b/ietf/static/ietf/js/agenda/timeslot_edit.js @@ -254,7 +254,7 @@ function build_select_box(roomtype, domid, slot_id, select_id) { if(roomtype == "regular") { roomtypesession="selected"; - roomtypeclass="agenda_slot_session"; + roomtypeclass="agenda_slot_regular"; } else if(roomtype == "other") { roomtypeother="selected"; roomtypeclass="agenda_slot_other"; @@ -290,7 +290,7 @@ function insert_timeslotedit_cell(ts) { $(slot_id).removeClass("agenda_slot_unavailable") $(slot_id).removeClass("agenda_slot_other") - $(slot_id).removeClass("agenda_slot_session") + $(slot_id).removeClass("agenda_slot_regular") $(slot_id).removeClass("agenda_slot_plenary") $(slot_id).removeClass("agenda_slot_reserved") $(slot_id).removeClass("no_timeslot"); @@ -341,7 +341,7 @@ function create_timeslotedit_cell(slot_id) { /* $(slot_id).removeClass("agenda_slot_unavailable") */ $(slot_id).removeClass("agenda_slot_other") - $(slot_id).removeClass("agenda_slot_session") + $(slot_id).removeClass("agenda_slot_regular") $(slot_id).removeClass("agenda_slot_plenary") $(slot_id).removeClass("agenda_slot_reserved")