Repository: ietf-tools/datatracker

Issue:
Provide a clearer way to specify session/timeslot names on the agenda
### Description

From a conversation with @flynnliz, it would be helpful to provide a clearer way to set the name that will be shown for an entry on the agenda.

The `Session` and `TimeSlot` models each have a `name` field and how these are shown (or not) on the agenda is not clear through the UI. This is probably another case of the UI too closely following the underlying modeling.

The editor UI should clearly display the name for a session that will be shown on the actual agenda. It should provide a straightforward way to modify this.

### Code of Conduct

- [X] I agree to follow the [IETF's Code of Conduct](https://github.com/ietf-tools/.github/blob/main/CODE_OF_CONDUCT.md)


Discussion:
As of Oct 2022, the process for modifying a session title looked something like this (per email from @flynnliz)

Here’s the order of operations to change the spelling of a non-WG session: 


- I created the session requests and time slots for four new participant related events
- The folks in charge decided to change the spelling of the titles of all four events to add an apostrophe to the end of “participants” 
- I edited all four time slots to add the apostrophe to the titles
- I noticed the old punctuation still appeared on the agenda so I realized that changing the text in the timeslot was not the right thing to do
- I unlocked the session request tool (I am now able to add a new session request without unlocking/re-locking the tool altogether, but in order to edit an existing request I still have to unlock it)
- I edited the EMODIR session request to add the apostrophes to each session name
- I re-locked the session request tool 
- Updating the session request pushes out a confusing email to Lars and the EMODIR chairs, because it only says that “an update has been submitted” but it doesn’t specify what their sessions are or what may have changed, and it also says “The third session requires your approval” even though it doesn’t really, so now one of them is going to ask me why they got the email and what they’re supposed to do with it and I will have to tell them to ignore it again.

Resolving patch:
diff --git a/ietf/meeting/migrations/0059_rename_sessions.py b/ietf/meeting/migrations/0059_rename_sessions.py
new file mode 100644
--- /dev/null
+++ b/ietf/meeting/migrations/0059_rename_sessions.py
@@ -0,0 +1,41 @@
+# Generated by Django 2.2.28 on 2023-02-06 22:20
+from django.db import migrations
+from django.db.models import ExpressionWrapper, F, IntegerField
+
+
+def forward(apps, schema_editor):
+    Meeting = apps.get_model('meeting', 'Meeting')
+    meetings = Meeting.objects.filter(type='ietf', schedule__isnull=False).annotate(
+        number_as_int=ExpressionWrapper(F('number'), output_field=IntegerField())
+    ).order_by('date')
+    # print('session.pk,meeting.number,session.name,timeslot.name')
+    for m in meetings.filter(number_as_int__lt=91):
+        # until meeting 91, TimeSlots had the more descriptive names
+        for assignment in m.schedule.assignments.exclude(session__type='regular').exclude(session__name=F('timeslot__name')):
+            # print(f'{assignment.session.pk},{m.number},{assignment.session.name},{assignment.timeslot.name}')
+            assignment.session.name = assignment.timeslot.name
+            assignment.session.save()
+
+    # meetings 91-98 had no differences or were better off with session names as they were
+
+    for m in meetings.filter(number_as_int__gte=99):
+        # for meetings 99+, TimeSlots again had the better names
+        for assignment in m.schedule.assignments.exclude(session__type='regular').exclude(session__name=F('timeslot__name')):
+            # print(f'{assignment.session.pk},{m.number},{assignment.session.name},{assignment.timeslot.name}')
+            assignment.session.name = assignment.timeslot.name
+            assignment.session.save()
+
+
+def reverse(apps, schema_editor):
+    pass  # can't undo
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('meeting', '0058_meeting_time_zone_not_blank'),
+    ]
+
+    operations = [
+        migrations.RunPython(forward, reverse),
+    ]
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -1704,7 +1704,8 @@ def agenda_extract_schedule (item):
         } if (item.timeslot.show_location and item.timeslot.location and item.timeslot.location.floorplan) else {},
         "acronym": item.acronym,
         "duration": item.timeslot.duration.seconds,
