feat: load agenda asynchronously via REST endpoint (#4257)

* feat: load agenda asynchronously via REST endpoint

* fix: handle invalid meeting number + pre-64 meetings redirect
This commit is contained in:
Nicolas Giard 2022-07-24 11:34:57 -04:00 committed by GitHub
parent b4e5cfcf91
commit a605b08de6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 159 additions and 97 deletions

View file

@ -1,5 +1,6 @@
<template lang="pug">
.agenda(
v-if='agendaStore.isLoaded'
:class='{ "bolder-text": agendaStore.bolderText }'
)
h1
@ -213,6 +214,8 @@ watch(() => agendaStore.meetingDays, () => {
})
})
watch(() => agendaStore.isLoaded, handleCurrentMeetingRedirect)
// COMPUTED
const titleExtra = computed(() => {
@ -277,6 +280,13 @@ function toggleSettings () {
})
}
// -> Go to current meeting if not provided
function handleCurrentMeetingRedirect () {
if (!route.params.meetingNumber && agendaStore.meeting.number) {
router.replace({ params: { meetingNumber: agendaStore.meeting.number } })
}
}
// --------------------------------------------------------------------
// Handle day indicator / scroll
// --------------------------------------------------------------------
@ -341,13 +351,12 @@ onBeforeUnmount(() => {
// MOUNTED
onMounted(() => {
// -> Go to current meeting if not provided
if (!route.params.meetingNumber && agendaStore.meeting.number) {
router.replace({ params: { meetingNumber: agendaStore.meeting.number } })
}
handleCurrentMeetingRedirect()
// -> Hide Loading Screen
agendaStore.hideLoadingScreen()
if (agendaStore.isLoaded) {
agendaStore.hideLoadingScreen()
}
})
// CREATED

View file

@ -1,6 +1,9 @@
<template lang="pug">
n-theme
n-message-provider
.app-error(v-if='agendaStore.criticalError')
i.bi.bi-x-octagon-fill.me-2
span {{agendaStore.criticalError}}
.app-container(ref='appContainer')
router-view.meeting
</template>
@ -50,6 +53,15 @@ onBeforeUnmount(() => {
@import "bootstrap/scss/variables";
@import "../shared/breakpoints";
.app-error {
background-color: $red-500;
border-radius: 5px;
color: #FFF;
font-weight: 500;
padding: 1rem;
text-align: center;
}
.meeting {
> h1 {
font-weight: 500;

View file

@ -1,29 +1,30 @@
<template lang="pug">
.floorplan
h1
span #[strong IETF {{agendaStore.meeting.number}}] Floor Plan
.meeting-h1-badges.d-none.d-sm-flex
span.meeting-warning(v-if='agendaStore.meeting.warningNote') {{agendaStore.meeting.warningNote}}
span.meeting-beta BETA
h4
span {{agendaStore.meeting.city}}, {{ meetingDate }}
template(v-if='agendaStore.isLoaded')
h1
span #[strong IETF {{agendaStore.meeting.number}}] Floor Plan
.meeting-h1-badges.d-none.d-sm-flex
span.meeting-warning(v-if='agendaStore.meeting.warningNote') {{agendaStore.meeting.warningNote}}
span.meeting-beta BETA
h4
span {{agendaStore.meeting.city}}, {{ meetingDate }}
.floorplan-topnav.my-3
meeting-navigation
.floorplan-topnav.my-3
meeting-navigation
nav.floorplan-floors.nav.nav-pills.nav-justified
a.nav-link(
v-for='floor of agendaStore.floors'
:key='floor.id'
:name='floor.name'
:class='{ active: state.currentFloor === floor.id }'
@click='state.currentFloor = floor.id'
)
i.bi.bi-arrow-down-right-square.me-2
span {{floor.name}}
nav.floorplan-floors.nav.nav-pills.nav-justified(v-if='agendaStore.isLoaded')
a.nav-link(
v-for='floor of agendaStore.floors'
:key='floor.id'
:name='floor.name'
:class='{ active: state.currentFloor === floor.id }'
@click='state.currentFloor = floor.id'
)
i.bi.bi-arrow-down-right-square.me-2
span {{floor.name}}
.row.mt-3
.col-12.col-md-auto
.col-12.col-md-auto(v-if='agendaStore.isLoaded')
.floorplan-rooms.list-group.shadow-sm
router-link.list-group-item.list-group-item-action(
v-for='room of floor.rooms'
@ -149,6 +150,8 @@ watch(() => agendaStore.viewport, () => {
})
})
watch(() => agendaStore.isLoaded, handleCurrentMeetingRedirect)
// METHODS
function computePlanSizeRatio () {
@ -187,6 +190,13 @@ function handleDesiredRoom () {
}
}
// -> Go to current meeting if not provided
function handleCurrentMeetingRedirect () {
if (!route.params.meetingNumber && agendaStore.meeting.number) {
router.replace({ params: { meetingNumber: agendaStore.meeting.number } })
}
}
// --------------------------------------------------------------------
// Handle browser resize
// --------------------------------------------------------------------
@ -212,7 +222,9 @@ onMounted(() => {
}
// -> Hide Loading Screen
agendaStore.hideLoadingScreen()
if (agendaStore.isLoaded) {
agendaStore.hideLoadingScreen()
}
// -> Set Current Floor
if (agendaStore.floors?.length > 0) {

View file

@ -1,5 +1,5 @@
<template lang="pug">
ul.nav.nav-tabs.meeting-nav
ul.nav.nav-tabs.meeting-nav(v-if='agendaStore.isLoaded')
li.nav-item(v-for='tab of tabs')
a.nav-link(
v-if='tab.href'

View file

@ -23,6 +23,7 @@ export const useAgendaStore = defineStore('agenda', {
{ hex: '#20c997', tag: 'Attended' }
],
colorAssignments: {},
criticalError: null,
currentTab: 'agenda',
dayIntersectId: '',
defaultCalendarView: 'week',
@ -132,29 +133,42 @@ export const useAgendaStore = defineStore('agenda', {
}
},
actions: {
fetch () {
const agendaData = JSON.parse(document.getElementById('agenda-data').textContent)
async fetch () {
try {
const meetingData = JSON.parse(document.getElementById('meeting-data').textContent)
// -> Switch to meeting timezone
this.timezone = agendaData.meeting.timezone
const resp = await fetch(`/api/meeting/${meetingData.meetingNumber}/agenda-data`, { credentials: 'omit' })
if (!resp.ok) {
throw new Error(resp.statusText)
}
const agendaData = await resp.json()
// -> Load meeting data
this.categories = agendaData.categories
this.floors = agendaData.floors
this.isCurrentMeeting = agendaData.isCurrentMeeting
this.meeting = agendaData.meeting
this.schedule = agendaData.schedule
this.useHedgeDoc = agendaData.useHedgeDoc
// -> Switch to meeting timezone
this.timezone = agendaData.meeting.timezone
// -> Compute current info note hash
this.infoNoteHash = murmur(agendaData.meeting.infoNote, 0).toString()
// -> Load meeting data
this.categories = agendaData.categories
this.floors = agendaData.floors
this.isCurrentMeeting = agendaData.isCurrentMeeting
this.meeting = agendaData.meeting
this.schedule = agendaData.schedule
this.useHedgeDoc = agendaData.useHedgeDoc
// -> Load meeting-specific preferences
this.infoNoteShown = !(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.hideInfo`) === this.infoNoteHash)
this.colorAssignments = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.colorAssignments`) || '{}')
this.pickedEvents = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.pickedEvents`) || '[]')
// -> Compute current info note hash
this.infoNoteHash = murmur(agendaData.meeting.infoNote, 0).toString()
this.isLoaded = true
// -> Load meeting-specific preferences
this.infoNoteShown = !(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.hideInfo`) === this.infoNoteHash)
this.colorAssignments = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.colorAssignments`) || '{}')
this.pickedEvents = JSON.parse(window.localStorage.getItem(`agenda.${agendaData.meeting.number}.pickedEvents`) || '[]')
this.isLoaded = true
} catch (err) {
console.error(err)
this.criticalError = `Failed to load this meeting: ${err.message}`
}
this.hideLoadingScreen()
},
persistMeetingPreferences () {
if (this.infoNoteShown) {

View file

@ -27,6 +27,8 @@ urlpatterns = [
url(r'^iesg/position', views_ballot.api_set_position),
# Let Meetecho set session video URLs
url(r'^meeting/session/video/url$', meeting_views.api_set_session_video_url),
# Meeting agenda + floorplan data
url(r'^meeting/(?P<num>[A-Za-z0-9._+-]+)/agenda-data$', meeting_views.api_get_agenda_data),
# Let Meetecho trigger recording imports
url(r'^notify/meeting/import_recordings/(?P<number>[a-z0-9-]+)/?$', meeting_views.api_import_recordings),
# Let MeetEcho upload bluesheets

View file

@ -169,6 +169,31 @@ class MeetingTests(BaseMeetingTestCase):
r = self.client.get(urlreverse("agenda-neue", kwargs=dict(num=meeting.number,utc='-utc')))
self.assertEqual(r.status_code, 200)
# Agenda API tests
r = self.client.get(urlreverse("ietf.meeting.views.api_get_agenda_data", kwargs=dict(num=meeting.number)))
self.assertEqual(r.status_code, 200)
rjson = json.loads(r.content.decode("utf8"))
self.assertJSONEqual(
r.content.decode("utf8"),
{
"meeting": {
"number": meeting.number,
"city": meeting.city,
"startDate": meeting.date.isoformat(),
"endDate": meeting.end_date().isoformat(),
"updated": rjson.get("meeting").get("updated"), # Just expect the value to exist
"timezone": meeting.time_zone,
"infoNote": meeting.agenda_info_note,
"warningNote": meeting.agenda_warning_note
},
"categories": rjson.get("categories"), # Just expect the value to exist
"isCurrentMeeting": True,
"useHedgeDoc": True,
"schedule": rjson.get("schedule"), # Just expect the value to exist
"floors": []
}
)
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number,utc='-utc')))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)

View file

@ -1571,38 +1571,39 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
@ensure_csrf_cookie
def agenda_neue(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
base = base if base else 'agenda'
ext = ext if ext else '.html'
mimetype = {
".html":"text/html; charset=%s"%settings.DEFAULT_CHARSET,
".txt": "text/plain; charset=%s"%settings.DEFAULT_CHARSET,
".csv": "text/csv; charset=%s"%settings.DEFAULT_CHARSET,
}
if ext not in mimetype:
raise Http404('Extension not allowed')
# Get current meeting if not specified
if num is None:
num = get_current_ietf_meeting_num()
# We do not have the appropriate data in the datatracker for IETF 64 and earlier.
# So that we're not producing misleading pages, redirect to their proceedings.
# The datatracker DB does include a Meeting instance for every IETF meeting, though,
# so we can use that to validate that num is a valid meeting number.
if int(num) <= 64:
meeting = get_ietf_meeting(num)
if meeting is None:
raise Http404("No such full IETF meeting")
else:
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}')
return render(request, "meeting/agenda-neue.html", {
"meetingData": {
"meetingNumber": num
}
})
@cache_page(5 * 60)
def api_get_agenda_data (request, num=None):
meeting = get_ietf_meeting(num)
if meeting is None:
raise Http404("No such full IETF meeting")
elif int(meeting.number) <= 64:
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}')
return Http404("Pre-IETF 64 meetings are not available through this API")
else:
pass
# Select the schedule to show
if name is None:
schedule = get_schedule(meeting, name)
else:
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
if schedule == None:
base = base.replace("-utc", "")
return render(request, "meeting/no-"+base+ext, {'meeting':meeting }, content_type=mimetype[ext])
schedule = get_schedule(meeting, None)
updated = meeting.updated()
@ -1613,10 +1614,6 @@ def agenda_neue(request, num=None, name=None, base=None, ext=None, owner=None, u
)
AgendaKeywordTagger(assignments=filtered_assignments).apply()
# Done processing for CSV output
if ext == ".csv":
return agenda_csv(schedule, filtered_assignments)
filter_organizer = AgendaFilterOrganizer(assignments=filtered_assignments)
is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
@ -1624,32 +1621,23 @@ def agenda_neue(request, num=None, name=None, base=None, ext=None, owner=None, u
# Get Floor Plans
floors = FloorPlan.objects.filter(meeting=meeting).order_by('order')
rendered_page = render(request, "meeting/agenda-neue.html", {
"schedule_json": {
"meeting": {
"number": schedule.meeting.number,
"city": schedule.meeting.city,
"startDate": schedule.meeting.date.isoformat(),
"endDate": schedule.meeting.end_date().isoformat(),
"updated": updated,
"timezone": meeting.time_zone,
"infoNote": schedule.meeting.agenda_info_note,
"warningNote": schedule.meeting.agenda_warning_note
},
"categories": filter_organizer.get_filter_categories(),
"isCurrentMeeting": is_current_meeting,
"useHedgeDoc": True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False,
"schedule": list(map(agenda_extract_schedule, filtered_assignments)),
"floors": list(map(agenda_extract_floorplan, floors))
return JsonResponse({
"meeting": {
"number": schedule.meeting.number,
"city": schedule.meeting.city,
"startDate": schedule.meeting.date.isoformat(),
"endDate": schedule.meeting.end_date().isoformat(),
"updated": updated,
"timezone": meeting.time_zone,
"infoNote": schedule.meeting.agenda_info_note,
"warningNote": schedule.meeting.agenda_warning_note
},
"schedule": {
"meeting": {
"number": schedule.meeting.number,
}
}
}, content_type=mimetype[ext])
return rendered_page
"categories": filter_organizer.get_filter_categories(),
"isCurrentMeeting": is_current_meeting,
"useHedgeDoc": True if meeting.date>=settings.MEETING_USES_CODIMD_DATE else False,
"schedule": list(map(agenda_extract_schedule, filtered_assignments)),
"floors": list(map(agenda_extract_floorplan, floors))
})
def agenda_extract_schedule (item):
return {

View file

@ -8,12 +8,12 @@
{% load django_vite %}
{% block title %}
IETF {{ schedule.meeting.number }} Meeting Agenda
IETF {{ meetingData.meetingNumber }} Meeting Agenda
{% endblock %}
{% block pagehead %}
<!-- AGENDA VUE COMPONENT -->
<!-- [html-validate-disable-block void-style, attribute-empty-style] -->
{{ schedule_json|json_script:"agenda-data" }}
{{ meetingData|json_script:"meeting-data" }}
{% vite_asset 'client/agenda/main.js' %}
{% endblock %}
{% block morecss %}
@ -54,7 +54,7 @@ body {
}
#app-meeting-loading:after {
content: 'Loading meeting {{ schedule.meeting.number }}...';
content: 'Loading meeting {{ meetingData.meetingNumber }}...';
position: absolute;
text-align: center;
top: 50%;
@ -69,7 +69,7 @@ body {
{% block precontent %}
<div class="meeting-switch">
<i class="bi bi-arrow-left-right me-2"></i>
<a href="{% url 'ietf.meeting.views.agenda' num=schedule.meeting.number %}">Switch to Legacy Agenda Display</a>
<a href="{% url 'ietf.meeting.views.agenda' num=meetingData.meetingNumber %}">Switch to Legacy Agenda Display</a>
</div>
{% endblock %}
{% block content %}