feat: add slides / session materials links to session details modal (#4535)

* feat: add propose/upload slides button to session details modal

* refactor: remove unneeded chaining operator

* test: fix quotes around template string

* feat: link to meeting materials page from AgendaDetailsModal

* refactor: compute session details URL in JS instead of view

* chore: restyle materials page link

* test: fix test case to match changes to session modal
This commit is contained in:
Jennifer Richards 2022-10-12 18:07:23 -03:00 committed by GitHub
parent 6f2114fb0c
commit 0ad293d6e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 90 additions and 31 deletions

View file

@ -55,6 +55,17 @@ n-modal(v-model:show='modalShown')
) )
i.bi.bi-journal-text.me-2 i.bi.bi-journal-text.me-2
span Notepad 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-content
.detail-title .detail-title
h6 h6
@ -95,19 +106,29 @@ n-modal(v-model:show='modalShown')
:src='eventDetails.materialsUrl' :src='eventDetails.materialsUrl'
) )
template(v-else-if='state.tab === `slides`') template(v-else-if='state.tab === `slides`')
.text-center(v-if='state.isLoading') n-card(
n-spin(description='Loading slides...') :bordered='false'
.text-center.p-3(v-else-if='!state.materials || !state.materials.slides || state.materials.slides.length < 1') size='small'
span No slides submitted for this session. )
.list-group(v-else) .text-center(v-if='state.isLoading')
a.list-group-item( n-spin(description='Loading slides...')
v-for='slide of state.materials.slides' .text-center.p-3(v-else-if='!state.materials || !state.materials.slides || !state.materials.slides.decks || state.materials.slides.decks.length < 1')
:key='slide.id' span No slides submitted for this session.
:href='slide.url' .list-group(v-else)
target='_blank' a.list-group-item(
) v-for='deck of state.materials.slides.decks'
i.bi.me-2(:class='`bi-filetype-` + slide.ext') :key='deck.id'
span {{slide.title}} :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) template(v-else)
.text-center(v-if='state.isLoading') .text-center(v-if='state.isLoading')
n-spin(description='Loading minutes...') 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, title: props.event.type === 'regular' ? `${props.event.groupName} (${props.event.acronym})` : props.event.name,
showAgenda: props.event.flags.showAgenda, showAgenda: props.event.flags.showAgenda,
materialsUrl: materialsUrl, materialsUrl: materialsUrl,
detailsUrl: `/meeting/${agendaStore.meeting.number}/session/${props.event.acronym}/`,
tarUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.tgz`, tarUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.tgz`,
pdfUrl: `/meeting/${agendaStore.meeting.number}/agenda/${props.event.acronym}-drafts.pdf`, 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 state.isLoading = true
try { 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) { if (!resp.ok) {
throw new Error(resp.statusText) throw new Error(resp.statusText)
} }

View file

@ -1642,10 +1642,32 @@ def api_get_session_materials (request, session_id=None):
session = get_object_or_404(Session,pk=session_id) session = get_object_or_404(Session,pk=session_id)
minutes = session.minutes() 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({ return JsonResponse({
"url": session.agenda().get_href(), "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": { "minutes": {
"id": minutes.id, "id": minutes.id,
"title": minutes.title, "title": minutes.title,
@ -1700,7 +1722,10 @@ def agenda_extract_schedule (item):
"audioStream": item.timeslot.location.audio_stream_url() if item.timeslot.location else "", "audioStream": item.timeslot.location.audio_stream_url() if item.timeslot.location else "",
"webex": item.timeslot.location.webex_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 "", "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": { # "slotType": {
# "slug": item.slot_type.slug # "slug": item.slot_type.slug

View file

@ -368,12 +368,18 @@ test.describe('past - desktop', () => {
const materialsUrl = (new URL(event.agenda.url)).pathname const materialsUrl = (new URL(event.agenda.url)).pathname
const materialsInfo = { const materialsInfo = {
url: event.agenda.url, url: event.agenda.url,
slides: _.times(5, idx => ({ slides: {
id: 100000 + idx, decks: _.times(5, idx => ({
title: faker.commerce.productName(), id: 100000 + idx,
url: `/meeting/${meetingData.meeting.number}/materials/slides-${meetingData.meeting.number}-${event.acronym}-${faker.internet.domainWord()}`, title: faker.commerce.productName(),
ext: ['pdf', 'html', 'md', 'txt', 'pptx'][idx] 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: { minutes: {
ext: 'md', ext: 'md',
id: 123456, id: 123456,
@ -427,13 +433,16 @@ test.describe('past - desktop', () => {
await navLocator.nth(1).click() await navLocator.nth(1).click()
await expect(navLocator.nth(1)).toHaveClass(/active/) await expect(navLocator.nth(1)).toHaveClass(/active/)
await expect(navLocator.first()).not.toHaveClass(/active/) await expect(navLocator.first()).not.toHaveClass(/active/)
const slidesLocator = page.locator('.agenda-eventdetails .detail-text > .list-group > .list-group-item') const slideDecksLocator = page.locator('.agenda-eventdetails .detail-text .n-card__content > .list-group > .list-group-item')
await expect(slidesLocator).toHaveCount(materialsInfo.slides.length) await expect(slideDecksLocator).toHaveCount(materialsInfo.slides.decks.length)
for (let idx = 0; idx < materialsInfo.slides.length; idx++) { for (let idx = 0; idx < materialsInfo.slides.decks.length; idx++) {
await expect(slidesLocator.nth(idx)).toHaveAttribute('href', materialsInfo.slides[idx].url) await expect(slideDecksLocator.nth(idx)).toHaveAttribute('href', materialsInfo.slides.decks[idx].url)
await expect(slidesLocator.nth(idx).locator('.bi')).toHaveClass(new RegExp(`bi-filetype-${materialsInfo.slides[idx].ext}`)) await expect(slideDecksLocator.nth(idx).locator('.bi')).toHaveClass(new RegExp(`bi-filetype-${materialsInfo.slides.decks[idx].ext}`))
await expect(slidesLocator.nth(idx).locator('span')).toContainText(materialsInfo.slides[idx].title) 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 // Minutes Tab
await navLocator.last().click() await navLocator.last().click()
await expect(navLocator.last()).toHaveClass(/active/) await expect(navLocator.last()).toHaveClass(/active/)
@ -442,7 +451,7 @@ test.describe('past - desktop', () => {
// Footer Buttons // Footer Buttons
const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}` 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') 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()).toContainText('Download as tarball')
await expect(footerBtnsLocator.first()).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`) await expect(footerBtnsLocator.first()).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.tgz`)
await expect(footerBtnsLocator.nth(1)).toContainText('Download as PDF') await expect(footerBtnsLocator.nth(1)).toContainText('Download as PDF')
@ -483,7 +492,7 @@ test.describe('past - desktop', () => {
await expect(page.locator('.agenda-eventdetails')).toBeVisible() await expect(page.locator('.agenda-eventdetails')).toBeVisible()
// Slides Tab // Slides Tab
await page.locator('.agenda-eventdetails .detail-nav > a').nth(1).click() 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 // Minutes Tab
await page.locator('.agenda-eventdetails .detail-nav > a').nth(2).click() 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.') await expect(page.locator('.agenda-eventdetails .detail-text')).toContainText('No minutes submitted for this session.')