-        "name": item.timeslot.name,
+        "name": item.session.name,
+        "slotName": item.timeslot.name,
         "startDateTime": item.timeslot.time.isoformat(),
         "status": item.session.current_status,
         "type": item.session.type.slug,


Test patch:
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -172,6 +172,42 @@ def test_agenda_extract_schedule_location(self):
         self.assertEqual(shown['room'], room.name)
         self.assertEqual(shown['location'], {'name': room.floorplan.name, 'short': room.floorplan.short})
 
+    def test_agenda_extract_schedule_names(self):
+        meeting = MeetingFactory(type_id='ietf')
+        named_timeslots = TimeSlotFactory.create_batch(2, meeting=meeting, name='Timeslot Name')
+        unnamed_timeslots = TimeSlotFactory.create_batch(2, meeting=meeting, name='')
+        named_sessions = SessionFactory.create_batch(2, meeting=meeting, name='Session Name')
+        unnamed_sessions = SessionFactory.create_batch(2, meeting=meeting, name='')
+        pk_with = {
+            'both named': named_sessions[0].timeslotassignments.create(
+                schedule=meeting.schedule,
+                timeslot=named_timeslots[0],
+            ).pk,
+            'session named': named_sessions[1].timeslotassignments.create(
+                schedule=meeting.schedule,
+                timeslot=unnamed_timeslots[0],
+            ).pk,
+            'timeslot named': unnamed_sessions[0].timeslotassignments.create(
+                schedule=meeting.schedule,
+                timeslot=named_timeslots[1],
+            ).pk,
+            'neither named': unnamed_sessions[1].timeslotassignments.create(
+                schedule=meeting.schedule,
+                timeslot=unnamed_timeslots[1],
+            ).pk,
+        }
+        processed = preprocess_assignments_for_agenda(meeting.schedule.assignments.all(), meeting)
+        AgendaKeywordTagger(assignments=processed).apply()
+        extracted = {item.pk: agenda_extract_schedule(item) for item in processed}
+        self.assertEqual(extracted[pk_with['both named']]['name'], 'Session Name')
+        self.assertEqual(extracted[pk_with['both named']]['slotName'], 'Timeslot Name')
+        self.assertEqual(extracted[pk_with['session named']]['name'], 'Session Name')
+        self.assertEqual(extracted[pk_with['session named']]['slotName'], '')
+        self.assertEqual(extracted[pk_with['timeslot named']]['name'], '')
+        self.assertEqual(extracted[pk_with['timeslot named']]['slotName'], 'Timeslot Name')
+        self.assertEqual(extracted[pk_with['neither named']]['name'], '')
+        self.assertEqual(extracted[pk_with['neither named']]['slotName'], '')
+
 
 class MeetingTests(BaseMeetingTestCase):
     def test_meeting_agenda(self):
diff --git a/playwright/tests/meeting/agenda.spec.js b/playwright/tests/meeting/agenda.spec.js
--- a/playwright/tests/meeting/agenda.spec.js
+++ b/playwright/tests/meeting/agenda.spec.js
@@ -213,7 +213,7 @@ test.describe('past - desktop', () => {
           const headerRow = page.locator(`#agenda-rowid-sesshd-${event.id}`)
           await expect(headerRow).toBeVisible()
           await expect(headerRow.locator('.agenda-table-cell-ts')).toContainText(eventTimeSlot)
-          await expect(headerRow.locator('.agenda-table-cell-name')).toContainText(`${DateTime.fromISO(event.startDateTime).toFormat('cccc')} ${event.name}`)
+          await expect(headerRow.locator('.agenda-table-cell-name')).toContainText(`${DateTime.fromISO(event.startDateTime).toFormat('cccc')} ${event.slotName}`)
         }
         // Timeslot
         await expect(row.locator('.agenda-table-cell-ts')).toContainText('—')