diff --git a/client/agenda/AgendaDetailsModal.vue b/client/agenda/AgendaDetailsModal.vue index 8b2857204..a58e8f83f 100644 --- a/client/agenda/AgendaDetailsModal.vue +++ b/client/agenda/AgendaDetailsModal.vue @@ -55,6 +55,17 @@ n-modal(v-model:show='modalShown') ) i.bi.bi-journal-text.me-2 span Notepad + n-button.float-end( + ghost + color='gray' + strong + tag='a' + :href='eventDetails.detailsUrl' + target='_blank' + aria-label='Materials page' + ) + span.me-2 {{props.event.groupAcronym}} materials page + i.bi.bi-box-arrow-up-right .detail-content .detail-title h6 @@ -95,19 +106,29 @@ n-modal(v-model:show='modalShown') :src='eventDetails.materialsUrl' ) template(v-else-if='state.tab === `slides`') - .text-center(v-if='state.isLoading') - n-spin(description='Loading slides...') - .text-center.p-3(v-else-if='!state.materials || !state.materials.slides || state.materials.slides.length < 1') - span No slides submitted for this session. - .list-group(v-else) - a.list-group-item( - v-for='slide of state.materials.slides' - :key='slide.id' - :href='slide.url' - target='_blank' - ) - i.bi.me-2(:class='`bi-filetype-` + slide.ext') - span {{slide.title}} + n-card( + :bordered='false' + size='small' + ) + .text-center(v-if='state.isLoading') + n-spin(description='Loading slides...') + .text-center.p-3(v-else-if='!state.materials || !state.materials.slides || !state.materials.slides.decks || state.materials.slides.decks.length < 1') + span No slides submitted for this session. + .list-group(v-else) + a.list-group-item( + v-for='deck of state.materials.slides.decks' + :key='deck.id' + :href='deck.url' + target='_blank' + ) + i.bi.me-2(:class='`bi-filetype-` + deck.ext') + span {{deck.title}} + template(#action, v-if='state.materials.slides.actions') + n-button( + v-for='action of state.materials.slides.actions' + tag='a' + :href='action.url' + ) {{action.label}} template(v-else) .text-center(v-if='state.isLoading') n-spin(description='Loading minutes...') @@ -184,9 +205,10 @@ const eventDetails = computed(() => { title: props.event.type === 'regular' ? `${props.event.groupName} (${props.event.acronym})` : props.event.name, showAgenda: props.event.flags.showAgenda, materialsUrl: materialsUrl, + detailsUrl: `/meeting/${agendaStore.meeting.number}/session/${props.event.acronym}/`, tarUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.tgz`, pdfUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.pdf`, - notepadUrl: `https://notes.ietf.org/notes-ietf-${agendaStore.meeting.number}-${props.event.type === 'plenary' ? 'plenary' : props.event.acronym}` + notepadUrl: `https://notes.ietf.org/notes-ietf-${agendaStore.meeting.number}-${props.event.type === 'plenary' ? 'plenary' : props.event.acronym}`, } }) @@ -219,7 +241,10 @@ async function fetchSessionMaterials () { state.isLoading = true try { - const resp = await fetch(`/api/meeting/session/${props.event.sessionId}/materials`, { credentials: 'omit' }) + const resp = await fetch( + `/api/meeting/session/${props.event.sessionId}/materials`, + { credentials: 'include' } + ) if (!resp.ok) { throw new Error(resp.statusText) } diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index ebc8ae8ff..f5a06d6fb 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -1642,10 +1642,32 @@ def api_get_session_materials (request, session_id=None): session = get_object_or_404(Session,pk=session_id) minutes = session.minutes() + slides_actions = [] + if can_manage_session_materials(request.user, session.group, session): + slides_actions.append({ + 'label': 'Upload slides', + 'url': reverse( + 'ietf.meeting.views.upload_session_slides', + kwargs={'num': session.meeting.number, 'session_id': session.pk}, + ), + }) + elif not session.is_material_submission_cutoff(): + slides_actions.append({ + 'label': 'Propose slides', + 'url': reverse( + 'ietf.meeting.views.propose_session_slides', + kwargs={'num': session.meeting.number, 'session_id': session.pk}, + ), + }) + else: + pass # no action available if it's past cutoff return JsonResponse({ "url": session.agenda().get_href(), - "slides": list(map(agenda_extract_slide, session.slides())), + "slides": { + "decks": list(map(agenda_extract_slide, session.slides())), + "actions": slides_actions, + }, "minutes": { "id": minutes.id, "title": minutes.title, @@ -1700,7 +1722,10 @@ def agenda_extract_schedule (item): "audioStream": item.timeslot.location.audio_stream_url() if item.timeslot.location else "", "webex": item.timeslot.location.webex_url() if item.timeslot.location else "", "onsiteTool": item.timeslot.location.onsite_tool_url() if item.timeslot.location else "", - "calendar": reverse('ietf.meeting.views.agenda_ical', kwargs={'num': item.schedule.meeting.number, 'session_id': item.session.id, }) + "calendar": reverse( + 'ietf.meeting.views.agenda_ical', + kwargs={'num': item.schedule.meeting.number, 'session_id': item.session.id}, + ), } # "slotType": { # "slug": item.slot_type.slug diff --git a/playwright/tests/meeting/agenda.spec.js b/playwright/tests/meeting/agenda.spec.js index 23fe69571..04594e808 100644 --- a/playwright/tests/meeting/agenda.spec.js +++ b/playwright/tests/meeting/agenda.spec.js @@ -368,12 +368,18 @@ test.describe('past - desktop', () => { const materialsUrl = (new URL(event.agenda.url)).pathname const materialsInfo = { url: event.agenda.url, - slides: _.times(5, idx => ({ - id: 100000 + idx, - title: faker.commerce.productName(), - url: `/meeting/${meetingData.meeting.number}/materials/slides-${meetingData.meeting.number}-${event.acronym}-${faker.internet.domainWord()}`, - ext: ['pdf', 'html', 'md', 'txt', 'pptx'][idx] - })), + slides: { + decks: _.times(5, idx => ({ + id: 100000 + idx, + title: faker.commerce.productName(), + url: `/meeting/${meetingData.meeting.number}/materials/slides-${meetingData.meeting.number}-${event.acronym}-${faker.internet.domainWord()}`, + ext: ['pdf', 'html', 'md', 'txt', 'pptx'][idx] + })), + actions: [{ + label: 'Propose slides', + url: `/meeting/${meetingData.meeting.number}/session/${event.sessionId}/propose_slides` + }] + }, minutes: { ext: 'md', id: 123456, @@ -427,13 +433,16 @@ test.describe('past - desktop', () => { await navLocator.nth(1).click() await expect(navLocator.nth(1)).toHaveClass(/active/) await expect(navLocator.first()).not.toHaveClass(/active/) - const slidesLocator = page.locator('.agenda-eventdetails .detail-text > .list-group > .list-group-item') - await expect(slidesLocator).toHaveCount(materialsInfo.slides.length) - for (let idx = 0; idx < materialsInfo.slides.length; idx++) { - await expect(slidesLocator.nth(idx)).toHaveAttribute('href', materialsInfo.slides[idx].url) - await expect(slidesLocator.nth(idx).locator('.bi')).toHaveClass(new RegExp(`bi-filetype-${materialsInfo.slides[idx].ext}`)) - await expect(slidesLocator.nth(idx).locator('span')).toContainText(materialsInfo.slides[idx].title) + const slideDecksLocator = page.locator('.agenda-eventdetails .detail-text .n-card__content > .list-group > .list-group-item') + await expect(slideDecksLocator).toHaveCount(materialsInfo.slides.decks.length) + for (let idx = 0; idx < materialsInfo.slides.decks.length; idx++) { + await expect(slideDecksLocator.nth(idx)).toHaveAttribute('href', materialsInfo.slides.decks[idx].url) + await expect(slideDecksLocator.nth(idx).locator('.bi')).toHaveClass(new RegExp(`bi-filetype-${materialsInfo.slides.decks[idx].ext}`)) + await expect(slideDecksLocator.nth(idx).locator('span')).toContainText(materialsInfo.slides.decks[idx].title) } + const slideActionButtonLocator = page.locator('.agenda-eventdetails .detail-text .n-card__action > a') + await expect(slideActionButtonLocator).toHaveCount(1) + await expect(slideActionButtonLocator.first().locator('span')).toContainText('Propose slides') // Minutes Tab await navLocator.last().click() await expect(navLocator.last()).toHaveClass(/active/) @@ -442,7 +451,7 @@ test.describe('past - desktop', () => { // Footer Buttons const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}` const footerBtnsLocator = page.locator('.agenda-eventdetails .detail-action > a') - await expect(footerBtnsLocator).toHaveCount(3) + await expect(footerBtnsLocator).toHaveCount(4) await expect(footerBtnsLocator.first()).toContainText('Download as tarball') await expect(footerBtnsLocator.first()).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`) await expect(footerBtnsLocator.nth(1)).toContainText('Download as PDF') @@ -483,7 +492,7 @@ test.describe('past - desktop', () => { await expect(page.locator('.agenda-eventdetails')).toBeVisible() // Slides Tab await page.locator('.agenda-eventdetails .detail-nav > a').nth(1).click() - await expect(page.locator('.agenda-eventdetails .detail-text')).toContainText('No slides submitted for this session.') + await expect(page.locator('.agenda-eventdetails .detail-text .n-card__content')).toContainText('No slides submitted for this session.') // Minutes Tab await page.locator('.agenda-eventdetails .detail-nav > a').nth(2).click() await expect(page.locator('.agenda-eventdetails .detail-text')).toContainText('No minutes submitted for this session.')