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:
parent
b4e5cfcf91
commit
a605b08de6
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 %}
|
||||
|
|
Loading…
Reference in a new issue