chore: merge main into feat/tzaware

This commit is contained in:
Jennifer Richards 2022-10-14 17:14:48 -03:00 committed by GitHub
commit c5619948c3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
79 changed files with 1363 additions and 3193 deletions

View file

@ -47,7 +47,7 @@ jobs:
pkg_version: ${{ steps.buildvars.outputs.pkg_version }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
@ -70,7 +70,7 @@ jobs:
echo "NEXT_VERSION=$nextStrict" >> $GITHUB_ENV
- name: Create Draft Release
uses: ncipollo/release-action@v1
uses: ncipollo/release-action@v1.11.1
if: ${{ github.event.inputs.publish == 'true' && github.event.inputs.dryrun == 'false' }}
with:
prerelease: true
@ -167,7 +167,7 @@ jobs:
mv latest-coverage.json coverage.json
- name: Upload Coverage Results as Build Artifact
uses: actions/upload-artifact@v3.0.0
uses: actions/upload-artifact@v3
if: ${{ always() }}
with:
name: coverage
@ -186,7 +186,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: '18'
@ -203,7 +203,7 @@ jobs:
npx playwright test --project=${{ matrix.project }}
- name: Upload Report
uses: actions/upload-artifact@v3.0.0
uses: actions/upload-artifact@v3
if: ${{ always() }}
continue-on-error: true
with:
@ -256,7 +256,7 @@ jobs:
yarn cypress:legacy
- name: Upload Video Recordings
uses: actions/upload-artifact@v3.0.0
uses: actions/upload-artifact@v3
if: ${{ always() }}
continue-on-error: true
with:
@ -265,7 +265,7 @@ jobs:
if-no-files-found: ignore
- name: Upload Screenshots
uses: actions/upload-artifact@v3.0.0
uses: actions/upload-artifact@v3
if: ${{ always() }}
continue-on-error: true
with:
@ -286,17 +286,17 @@ jobs:
PKG_VERSION: ${{needs.prepare.outputs.pkg_version}}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v3.0.0
uses: actions/setup-node@v3
with:
node-version: 16.x
node-version: '16'
- name: Setup Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: '3.x'
@ -364,7 +364,7 @@ jobs:
histCoveragePath: historical-coverage.json
- name: Create Release
uses: ncipollo/release-action@v1
uses: ncipollo/release-action@v1.11.1
if: ${{ env.SHOULD_DEPLOY == 'true' && github.event.inputs.dryrun == 'false' }}
with:
allowUpdates: true
@ -376,7 +376,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update Baseline Coverage
uses: ncipollo/release-action@v1
uses: ncipollo/release-action@v1.11.1
if: ${{ github.event.inputs.updateCoverage == 'true' && github.event.inputs.dryrun == 'false' }}
with:
allowUpdates: true
@ -389,8 +389,19 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Upload Build Artifacts
uses: actions/upload-artifact@v2.3.1
uses: actions/upload-artifact@v3
if: ${{ env.SHOULD_DEPLOY == 'false' || github.event.inputs.dryrun == 'true' }}
with:
name: release-${{ env.PKG_VERSION }}
path: /home/runner/work/release/release.tar.gz
- name: Notify on Slack
uses: slackapi/slack-github-action@v1.23.0
with:
channel-id: ${{ secrets.SLACK_GH_BUILDS_CHANNEL_ID }}
payload: |
{
"text": "Datatracker - Build <https://github.com/ietf-tools/datatracker/actions/runs/${{ github.run_id }}|${{ env.PKG_VERSION }}> by ${{ github.triggering_actor }} completed - <@${{ secrets.SLACK_UID_RJSPARKS }}>"
}
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_GH_BOT }}

View file

@ -81,7 +81,7 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v2
- uses: actions/setup-node@v3
with:
node-version: '18'

View file

@ -218,15 +218,34 @@ Frontend tests are done via Cypress. There're 2 different type of tests:
#### Run Vue Tests
To run the tests headlessly (command line mode):
```sh
yarn cypress
```
To run the tests visually **(CANNOT run in docker)**:
```sh
yarn cypress:open
```
> It can take a few seconds before the tests start or the GUI opens.
> :warning: All commands below **MUST** be run from the `./playwright` directory, unless noted otherwise.
1. Run **once** to install dependencies on your system:
```sh
npm install
npx playwright install --with-deps
```
2. Run in a **separate process**, from the **project root directory**:
```sh
yarn preview
```
3. Run the tests, in of these 3 modes, from the `./playwright` directory:
3.1 To run the tests headlessly (command line mode):
```sh
npm test
```
3.2 To run the tests visually **(CANNOT run in docker)**:
```sh
npm run test:visual
```
3.3 To run the tests in debug mode **(CANNOT run in docker)**:
```sh
npm run test:debug
```
#### Run Legacy Views Tests

View file

@ -1,9 +1,9 @@
<template lang="pug">
n-theme
n-message-provider
.app-error(v-if='agendaStore.criticalError')
.app-error(v-if='siteStore.criticalError')
i.bi.bi-x-octagon-fill.me-2
span {{agendaStore.criticalError}}
span {{siteStore.criticalError}}
.app-container(ref='appContainer')
router-view.meeting
</template>
@ -12,13 +12,13 @@ n-theme
import { onBeforeUnmount ,onMounted, ref } from 'vue'
import { NMessageProvider } from 'naive-ui'
import { useAgendaStore } from './agenda/store'
import { useSiteStore } from './shared/store'
import NTheme from './components/n-theme.vue'
// STORES
const agendaStore = useAgendaStore()
const siteStore = useSiteStore()
// STATE
@ -29,14 +29,14 @@ const appContainer = ref(null)
// --------------------------------------------------------------------
const resizeObserver = new ResizeObserver(entries => {
agendaStore.$patch({ viewport: Math.round(window.innerWidth) })
siteStore.$patch({ viewport: Math.round(window.innerWidth) })
// for (const entry of entries) {
// const newWidth = entry.contentBoxSize ? entry.contentBoxSize[0].inlineSize : entry.contentRect.width
// }
})
onMounted(() => {
resizeObserver.observe(appContainer.value, { box: 'device-pixel-content-box' })
resizeObserver.observe(appContainer.value)
})
onBeforeUnmount(() => {
@ -47,7 +47,6 @@ onBeforeUnmount(() => {
<style lang="scss">
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "./shared/breakpoints";
.app-error {
background-color: $red-500;
@ -57,69 +56,4 @@ onBeforeUnmount(() => {
padding: 1rem;
text-align: center;
}
.meeting {
> h1 {
font-weight: 500;
color: $gray-700;
display: flex;
justify-content: space-between;
align-items: center;
@media screen and (max-width: $bs5-break-sm) {
justify-content: center;
> span {
font-size: .95em;
}
}
strong {
font-weight: 700;
background: linear-gradient(220deg, $blue-500 20%, $purple-500 70%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
box-decoration-break: clone;
}
}
&-h1-badges {
display: flex;
justify-content: end;
align-items: center;
> span {
font-size: 13px;
font-weight: 700;
background-color: $pink-500;
box-shadow: 0 0 5px 0 rgba($pink-500, .5);
color: #FFF;
padding: 5px 8px;
border-radius: 6px;
& + span {
margin-left: 10px;
}
}
}
&-warning {
background-color: $red-500 !important;
box-shadow: 0 0 5px 0 rgba($red-500, .5) !important;
color: #FFF;
animation: warningBorderFlash 1s ease infinite;
}
> h4 {
@media screen and (max-width: $bs5-break-sm) {
text-align: center;
> span {
font-size: .8em;
text-align: center;
}
}
}
}
</style>

42
client/Embedded.vue Normal file
View file

@ -0,0 +1,42 @@
<template lang="pug">
n-theme
n-message-provider
component(:is='currentComponent', :component-id='props.componentId')
</template>
<script setup>
import { defineAsyncComponent, markRaw, onMounted, ref } from 'vue'
import { NMessageProvider } from 'naive-ui'
import NTheme from './components/n-theme.vue'
// COMPONENTS
const availableComponents = {
ChatLog: defineAsyncComponent(() => import('./components/ChatLog.vue')),
Polls: defineAsyncComponent(() => import('./components/Polls.vue')),
}
// PROPS
const props = defineProps({
componentName: {
type: String,
default: null
},
componentId: {
type: String,
default: null
}
})
// STATE
const currentComponent = ref(null)
// MOUNTED
onMounted(() => {
currentComponent.value = markRaw(availableComponents[props.componentName] || null)
})
</script>

View file

@ -7,7 +7,6 @@
span #[strong IETF {{agendaStore.meeting.number}}] Meeting Agenda {{titleExtra}}
.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 }}
h6.float-end.d-none.d-lg-inline(v-if='meetingUpdated') #[span.text-muted Updated:] {{ meetingUpdated }}
@ -54,7 +53,7 @@
@click='setTimezone(`UTC`)'
) UTC
n-select.agenda-timezone-ddn(
v-if='agendaStore.viewport > 1250'
v-if='siteStore.viewport > 1250'
v-model:value='agendaStore.timezone'
:options='timezones'
placeholder='Select Time Zone'
@ -134,7 +133,7 @@
// -----------------------------------
// -> Anchored Day Quick Access Menu
// -----------------------------------
.col-auto.d-print-none(v-if='agendaStore.viewport >= 990')
.col-auto.d-print-none(v-if='siteStore.viewport >= 990')
agenda-quick-access
agenda-mobile-bar
@ -166,6 +165,9 @@ import MeetingNavigation from './MeetingNavigation.vue'
import timezones from '../shared/timezones'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store'
import './agenda.scss'
// MESSAGE PROVIDER
@ -174,6 +176,7 @@ const message = useMessage()
// STORES
const agendaStore = useAgendaStore()
const siteStore = useSiteStore()
// ROUTER
@ -215,7 +218,35 @@ watch(() => agendaStore.meetingDays, () => {
})
})
watch(() => agendaStore.isLoaded, handleCurrentMeetingRedirect)
watch(() => agendaStore.isLoaded, () => {
if (route.query.show) {
// Handle legacy ?show= parameter
const keywords = route.query.show.split(',').map(k => k.trim()).filter(k => !!k)
if (keywords?.length > 0) {
const pickedIds = []
for (const ev of agendaStore.scheduleAdjusted) {
if (keywords.includes(ev.sessionKeyword)) {
pickedIds.push(ev.id)
}
}
if (pickedIds.length > 0) {
agendaStore.$patch({
pickerMode: true,
pickerModeView: true,
pickedEvents: pickedIds
})
agendaStore.persistMeetingPreferences()
}
}
}
if (route.query.pick) {
// Handle legacy /personalize path (open picker mode)
agendaStore.$patch({ pickerMode: true })
router.replace({ query: null })
}
handleCurrentMeetingRedirect()
})
// COMPUTED

View file

@ -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)
}

View file

@ -1,5 +1,5 @@
<template lang="pug">
.agenda-mobile-bar(v-if='agendaStore.viewport < 990')
.agenda-mobile-bar(v-if='siteStore.viewport < 990')
button(@click='agendaStore.$patch({ filterShown: true })')
i.bi.bi-filter-square-fill.me-2
span Filters
@ -31,6 +31,7 @@ import {
} from 'naive-ui'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store';
// MESSAGE PROVIDER
@ -39,6 +40,7 @@ const message = useMessage()
// STORES
const agendaStore = useAgendaStore()
const siteStore = useSiteStore()
// Download Ics Options

View file

@ -108,6 +108,7 @@
<script setup>
import { computed, h } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { DateTime } from 'luxon'
import {
NAffix,
@ -119,6 +120,7 @@ import {
} from 'naive-ui'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store';
// MESSAGE PROVIDER
@ -127,6 +129,12 @@ const message = useMessage()
// STORES
const agendaStore = useAgendaStore()
const siteStore = useSiteStore()
// ROUTER
const router = useRouter()
const route = useRoute()
// Download Ics Options
@ -146,7 +154,7 @@ const downloadIcsOptions = [
// COMPUTED
const shortMode = computed(() => {
return agendaStore.viewport <= 1350
return siteStore.viewport <= 1350
})
// METHODS
@ -163,6 +171,9 @@ function pickerModify () {
}
function pickerDiscard () {
agendaStore.$patch({ pickerMode: false })
if (route.query.show) {
router.push({ query: null })
}
}
function downloadIcs (key) {

View file

@ -4,7 +4,7 @@ n-drawer(v-model:show='isShown', placement='bottom', :height='state.drawerHeight
template(#header)
span Calendar View
.agenda-calendar-actions
template(v-if='agendaStore.viewport > 990')
template(v-if='siteStore.viewport > 990')
i.bi.bi-globe.me-2
small.me-2: strong Timezone:
n-button-group
@ -91,10 +91,12 @@ import bootstrap5Plugin from '@fullcalendar/bootstrap5'
import AgendaDetailsModal from './AgendaDetailsModal.vue'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store'
// STORES
const agendaStore = useAgendaStore()
const siteStore = useSiteStore()
// STATE

View file

@ -7,7 +7,7 @@
th.agenda-table-head-check(v-if='pickerModeActive') &nbsp;
th.agenda-table-head-time Time
th.agenda-table-head-location(colspan='2') Location
th.agenda-table-head-event(colspan='2') {{ agendaStore.viewport < 990 ? '' : 'Event' }}
th.agenda-table-head-event(colspan='2') {{ siteStore.viewport < 990 ? '' : 'Event' }}
tbody
tr.agenda-table-display-noresult(
v-if='!meetingEvents || meetingEvents.length < 1'
@ -58,13 +58,13 @@
span.badge {{item.location.short}}
span {{item.location.name}}
router-link.discreet(
:to='`/meeting/` + agendaStore.meeting.number + `/floor-plan-neue?room=` + xslugify(item.room)'
:to='`/meeting/` + agendaStore.meeting.number + `/floor-plan?room=` + xslugify(item.room)'
:aria-label='item.room'
) {{item.room}}
span(v-else) {{item.room}}
//- CELL - GROUP --------------------------
td.agenda-table-cell-group(v-if='item.type === `regular`')
span.badge(v-if='agendaStore.areaIndicatorsShown && agendaStore.viewport > 1200') {{item.groupAcronym}}
span.badge(v-if='agendaStore.areaIndicatorsShown && siteStore.viewport > 1200') {{item.groupAcronym}}
a.discreet(:href='`/group/` + item.acronym + `/about/`') {{item.acronym}}
//- CELL - NAME ---------------------------
td.agenda-table-cell-name
@ -105,7 +105,7 @@
template(v-else)
span.badge.is-cancelled(v-if='!isMobile && item.status === `canceled`') Cancelled
span.badge.is-rescheduled(v-else-if='!isMobile && item.status === `resched`') Rescheduled
.agenda-table-cell-links-buttons(v-else-if='agendaStore.viewport < 1200 && item.links && item.links.length > 0')
.agenda-table-cell-links-buttons(v-else-if='siteStore.viewport < 1200 && item.links && item.links.length > 0')
n-dropdown(
v-if='!agendaStore.colorPickerVisible'
trigger='click'
@ -201,6 +201,7 @@ import {
import AgendaDetailsModal from './AgendaDetailsModal.vue'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store'
// MESSAGE PROVIDER
@ -209,6 +210,7 @@ const message = useMessage()
// STORES
const agendaStore = useAgendaStore()
const siteStore = useSiteStore()
// DATA
@ -236,7 +238,7 @@ const meetingEvents = computed(() => {
return reduce(sortBy(agendaStore.scheduleAdjusted, 'adjustedStartDate'), (acc, item) => {
const isLive = current >= item.adjustedStart && current < item.adjustedEnd
const itemTimeSlot = agendaStore.viewport > 576 ?
const itemTimeSlot = siteStore.viewport > 576 ?
`${item.adjustedStart.toFormat('HH:mm')} - ${item.adjustedEnd.toFormat('HH:mm')}` :
`${item.adjustedStart.toFormat('HH:mm')} ${item.adjustedEnd.toFormat('HH:mm')}`
@ -482,7 +484,7 @@ const pickedEvents = computed({
})
const isMobile = computed(() => {
return agendaStore.viewport < 576
return siteStore.viewport < 576
})
// METHODS

View file

@ -198,6 +198,8 @@ import {
} from 'naive-ui'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store'
import timezones from '../shared/timezones'
// MESSAGE PROVIDER
@ -207,6 +209,7 @@ const message = useMessage()
// STORES
const agendaStore = useAgendaStore()
const siteStore = useSiteStore()
// STATE
@ -266,7 +269,7 @@ const calcOffset = computed(() => {
return agendaStore.nowDebugDiff ? JSON.stringify(agendaStore.nowDebugDiff.toObject()) : 'None'
})
const panelWidth = computed(() => {
return agendaStore.viewport > 500 ? 500 : agendaStore.viewport
return siteStore.viewport > 500 ? 500 : siteStore.viewport
})
// WATCHERS

View file

@ -59,13 +59,18 @@ import find from 'lodash/find'
import xslugify from '../shared/xslugify'
import { DateTime } from 'luxon'
import { useRoute, useRouter } from 'vue-router'
import { useAgendaStore } from './store'
import { useSiteStore } from '../shared/store'
import MeetingNavigation from './MeetingNavigation.vue'
import './agenda.scss'
// STORES
const agendaStore = useAgendaStore()
const siteStore = useSiteStore()
// ROUTER
@ -144,7 +149,7 @@ watch(() => state.currentRoom, () => {
}, 100)
})
})
watch(() => agendaStore.viewport, () => {
watch(() => siteStore.viewport, () => {
nextTick(() => {
computePlanSizeRatio()
})

View file

@ -10,7 +10,7 @@ ul.nav.nav-tabs.meeting-nav(v-if='agendaStore.isLoaded')
router-link.nav-link(
v-else
active-class='active'
:to='`/meeting/` + agendaStore.meeting.number + `/` + tab.key + `-neue`'
:to='`/meeting/` + agendaStore.meeting.number + `/` + tab.key'
)
i.bi.me-2.d-none.d-sm-inline(:class='tab.icon')
span {{tab.title}}

68
client/agenda/agenda.scss Normal file
View file

@ -0,0 +1,68 @@
@import "bootstrap/scss/functions";
@import "bootstrap/scss/variables";
@import "../shared/breakpoints";
.meeting {
> h1 {
font-weight: 500;
color: $gray-700;
display: flex;
justify-content: space-between;
align-items: center;
@media screen and (max-width: $bs5-break-sm) {
justify-content: center;
> span {
font-size: .95em;
}
}
strong {
font-weight: 700;
background: linear-gradient(220deg, $blue-500 20%, $purple-500 70%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
box-decoration-break: clone;
}
}
&-h1-badges {
display: flex;
justify-content: end;
align-items: center;
> span {
font-size: 13px;
font-weight: 700;
background-color: $pink-500;
box-shadow: 0 0 5px 0 rgba($pink-500, .5);
color: #FFF;
padding: 5px 8px;
border-radius: 6px;
& + span {
margin-left: 10px;
}
}
}
&-warning {
background-color: $red-500 !important;
box-shadow: 0 0 5px 0 rgba($red-500, .5) !important;
color: #FFF;
animation: warningBorderFlash 1s ease infinite;
}
> h4 {
@media screen and (max-width: $bs5-break-sm) {
text-align: center;
> span {
font-size: .8em;
text-align: center;
}
}
}
}

View file

@ -3,6 +3,8 @@ import { DateTime } from 'luxon'
import uniqBy from 'lodash/uniqBy'
import murmur from 'murmurhash-js/murmurhash3_gc'
import { useSiteStore } from '../shared/store'
const urlRe = /http[s]?:\/\/(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+/
const conferenceDomains = ['webex.com', 'zoom.us', 'jitsi.org', 'meetecho.com', 'gather.town']
@ -23,7 +25,6 @@ export const useAgendaStore = defineStore('agenda', {
{ hex: '#20c997', tag: 'Attended' }
],
colorAssignments: {},
criticalError: null,
currentTab: 'agenda',
dayIntersectId: '',
defaultCalendarView: 'week',
@ -35,7 +36,6 @@ export const useAgendaStore = defineStore('agenda', {
infoNoteShown: true,
isCurrentMeeting: false,
isLoaded: false,
isMobile: /Mobi/i.test(navigator.userAgent),
listDayCollapse: false,
meeting: {},
nowDebugDiff: null,
@ -50,7 +50,6 @@ export const useAgendaStore = defineStore('agenda', {
settingsShown: false,
timezone: DateTime.local().zoneName,
useHedgeDoc: false,
viewport: Math.round(window.innerWidth),
visibleDays: []
}),
getters: {
@ -119,10 +118,11 @@ export const useAgendaStore = defineStore('agenda', {
})
},
meetingDays () {
const siteStore = useSiteStore()
return uniqBy(this.scheduleAdjusted, 'adjustedStartDate').sort().map(s => ({
slug: s.id.toString(),
ts: s.adjustedStartDate,
label: this.viewport < 1350 ? DateTime.fromISO(s.adjustedStartDate).toFormat('ccc LLL d') : DateTime.fromISO(s.adjustedStartDate).toLocaleString(DateTime.DATE_HUGE)
label: siteStore.viewport < 1350 ? DateTime.fromISO(s.adjustedStartDate).toFormat('ccc LLL d') : DateTime.fromISO(s.adjustedStartDate).toLocaleString(DateTime.DATE_HUGE)
}))
},
isMeetingLive (state) {
@ -168,7 +168,10 @@ export const useAgendaStore = defineStore('agenda', {
this.isLoaded = true
} catch (err) {
console.error(err)
this.criticalError = `Failed to load this meeting: ${err.message}`
const siteStore = useSiteStore()
siteStore.$patch({
criticalError: `Failed to load this meeting: ${err.message}`
})
}
this.hideLoadingScreen()

View file

@ -0,0 +1,98 @@
<template lang="pug">
.chatlog
n-timeline(
v-if='state.items.length > 0'
:icon-size='18'
size='large'
)
n-timeline-item(
v-for='item of state.items'
:key='item.id'
type='default'
:color='item.color'
:title='item.author'
:time='item.time'
)
template(#default)
div(v-html='item.text')
span.text-muted(v-else)
em No chat log available.
</template>
<script setup>
import { onMounted, reactive } from 'vue'
import { DateTime } from 'luxon'
import {
NTimeline,
NTimelineItem
} from 'naive-ui'
// PROPS
const props = defineProps({
componentId: {
type: String,
required: true
}
})
// STATE
const state = reactive({
items: []
})
// bs5 colors
const colors = [
'#0d6efd',
'#dc3545',
'#20c997',
'#6f42c1',
'#fd7e14',
'#198754',
'#0dcaf0',
'#d63384',
'#ffc107',
'#6610f2',
'#adb5bd'
]
// MOUNTED
onMounted(() => {
const authorColors = {}
// Get chat log data from embedded json tag
const chatLog = JSON.parse(document.getElementById(`${props.componentId}-data`).textContent || '[]')
if (chatLog.length > 0) {
let idx = 1
let colorIdx = 0
for (const logItem of chatLog) {
// -> Get unique color per author
if (!authorColors[logItem.author]) {
authorColors[logItem.author] = colors[colorIdx]
colorIdx++
if (colorIdx >= colors.length) {
colorIdx = 0
}
}
// -> Generate log item
state.items.push({
id: `logitem-${idx}`,
color: authorColors[logItem.author],
author: logItem.author,
text: logItem.text,
time: DateTime.fromISO(logItem.time).toFormat('dd LLLL yyyy \'at\' HH:mm:ss a ZZZZ')
})
idx++
}
}
})
</script>
<style lang="scss">
.chatlog {
.n-timeline-item-content__content > div > p {
margin-bottom: 0;
}
}
</style>

View file

@ -0,0 +1,79 @@
<template lang="pug">
.polls
n-data-table(
v-if='state.items.length > 0'
:data='state.items'
:columns='columns'
striped
)
span.text-muted(v-else)
em No polls available.
</template>
<script setup>
import { onMounted, reactive } from 'vue'
import { DateTime } from 'luxon'
import {
NDataTable
} from 'naive-ui'
// PROPS
const props = defineProps({
componentId: {
type: String,
required: true
}
})
// STATE
const state = reactive({
items: []
})
const columns = [
{
title: 'Question',
key: 'question'
},
{
title: 'Start Time',
key: 'start_time',
},
{
title: 'End Time',
key: 'end_time'
},
{
title: 'Raise Hand',
key: 'raise_hand'
},
{
title: 'Do Not Raise Hand',
key: 'do_not_raise_hand'
}
]
// MOUNTED
onMounted(() => {
// Get polls from embedded json tag
const polls = JSON.parse(document.getElementById(`${props.componentId}-data`).textContent || '[]')
if (polls.length > 0) {
let idx = 1
for (const poll of polls) {
state.items.push({
id: `poll-${idx}`,
question: poll.text,
start_time: DateTime.fromISO(poll.start_time).toFormat('dd LLLL yyyy \'at\' HH:mm:ss a ZZZZ'),
end_time: DateTime.fromISO(poll.end_time).toFormat('dd LLLL yyyy \'at\' HH:mm:ss a ZZZZ'),
raise_hand: poll.raise_hand,
do_not_raise_hand: poll.do_not_raise_hand
})
idx++
}
}
})
</script>

13
client/embedded.js Normal file
View file

@ -0,0 +1,13 @@
import { createApp } from 'vue'
import Embedded from './Embedded.vue'
// Mount App
const mountEls = document.querySelectorAll('div.vue-embed')
for (const mnt of mountEls) {
const app = createApp(Embedded, {
componentName: mnt.dataset.component,
componentId: mnt.dataset.componentId
})
app.mount(mnt)
}

View file

@ -3,9 +3,12 @@ import { createRouter, createWebHistory } from 'vue-router'
export default createRouter({
history: createWebHistory(),
routes: [
// ---------------------------------------------------------
// MEETING
// ---------------------------------------------------------
{
name: 'agenda',
path: '/meeting/:meetingNumber(\\d+)?/agenda-neue',
path: '/meeting/:meetingNumber(\\d+)?/agenda',
component: () => import('./agenda/Agenda.vue'),
meta: {
hideLeftMenu: true
@ -13,11 +16,18 @@ export default createRouter({
},
{
name: 'floor-plan',
path: '/meeting/:meetingNumber(\\d+)?/floor-plan-neue',
path: '/meeting/:meetingNumber(\\d+)?/floor-plan',
component: () => import('./agenda/FloorPlan.vue'),
meta: {
hideLeftMenu: true
}
},
// -> Redirects
{
path: '/meeting/:meetingNumber(\\d+)?/agenda/personalize',
redirect: to => {
return { name: 'agenda', query: { ...to.query, pick: true } }
}
}
]
})

9
client/shared/store.js Normal file
View file

@ -0,0 +1,9 @@
import { defineStore } from 'pinia'
export const useSiteStore = defineStore('site', {
state: () => ({
criticalError: null,
isMobile: /Mobi/i.test(navigator.userAgent),
viewport: Math.round(window.innerWidth)
})
})

View file

@ -95,8 +95,8 @@ You can also open the datatracker project folder and click the **Reopen in conta
```sh
Copy-Item "docker/docker-compose.extend.yml" -Destination "docker/docker-compose.extend-custom.yml"
(Get-Content -path docker/docker-compose.extend-custom.yml -Raw) -replace 'CUSTOM_PORT','8000' | Set-Content -Path docker/docker-compose.extend-custom.yml
docker-compose -f docker-compose.yml -f docker/docker-compose.extend-custom.yml up -d
docker-compose exec app /bin/sh /docker-init.sh
docker compose -f docker-compose.yml -f docker/docker-compose.extend-custom.yml up -d
docker compose exec app /bin/sh /docker-init.sh
```
2. Wait for the containers to initialize. Upon completion, you will be dropped into a shell from which you can start the datatracker and execute related commands as usual, for example
@ -120,7 +120,7 @@ The containers will automatically be shut down on Linux / macOS.
On Windows, type the command
```sh
docker-compose down
docker compose down
```
to terminate the containers.
@ -138,9 +138,9 @@ cd docker
On Windows:
```sh
docker-compose down -v
docker-compose pull db
docker-compose build --no-cache db
docker compose down -v
docker compose pull db
docker compose build --no-cache db
```
### Clean all
@ -156,7 +156,7 @@ cd docker
On Windows:
```sh
docker-compose down -v --rmi all
docker compose down -v --rmi all
docker image prune
```
@ -164,7 +164,7 @@ docker image prune
The port is exposed but not mapped to `3306` to avoid potential conflicts with the host. To get the mapped port, run the command *(from the project `/docker` directory)*:
```sh
docker-compose port db 3306
docker compose port db 3306
```
## Notes / Troubleshooting

View file

@ -9,6 +9,7 @@ import sys
from importlib import import_module
from mock import patch
from pathlib import Path
from django.apps import apps
from django.conf import settings
@ -21,6 +22,7 @@ from tastypie.test import ResourceTestCaseMixin
import debug # pyflakes:ignore
import ietf
from ietf.doc.utils import get_unicode_document_content
from ietf.group.factories import RoleFactory
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.meeting.test_data import make_meeting_test_data
@ -213,6 +215,93 @@ class CustomApiTests(TestCase):
self.assertTrue(session.attended_set.filter(person=recman).exists())
self.assertTrue(session.attended_set.filter(person=otherperson).exists())
def test_api_upload_polls_and_chatlog(self):
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')
recmanrole.person.user.last_login = timezone.now()
recmanrole.person.user.save()
badrole = RoleFactory(group__type_id='ietf', name_id='ad')
badrole.person.user.last_login = timezone.now()
badrole.person.user.save()
meeting = MeetingFactory(type_id='ietf')
session = SessionFactory(group__type_id='wg', meeting=meeting)
for type_id, content in (
(
"chatlog",
"""[
{
"author": "Raymond Lutz",
"text": "<p>Yes I like that comment just made</p>",
"time": "2022-07-28T19:26:16Z"
},
{
"author": "Carsten Bormann",
"text": "<p>But software is not a thing.</p>",
"time": "2022-07-28T19:26:45Z"
}
]"""
),
(
"polls",
"""[
{
"start_time": "2022-07-28T19:19:54Z",
"end_time": "2022-07-28T19:20:23Z",
"text": "Are you willing to review the documents?",
"raise_hand": 57,
"do_not_raise_hand": 11
},
{
"start_time": "2022-07-28T19:20:56Z",
"end_time": "2022-07-28T19:21:30Z",
"text": "Would you be willing to edit or coauthor a document?",
"raise_hand": 31,
"do_not_raise_hand": 31
}
]"""
),
):
url = urlreverse(f"ietf.meeting.views.api_upload_{type_id}")
apikey = PersonalApiKey.objects.create(endpoint=url, person=recmanrole.person)
badapikey = PersonalApiKey.objects.create(endpoint=url, person=badrole.person)
r = self.client.post(url, {})
self.assertContains(r, "Missing apikey parameter", status_code=400)
r = self.client.post(url, {'apikey': badapikey.hash()} )
self.assertContains(r, "Restricted to role: Recording Manager", status_code=403)
r = self.client.get(url, {'apikey': apikey.hash()} )
self.assertContains(r, "Method not allowed", status_code=405)
r = self.client.post(url, {'apikey': apikey.hash()} )
self.assertContains(r, "Missing apidata parameter", status_code=400)
for baddict in (
'{}',
'{"bogons;drop table":"bogons;drop table"}',
'{"session_id":"Not an integer;drop table"}',
f'{{"session_id":{session.pk},"{type_id}":"not a list;drop table"}}',
f'{{"session_id":{session.pk},"{type_id}":"not a list;drop table"}}',
f'{{"session_id":{session.pk},"{type_id}":[{{}}, {{}}, "not an int;drop table", {{}}]}}',
):
r = self.client.post(url, {'apikey': apikey.hash(), 'apidata': baddict})
self.assertContains(r, "Malformed post", status_code=400)
bad_session_id = Session.objects.order_by('-pk').first().pk + 1
r = self.client.post(url, {'apikey': apikey.hash(), 'apidata': f'{{"session_id":{bad_session_id},"{type_id}":[]}}'})
self.assertContains(r, "Invalid session", status_code=400)
# Valid POST
r = self.client.post(url,{'apikey':apikey.hash(),'apidata': f'{{"session_id":{session.pk}, "{type_id}":{content}}}'})
self.assertEqual(r.status_code, 200)
newdoc = session.sessionpresentation_set.get(document__type_id=type_id).document
newdoccontent = get_unicode_document_content(newdoc.name, Path(session.meeting.get_materials_path()) / type_id / newdoc.uploaded_filename)
self.assertEqual(json.loads(content), json.loads(newdoccontent))
def test_api_upload_bluesheet(self):
url = urlreverse('ietf.meeting.views.api_upload_bluesheet')
recmanrole = RoleFactory(group__type_id='ietf', name_id='recman')

View file

@ -37,6 +37,10 @@ urlpatterns = [
url(r'^notify/meeting/bluesheet/?$', meeting_views.api_upload_bluesheet),
# Let MeetEcho tell us about session attendees
url(r'^notify/session/attendees/?$', meeting_views.api_add_session_attendees),
# Let MeetEcho upload session chatlog
url(r'^notify/session/chatlog/?$', meeting_views.api_upload_chatlog),
# Let MeetEcho upload session polls
url(r'^notify/session/polls/?$', meeting_views.api_upload_polls),
# Let the registration system notify us about registrations
url(r'^notify/meeting/registration/?', api_views.api_new_meeting_registration),
# OpenID authentication provider

View file

@ -0,0 +1,34 @@
# Copyright The IETF Trust 2022, All Rights Reserved
from django.db import migrations
def forward(apps, schema_editor):
StateType = apps.get_model("doc", "StateType")
State = apps.get_model("doc", "State")
for slug in ("chatlog", "polls"):
StateType.objects.create(slug=slug, label="State")
for state_slug in ("active", "deleted"):
State.objects.create(
type_id = slug,
slug = state_slug,
name = state_slug.capitalize(),
used = True,
desc = "",
order = 0,
)
def reverse(apps, schema_editor):
StateType = apps.get_model("doc", "StateType")
State = apps.get_model("doc", "State")
State.objects.filter(type_id__in=("chatlog", "polls")).delete()
StateType.objects.filter(slug__in=("chatlog", "polls")).delete()
class Migration(migrations.Migration):
dependencies = [
('doc', '0044_procmaterials_states'),
('name', '0045_polls_and_chatlogs'),
]
operations = [
migrations.RunPython(forward, reverse),
]

View file

@ -7,7 +7,7 @@ import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('doc', '0044_procmaterials_states'),
('doc', '0045_docstates_chatlogs_polls'),
]
operations = [

View file

@ -52,7 +52,7 @@ def reverse(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('doc', '0045_use_timezone_now_for_doc_models'),
('doc', '0046_use_timezone_now_for_doc_models'),
('utils', '0003_pause_to_change_use_tz'),
]

View file

@ -138,7 +138,7 @@ class DocumentInfo(models.Model):
else:
self._cached_file_path = settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR
elif self.meeting_related() and self.type_id in (
"agenda", "minutes", "slides", "bluesheets", "procmaterials"
"agenda", "minutes", "slides", "bluesheets", "procmaterials", "chatlog", "polls"
):
meeting = self.get_related_meeting()
if meeting is not None:
@ -422,7 +422,7 @@ class DocumentInfo(models.Model):
return e != None and (e.text != "")
def meeting_related(self):
if self.type_id in ("agenda","minutes","bluesheets","slides","recording","procmaterials"):
if self.type_id in ("agenda","minutes","bluesheets","slides","recording","procmaterials","chatlog","polls"):
return self.type_id != "slides" or self.get_state_slug('reuse_policy')=='single'
return False

View file

@ -1470,6 +1470,10 @@ Man Expires September 22, 2015 [Page 3]
DocumentFactory(type_id='agenda',name='agenda-72-mars')
DocumentFactory(type_id='minutes',name='minutes-72-mars')
DocumentFactory(type_id='slides',name='slides-72-mars-1-active')
chatlog = DocumentFactory(type_id="chatlog",name='chatlog-72-mars-197001010000')
polls = DocumentFactory(type_id="polls",name='polls-72-mars-197001010000')
SessionPresentationFactory(document=chatlog)
SessionPresentationFactory(document=polls)
statchg = DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review')
statchg.set_state(State.objects.get(type_id='statchg',slug='adrev'))
@ -1481,6 +1485,8 @@ Man Expires September 22, 2015 [Page 3]
"agenda-72-mars",
"minutes-72-mars",
"slides-72-mars-1-active",
"chatlog-72-mars-197001010000",
"polls-72-mars-197001010000",
# TODO: add
#"bluesheets-72-mars-1",
#"recording-72-mars-1-00",

View file

@ -42,6 +42,7 @@ import os
import re
from urllib.parse import quote
from pathlib import Path
from django.http import HttpResponse, Http404
from django.shortcuts import render, get_object_or_404, redirect
@ -641,9 +642,7 @@ def document_main(request, name, rev=None):
sorted_relations=sorted_relations,
))
# TODO : Add "recording", and "bluesheets" here when those documents are appropriately
# created and content is made available on disk
if doc.type_id in ("slides", "agenda", "minutes", "bluesheets","procmaterials",):
if doc.type_id in ("slides", "agenda", "minutes", "bluesheets", "procmaterials",):
can_manage_material = can_manage_materials(request.user, doc.group)
presentations = doc.future_presentations()
if doc.uploaded_filename:
@ -725,6 +724,29 @@ def document_main(request, name, rev=None):
assignments=assignments,
))
if doc.type_id in ("chatlog", "polls"):
if isinstance(doc,DocHistory):
session = doc.doc.sessionpresentation_set.last().session
else:
session = doc.sessionpresentation_set.last().session
pathname = Path(session.meeting.get_materials_path()) / doc.type_id / doc.uploaded_filename
content = get_unicode_document_content(doc.name, str(pathname))
return render(
request,
f"doc/document_{doc.type_id}.html",
dict(
doc=doc,
top=top,
content=content,
revisions=revisions,
latest_rev=latest_rev,
snapshot=snapshot,
session=session,
)
)
raise Http404("Document not found: %s" % (name + ("-%s"%rev if rev else "")))

View file

@ -253,18 +253,35 @@ class Meeting(models.Model):
number = self.get_number()
if number is None or number < 110:
return None
Attendance = namedtuple('Attendance', 'onsite online')
Attendance = namedtuple('Attendance', 'onsite remote')
# MeetingRegistration.attended started conflating badge-pickup and session attendance before IETF 114.
# We've separated session attendence off to ietf.meeting.Attended, but need to report attendance at older
# meetings correctly.
attended_per_meetingregistration = (
Q(meetingregistration__meeting=self) & (
Q(meetingregistration__attended=True) |
Q(meetingregistration__checkedin=True)
)
)
attended_per_meeting_attended = (
Q(attended__session__meeting=self)
# Note that we are not filtering to plenary, wg, or rg sessions
# as we do for nomcom eligibility - if picking up a badge (see above)
# is good enough, just attending e.g. a training session is also good enough
)
attended = Person.objects.filter(
attended_per_meetingregistration | attended_per_meeting_attended
).distinct()
onsite=set(attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='onsite'))
remote=set(attended.filter(meetingregistration__meeting=self, meetingregistration__reg_type='remote'))
remote.difference_update(onsite)
return Attendance(
onsite=Person.objects.filter(
meetingregistration__meeting=self,
meetingregistration__attended=True,
meetingregistration__reg_type__contains='in_person',
).distinct().count(),
online=Person.objects.filter(
meetingregistration__meeting=self,
meetingregistration__attended=True,
meetingregistration__reg_type__contains='remote',
).distinct().count(),
onsite=len(onsite),
remote=len(remote)
)
@property
@ -464,7 +481,7 @@ class Room(models.Model):
if not mtg_num:
return None
elif self.floorplan:
base_url = urlreverse('ietf.meeting.views.floor_plan', kwargs=dict(num=mtg_num))
base_url = urlreverse('floor-plan', kwargs=dict(num=mtg_num))
else:
return None
return f'{base_url}?room={xslugify(self.name)}'

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ import datetime
from mock import patch
from ietf.meeting.factories import MeetingFactory, SessionFactory
from ietf.meeting.factories import MeetingFactory, SessionFactory, AttendedFactory
from ietf.stats.factories import MeetingRegistrationFactory
from ietf.utils.test_utils import TestCase
@ -19,41 +19,75 @@ class MeetingTests(TestCase):
MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='in_person')
self.assertIsNone(meeting.get_attendance())
def test_get_attendance(self):
"""Post-110 meetings do calculate attendance"""
def test_get_attendance_110(self):
"""Look at attendance as captured at 110"""
meeting = MeetingFactory(type_id='ietf', number='110')
# start with attendees that should be ignored
MeetingRegistrationFactory.create_batch(3, meeting=meeting, reg_type='')
MeetingRegistrationFactory.create_batch(3, meeting=meeting, reg_type='', attended=True)
MeetingRegistrationFactory(meeting=meeting, reg_type='', attended=False)
attendance = meeting.get_attendance()
self.assertIsNotNone(attendance)
self.assertEqual(attendance.online, 0)
self.assertEqual(attendance.remote, 0)
self.assertEqual(attendance.onsite, 0)
# add online attendees with at least one who registered but did not attend
MeetingRegistrationFactory.create_batch(4, meeting=meeting, reg_type='remote')
MeetingRegistrationFactory.create_batch(4, meeting=meeting, reg_type='remote', attended=True)
MeetingRegistrationFactory(meeting=meeting, reg_type='remote', attended=False)
attendance = meeting.get_attendance()
self.assertIsNotNone(attendance)
self.assertEqual(attendance.online, 4)
self.assertEqual(attendance.remote, 4)
self.assertEqual(attendance.onsite, 0)
# and the same for onsite attendees
MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='in_person')
MeetingRegistrationFactory.create_batch(5, meeting=meeting, reg_type='onsite', attended=True)
MeetingRegistrationFactory(meeting=meeting, reg_type='in_person', attended=False)
attendance = meeting.get_attendance()
self.assertIsNotNone(attendance)
self.assertEqual(attendance.online, 4)
self.assertEqual(attendance.remote, 4)
self.assertEqual(attendance.onsite, 5)
# and once more after removing all the online attendees
meeting.meetingregistration_set.filter(reg_type='remote').delete()
attendance = meeting.get_attendance()
self.assertIsNotNone(attendance)
self.assertEqual(attendance.online, 0)
self.assertEqual(attendance.remote, 0)
self.assertEqual(attendance.onsite, 5)
def test_get_attendance_113(self):
"""Simulate IETF 113 attendance gathering data"""
meeting = MeetingFactory(type_id='ietf', number='113')
MeetingRegistrationFactory(meeting=meeting, reg_type='onsite', attended=True, checkedin=False)
MeetingRegistrationFactory(meeting=meeting, reg_type='onsite', attended=False, checkedin=True)
p1 = MeetingRegistrationFactory(meeting=meeting, reg_type='onsite', attended=False, checkedin=False).person
AttendedFactory(session__meeting=meeting, person=p1)
p2 = MeetingRegistrationFactory(meeting=meeting, reg_type='remote', attended=False, checkedin=False).person
AttendedFactory(session__meeting=meeting, person=p2)
attendance = meeting.get_attendance()
self.assertEqual(attendance.onsite, 3)
self.assertEqual(attendance.remote, 1)
def test_get_attendance_keeps_meetings_distinct(self):
"""No cross-talk between attendance for different meetings"""
# numbers are arbitrary here
first_mtg = MeetingFactory(type_id='ietf', number='114')
second_mtg = MeetingFactory(type_id='ietf', number='115')
# Create a person who attended a remote session for first_mtg and onsite for second_mtg without
# checking in for either.
p = MeetingRegistrationFactory(meeting=second_mtg, reg_type='onsite', attended=False, checkedin=False).person
AttendedFactory(session__meeting=first_mtg, person=p)
MeetingRegistrationFactory(meeting=first_mtg, person=p, reg_type='remote', attended=False, checkedin=False)
AttendedFactory(session__meeting=second_mtg, person=p)
att = first_mtg.get_attendance()
self.assertEqual(att.onsite, 0)
self.assertEqual(att.remote, 1)
att = second_mtg.get_attendance()
self.assertEqual(att.onsite, 1)
self.assertEqual(att.remote, 0)
def test_vtimezone(self):
# normal time zone that should have a zoneinfo file
meeting = MeetingFactory(type_id='ietf', time_zone='America/Los_Angeles', populate_schedule=False)

View file

@ -17,7 +17,7 @@ from pyquery import PyQuery
from lxml.etree import tostring
from io import StringIO, BytesIO
from bs4 import BeautifulSoup
from urllib.parse import urlparse, urlsplit, quote
from urllib.parse import urlparse, urlsplit
from PIL import Image
from pathlib import Path
from tempfile import NamedTemporaryFile
@ -52,7 +52,6 @@ from ietf.name.models import SessionStatusName, ImportantDateName, RoleName, Pro
from ietf.utils.decorators import skip_coverage
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
from ietf.utils.text import xslugify
from ietf.utils.timezone import date_today, time_now
from ietf.person.factories import PersonFactory
@ -168,7 +167,7 @@ class MeetingTests(BaseMeetingTestCase):
time_interval = r"%s<span.*/span>-%s" % (slot.utc_start_time().strftime("%H:%M").lstrip("0"), (slot.utc_start_time() + slot.duration).strftime("%H:%M").lstrip("0"))
# Extremely rudementary test of agenda-neue - to be replaced with back-end tests as the front-end tests are developed.
r = self.client.get(urlreverse("agenda-neue", kwargs=dict(num=meeting.number,utc='-utc')))
r = self.client.get(urlreverse("agenda", kwargs=dict(num=meeting.number,utc='-utc')))
self.assertEqual(r.status_code, 200)
# Agenda API tests
@ -215,56 +214,17 @@ class MeetingTests(BaseMeetingTestCase):
}
)
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)
agenda_content = q("#content").html()
self.assertIn(session.group.acronym, agenda_content)
self.assertIn(session.group.name, agenda_content)
self.assertIn(session.group.parent.acronym.upper(), agenda_content)
self.assertIn(slot.location.name, agenda_content)
self.assertRegex(agenda_content, time_interval)
self.assertIsNotNone(q(':input[value="%s"]' % meeting.time_zone),
'Time zone selector should show meeting timezone')
self.assertIsNotNone(q('.nav *:contains("%s")' % meeting.time_zone),
'Time zone indicator should be in nav sidebar')
# plain
time_interval = r"{}<span.*/span>-{}".format(
slot.time.astimezone(meeting.tz()).strftime("%H:%M").lstrip("0"),
slot.end_time().astimezone(meeting.tz()).strftime("%H:%M").lstrip("0"),
)
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number)))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
agenda_content = q("#content").html()
self.assertIn(session.group.acronym, agenda_content)
self.assertIn(session.group.name, agenda_content)
self.assertIn(session.group.parent.acronym.upper(), agenda_content)
self.assertIn(slot.location.name, agenda_content)
self.assertRegex(agenda_content, time_interval)
self.assertIn(registration_text, agenda_content)
# Make sure there's a frame for the session agenda and it points to the right place
assignment_url = urlreverse('ietf.meeting.views.session_materials', kwargs=dict(session_id=session.pk))
self.assertTrue(
any(
[assignment_url in x.attrib["data-src"]
for x in q('tr div.modal-body div.session-materials')]
)
)
# future meeting, no agenda
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=future_meeting.number)))
self.assertContains(r, "There is no agenda available yet.")
self.assertTemplateUsed(r, 'meeting/no-agenda.html')
# text
# the rest of the results don't have as nicely formatted times
time_interval = "%s-%s" % (slot.time.strftime("%H%M").lstrip("0"), (slot.time + slot.duration).strftime("%H%M").lstrip("0"))
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number, ext=".txt")))
r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".txt")))
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, session.group.parent.acronym.upper())
@ -272,16 +232,13 @@ class MeetingTests(BaseMeetingTestCase):
self.assertContains(r, time_interval)
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number,name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email())))
self.assertContains(r, 'not the official schedule')
# future meeting, no agenda
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=future_meeting.number, ext=".txt")))
r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=future_meeting.number, ext=".txt")))
self.assertContains(r, "There is no agenda available yet.")
self.assertTemplateUsed(r, 'meeting/no-agenda.txt')
# CSV
r = self.client.get(urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number, ext=".csv")))
r = self.client.get(urlreverse("ietf.meeting.views.agenda_plain", kwargs=dict(num=meeting.number, ext=".csv")))
self.assertContains(r, session.group.acronym)
self.assertContains(r, session.group.name)
self.assertContains(r, session.group.parent.acronym.upper())
@ -309,30 +266,11 @@ class MeetingTests(BaseMeetingTestCase):
'ietf.meeting.views.session_details',
kwargs=dict(num=meeting.number, acronym=session.group.acronym)),
msg_prefix='ical should contain link to meeting materials page for session')
self.assertContains(
r,
urlreverse(
'ietf.meeting.views.agenda', kwargs=dict(num=meeting.number)
) + f'#row-{session.official_timeslotassignment().slug()}',
msg_prefix='ical should contain link to agenda entry for session')
# week view
r = self.client.get(urlreverse("ietf.meeting.views.week_view", kwargs=dict(num=meeting.number)))
self.assertNotContains(r, 'CANCELLED')
self.assertContains(r, session.group.acronym)
self.assertContains(r, slot.location.name)
self.assertContains(r, registration_text)
# Floor Plan
r = self.client.get(urlreverse('floor-plan', kwargs=dict(num=meeting.number)))
self.assertEqual(r.status_code, 200)
# week view with a cancelled session
SchedulingEvent.objects.create(
session=session,
status=SessionStatusName.objects.get(slug='canceled'),
by=Person.objects.get(name='(System)')
)
r = self.client.get(urlreverse("ietf.meeting.views.week_view", kwargs=dict(num=meeting.number)))
self.assertContains(r, 'CANCELLED')
self.assertContains(r, session.group.acronym)
self.assertContains(r, slot.location.name)
@override_settings(PROCEEDINGS_V1_BASE_URL='https://example.com/{meeting.number}')
def test_agenda_redirects_for_old_meetings(self):
@ -341,7 +279,7 @@ class MeetingTests(BaseMeetingTestCase):
MeetingFactory(type_id='ietf', number='35', populate_schedule=False)
r = self.client.get(
urlreverse(
'ietf.meeting.views.agenda',
'agenda',
kwargs={'num': '35', 'ext': '.html'},
))
self.assertRedirects(r, 'https://example.com/35', fetch_redirect_response=False)
@ -350,7 +288,7 @@ class MeetingTests(BaseMeetingTestCase):
meeting_with_schedule = MeetingFactory(type_id='ietf', number='36', populate_schedule=True)
r = self.client.get(
urlreverse(
'ietf.meeting.views.agenda',
'agenda',
kwargs={'num': '36', 'ext': '.html'},
))
self.assertRedirects(r, 'https://example.com/36', fetch_redirect_response=False)
@ -359,7 +297,7 @@ class MeetingTests(BaseMeetingTestCase):
SessionFactory(meeting=meeting_with_schedule)
r = self.client.get(
urlreverse(
'ietf.meeting.views.agenda',
'agenda',
kwargs={'num': '36', 'ext': '.html'},
))
self.assertRedirects(r, 'https://example.com/36', fetch_redirect_response=False)
@ -369,203 +307,10 @@ class MeetingTests(BaseMeetingTestCase):
# Meetings pre-64 are redirected, but should be a 404 if there is no Meeting instance
r = self.client.get(
urlreverse(
'ietf.meeting.views.agenda',
'agenda',
kwargs={'num': '32', 'ext': '.html'},
))
self.assertEqual(r.status_code, 404)
# Check a post-64 meeting as well
r = self.client.get(
urlreverse(
'ietf.meeting.views.agenda',
kwargs={'num': '150', 'ext': '.html'},
))
self.assertEqual(r.status_code, 404)
def test_meeting_agenda_filters_ignored(self):
"""The agenda view should ignore filter querystrings
(They are handled by javascript on the front end)
"""
meeting = make_meeting_test_data()
expected_items = meeting.schedule.assignments.exclude(timeslot__type__in=['lead','offagenda'])
expected_rows = ['row-%s' % item.slug() for item in expected_items]
r = self.client.get(urlreverse('ietf.meeting.views.agenda'))
for row_id in expected_rows:
self.assertContains(r, row_id)
r = self.client.get(urlreverse('ietf.meeting.views.agenda') + '?show=mars')
for row_id in expected_rows:
self.assertContains(r, row_id)
r = self.client.get(urlreverse('ietf.meeting.views.agenda') + '?show=mars&hide=ames,mars,plenary,ietf,bof')
for row_id in expected_rows:
self.assertContains(r, row_id)
def test_agenda_iab_session(self):
date = datetime.date.today()
meeting = MeetingFactory(type_id='ietf', date=date )
make_meeting_test_data(meeting=meeting)
iab = Group.objects.get(acronym='iab')
venus = Group.objects.create(
name="Three letter acronym",
acronym="venus",
description="This group discusses exploration of Venus",
state_id="active",
type_id="program",
parent=iab,
list_email="venus@ietf.org",
)
venus_session = SessionFactory(
meeting=meeting,
group=venus,
attendees=10,
requested_duration=datetime.timedelta(minutes=60),
add_to_schedule=False,
)
system_person = Person.objects.get(name="(System)")
SchedulingEvent.objects.create(session=venus_session, status_id='schedw', by=system_person)
room = Room.objects.create(meeting=meeting,
name="Aphrodite",
capacity=100,
functional_name="Aphrodite Room")
room.session_types.add('regular')
session_date = meeting.date + datetime.timedelta(days=1)
slot3 = TimeSlot.objects.create(meeting=meeting, type_id='regular', location=room,
duration=datetime.timedelta(minutes=60),
time=meeting.tz().localize(
datetime.datetime.combine(session_date, datetime.time(13, 30))
))
SchedTimeSessAssignment.objects.create(timeslot=slot3, session=venus_session, schedule=meeting.schedule)
url = urlreverse('ietf.meeting.views.agenda', kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertContains(r, 'venus')
q = PyQuery(r.content)
venus_row = q('[id*="-iab-"]').html()
self.assertIn('venus', venus_row)
def test_agenda_current_audio(self):
date = datetime.date.today()
meeting = MeetingFactory(type_id='ietf', date=date )
make_meeting_test_data(meeting=meeting)
url = urlreverse("ietf.meeting.views.agenda", kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertContains(r, "Audio stream")
def test_agenda_by_room(self):
meeting = make_meeting_test_data()
url = urlreverse("ietf.meeting.views.agenda_by_room",kwargs=dict(num=meeting.number))
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertTrue(all([x in unicontent(r) for x in ['mars','IESG Breakfast','Test Room','Breakfast Room']]))
url = urlreverse("ietf.meeting.views.agenda_by_room",kwargs=dict(num=meeting.number,name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email()))
r = self.client.get(url)
self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room',]]))
self.assertNotContains(r, 'IESG Breakfast')
def test_agenda_by_type(self):
meeting = make_meeting_test_data()
url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number))
login_testing_unauthorized(self,"secretary",url)
r = self.client.get(url)
self.assertTrue(all([x in unicontent(r) for x in ['mars','IESG Breakfast','Test Room','Breakfast Room']]))
url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email()))
r = self.client.get(url)
self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room',]]))
self.assertNotContains(r, 'IESG Breakfast')
url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,type='regular'))
r = self.client.get(url)
self.assertTrue(all([x in unicontent(r) for x in ['mars','Test Room']]))
self.assertFalse(any([x in unicontent(r) for x in ['IESG Breakfast','Breakfast Room']]))
url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,type='lead'))
r = self.client.get(url)
self.assertFalse(any([x in unicontent(r) for x in ['mars','Test Room']]))
self.assertTrue(all([x in unicontent(r) for x in ['IESG Breakfast','Breakfast Room']]))
url = urlreverse("ietf.meeting.views.agenda_by_type",kwargs=dict(num=meeting.number,type='lead',name=meeting.unofficial_schedule.name,owner=meeting.unofficial_schedule.owner.email()))
r = self.client.get(url)
self.assertFalse(any([x in unicontent(r) for x in ['IESG Breakfast','Breakfast Room']]))
def test_agenda_week_view(self):
meeting = make_meeting_test_data()
url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "?show=farfut"
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertTrue(all([x in unicontent(r) for x in ['redraw_weekview', 'draw_calendar', ]]))
# Specifying a time zone should not change the output (time zones are handled by the JS)
url = urlreverse("ietf.meeting.views.week_view",kwargs=dict(num=meeting.number)) + "?show=farfut&" + quote("tz=Asia/Bangkok", safe='=')
r_with_tz = self.client.get(url)
self.assertEqual(r_with_tz.status_code,200)
self.assertEqual(r.content, r_with_tz.content)
def test_agenda_personalize(self):
"""Session selection page should have a checkbox for each session with appropriate keywords"""
meeting = make_meeting_test_data()
url = urlreverse("ietf.meeting.views.agenda_personalize",kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
for assignment in SchedTimeSessAssignment.objects.filter(
schedule__in=[meeting.schedule, meeting.schedule.base],
session__on_agenda=True,
):
row = q('#row-{}'.format(assignment.slug()))
self.assertIsNotNone(row, 'No row for assignment {}'.format(assignment))
checkboxes = row('input[type="checkbox"][name="selected-sessions"]')
self.assertEqual(len(checkboxes), 1,
'Row for assignment {} does not have a checkbox input'.format(assignment))
checkbox = checkboxes.eq(0)
kw_token = assignment.session.docname_token_only_for_multiple()
self.assertEqual(
checkbox.attr('data-filter-item'),
assignment.session.group.acronym.lower() + (
'' if kw_token is None else f'-{kw_token}'
)
)
def test_agenda_personalize_updates_urls(self):
"""The correct URLs should be updated when filter settings change on the personalize agenda view
Tests that the expected elements have the necessary classes. The actual update of these fields
is tested in the JS tests
"""
meeting = make_meeting_test_data()
url = urlreverse("ietf.meeting.views.agenda_personalize",kwargs=dict(num=meeting.number))
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
# Find all the elements expected to be updated
expected_elements = []
nav_tab_anchors = q('ul.nav.nav-tabs > li > a')
for anchor in nav_tab_anchors.items():
text = anchor.text().strip()
if text in ['Agenda (New)', 'Agenda', 'UTC agenda', 'Personalize agenda']:
expected_elements.append(anchor)
for btn in q('.buttonlist a.btn').items():
text = btn.text().strip()
if text in ['View personal agenda', 'Download .ics of filtered agenda', 'Subscribe to filtered agenda']:
expected_elements.append(btn)
# Check that all the expected elements have the correct classes
for elt in expected_elements:
self.assertTrue(elt.has_class('agenda-link'))
self.assertTrue(elt.has_class('filterable'))
# Finally, check that there are no unexpected elements marked to be updated.
# If there are, they should be added to the test above.
self.assertEqual(len(expected_elements),
len(q('.agenda-link.filterable')),
'Unexpected elements updated')
@override_settings(MEETING_MATERIALS_SERVE_LOCALLY=False, MEETING_DOC_HREFS = settings.MEETING_DOC_CDN_HREFS)
def test_materials_through_cdn(self):
@ -834,40 +579,6 @@ class MeetingTests(BaseMeetingTestCase):
self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S'))
self.assertNotContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S'))
def test_meeting_agenda_has_static_ical_links(self):
"""Links to the agenda_ical view must appear on the agenda page
Confirms that these have the correct querystrings. Does not test the JS-based
'Customized schedule' button.
"""
meeting = make_meeting_test_data()
# get the agenda
url = urlreverse('ietf.meeting.views.agenda', kwargs=dict(num=meeting.number))
r = self.client.get(url)
# Check that it has the links we expect
ical_url = urlreverse('ietf.meeting.views.agenda_ical', kwargs=dict(num=meeting.number))
q = PyQuery(r.content)
content = q('#content').html()
assignments = meeting.schedule.assignments.exclude(timeslot__type__in=['lead', 'offagenda'])
# Assume the test meeting is not using historic groups
groups = [a.session.group for a in assignments if a.session is not None]
for g in groups:
if g.parent_id is not None:
self.assertIn('%s?show=%s' % (ical_url, g.parent.acronym.lower()), content)
# The 'non-area events' are those whose keywords are in the last column of buttons
na_col = q('#customize .col-1:last') # find the column
non_area_labels = [e.attrib['data-filter-item']
for e in na_col.find('button.pickview')]
assert len(non_area_labels) > 0 # test setup must produce at least one label for this test
# Should be a 'non-area events' link showing appropriate types
self.assertIn('%s?show=%s' % (ical_url, ','.join(non_area_labels).lower()), content)
def test_parse_agenda_filter_params(self):
def _r(show=(), hide=(), showtypes=(), hidetypes=()):
"""Helper to create expected result dict"""
@ -4102,12 +3813,6 @@ class SessionDetailsTests(TestCase):
self.assertTrue(all([x in unicontent(r) for x in ('slides','agenda','minutes','draft')]))
self.assertNotContains(r, 'deleted')
q = PyQuery(r.content)
self.assertTrue(q('div#session-buttons-%s' % session.id),
'Session detail page does not contain session tool buttons')
self.assertFalse(q('div#session-buttons-%s span.bi-arrows-fullscreen' % session.id),
'The session detail page is incorrectly showing the "Show meeting materials" button')
def test_session_details_has_import_minutes_buttons(self):
group = GroupFactory.create(
type_id='wg',
@ -5783,25 +5488,6 @@ class AjaxTests(TestCase):
self.assertNotIn('error', data)
self.assertEqual(data['utc'], '20:00')
class FloorPlanTests(TestCase):
def test_floor_plan_page(self):
make_meeting_test_data()
meeting = Meeting.objects.filter(type_id='ietf').order_by('id').last()
floorplan = FloorPlanFactory.create(meeting=meeting)
# Extremely rudimentary test of floor-plan-neue
url = urlreverse('floor-plan-neue')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
url = urlreverse('ietf.meeting.views.floor_plan')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
url = urlreverse('ietf.meeting.views.floor_plan', kwargs={'floor': xslugify(floorplan.name)} )
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
class IphoneAppJsonTests(TestCase):
def test_iphone_app_json_interim(self):
make_interim_test_data()

View file

@ -7,6 +7,13 @@ from django.conf import settings
from ietf.meeting import views, views_proceedings
from ietf.utils.urls import url
class AgendaRedirectView(RedirectView):
ignore_kwargs = ('owner', 'name')
def get_redirect_url(self, *args, **kwargs):
for kwarg in self.ignore_kwargs:
kwargs.pop(kwarg, None)
return super().get_redirect_url(*args, **kwargs)
safe_for_all_meeting_types = [
url(r'^session/(?P<acronym>[-a-z0-9]+)/?$', views.session_details),
url(r'^session/(?P<session_id>\d+)/drafts$', views.add_session_drafts),
@ -33,16 +40,13 @@ type_ietf_only_patterns = [
url(r'^agenda/%(owner)s/%(schedule_name)s/delete$' % settings.URL_REGEXPS, views.delete_schedule),
url(r'^agenda/%(owner)s/%(schedule_name)s/make_official$' % settings.URL_REGEXPS, views.make_schedule_official),
url(r'^agenda/%(owner)s/%(schedule_name)s(\.(?P<ext>.html))?/?$' % settings.URL_REGEXPS, views.agenda),
url(r'^agenda/%(owner)s/%(schedule_name)s/week-view(?:.html)?/?$' % settings.URL_REGEXPS, views.week_view),
url(r'^agenda/%(owner)s/%(schedule_name)s/by-room/?$' % settings.URL_REGEXPS, views.agenda_by_room),
url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/?$' % settings.URL_REGEXPS, views.agenda_by_type),
url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/(?P<type>[a-z]+)$' % settings.URL_REGEXPS, views.agenda_by_type),
url(r'^agenda/%(owner)s/%(schedule_name)s/week-view(?:.html)?/?$' % settings.URL_REGEXPS, AgendaRedirectView.as_view(pattern_name='agenda', permanent=True)),
url(r'^agenda/%(owner)s/%(schedule_name)s/by-room/?$' % settings.URL_REGEXPS, AgendaRedirectView.as_view(pattern_name='agenda', permanent=True)),
url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/?$' % settings.URL_REGEXPS, AgendaRedirectView.as_view(pattern_name='agenda', permanent=True)),
url(r'^agenda/%(owner)s/%(schedule_name)s/by-type/(?P<type>[a-z]+)$' % settings.URL_REGEXPS, AgendaRedirectView.as_view(pattern_name='agenda', permanent=True)),
url(r'^agenda/%(owner)s/%(schedule_name)s/new/$' % settings.URL_REGEXPS, views.new_meeting_schedule),
url(r'^agenda/by-room$', views.agenda_by_room),
url(r'^agenda/by-type$', views.agenda_by_type),
url(r'^agenda/by-type/(?P<type>[a-z]+)$', views.agenda_by_type),
url(r'^agenda/by-type/(?P<type>[a-z]+)/ics$', views.agenda_by_type_ics),
url(r'^agenda/personalize', views.agenda_personalize),
url(r'^agenda/personalize', views.agenda, name='agenda-personalize'),
url(r'^agendas/list$', views.list_schedules),
url(r'^agendas/edit$', RedirectView.as_view(pattern_name='ietf.meeting.views.list_schedules', permanent=True)),
url(r'^agendas/diff/$', views.diff_schedules),
@ -64,10 +68,9 @@ type_interim_patterns = [
]
type_ietf_only_patterns_id_optional = [
url(r'^agenda(?P<utc>-utc)?(?P<ext>\.html)?/?$', views.agenda),
url(r'^agenda(?P<ext>\.txt)$', views.agenda),
url(r'^agenda(?P<ext>\.csv)$', views.agenda),
url(r'^agenda-neue(?P<utc>-utc)?(?P<ext>\.html)?/?$', views.agenda_neue, name='agenda-neue'),
url(r'^agenda(?P<utc>-utc)?(?P<ext>\.html)?/?$', views.agenda, name='agenda'),
url(r'^agenda(?P<ext>\.txt)$', views.agenda_plain),
url(r'^agenda(?P<ext>\.csv)$', views.agenda_plain),
url(r'^agenda/edit$',
RedirectView.as_view(pattern_name='ietf.meeting.views.edit_meeting_schedule', permanent=True),
name='ietf.meeting.views.edit_meeting_schedule'),
@ -76,11 +79,10 @@ type_ietf_only_patterns_id_optional = [
url(r'^agenda/agenda\.ics$', views.agenda_ical),
url(r'^agenda\.ics$', views.agenda_ical),
url(r'^agenda.json$', views.agenda_json),
url(r'^agenda/week-view(?:.html)?/?$', views.week_view),
url(r'^floor-plan/?$', views.floor_plan),
url(r'^floor-plan-neue/?$', views.agenda_neue, name='floor-plan-neue'),
url(r'^floor-plan/(?P<floor>[-a-z0-9_]+)/?$', views.floor_plan),
url(r'^week-view(?:.html)?/?$', views.week_view),
url(r'^agenda/week-view(?:.html)?/?$', RedirectView.as_view(pattern_name='agenda', permanent=True)),
url(r'^floor-plan/?$', views.agenda, name='floor-plan'),
url(r'^floor-plan/(?P<floor>[-a-z0-9_]+)/?$', RedirectView.as_view(pattern_name='floor-plan', permanent=True)),
url(r'^week-view(?:.html)?/?$', RedirectView.as_view(pattern_name='agenda', permanent=True)),
url(r'^materials(?:.html)?/?$', views.materials),
url(r'^request_minutes/?$', views.request_minutes),
url(r'^materials/%(document)s((?P<ext>\.[a-z0-9]+)|/)?$' % settings.URL_REGEXPS, views.materials_document),

View file

@ -23,7 +23,7 @@ from ietf.meeting.models import Session, SchedulingEvent, TimeSlot, Constraint,
from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent
from ietf.group.models import Group
from ietf.group.utils import can_manage_materials
from ietf.name.models import SessionStatusName, ConstraintName
from ietf.name.models import SessionStatusName, ConstraintName, DocTypeName
from ietf.person.models import Person
from ietf.secr.proceedings.proc_utils import import_audio_files
from ietf.utils.html import sanitize_document
@ -723,3 +723,35 @@ def handle_upload_file(file, filename, meeting, subdir, request=None, encoding=N
subprocess.call(['unzip', filename], cwd=path)
return None
def new_doc_for_session(type_id, session):
typename = DocTypeName.objects.get(slug=type_id)
ota = session.official_timeslotassignment()
if ota is None:
return None
sess_time = ota.timeslot.local_start_time()
if session.meeting.type_id == "ietf":
name = f"{typename.prefix}-{session.meeting.number}-{session.group.acronym}-{sess_time.strftime('%Y%m%d%H%M')}"
title = f"{typename.name} IETF{session.meeting.number}: {session.group.acronym}: {sess_time.strftime('%a %H:%M')}"
else:
name = f"{typename.prefix}-{session.meeting.number}-{sess_time.strftime('%Y%m%d%H%M')}"
title = f"{typename.name} {session.meeting.number}: {sess_time.strftime('%a %H:%M')}"
doc = Document.objects.create(
name = name,
type_id = type_id,
title = title,
group = session.group,
rev = '00',
)
doc.states.add(State.objects.get(type_id=type_id, slug='active'))
DocAlias.objects.create(name=doc.name).docs.add(doc)
session.sessionpresentation_set.create(document=doc,rev='00')
return doc
def write_doc_for_session(session, type_id, filename, contents):
filename = Path(filename)
path = Path(session.meeting.get_materials_path()) / type_id
path.mkdir(parents=True, exist_ok=True)
with open(path / filename, "wb") as file:
file.write(contents.encode('utf-8'))
return

View file

@ -81,6 +81,7 @@ from ietf.meeting.utils import preprocess_constraints_for_meeting_schedule_edito
from ietf.meeting.utils import diff_meeting_schedules, prefetch_schedule_diff_objects
from ietf.meeting.utils import swap_meeting_schedule_timeslot_assignments, bulk_create_timeslots
from ietf.meeting.utils import preprocess_meeting_important_dates
from ietf.meeting.utils import new_doc_for_session, write_doc_for_session
from ietf.message.utils import infer_message
from ietf.name.models import SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName
from ietf.secr.proceedings.proc_utils import (get_progress_stats, post_process, import_audio_files,
@ -1514,11 +1515,10 @@ def get_assignments_for_agenda(schedule):
@ensure_csrf_cookie
def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
def agenda_plain(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
base = base if base else 'agenda'
ext = ext if ext else '.html'
ext = ext if ext else '.txt'
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,
}
@ -1589,7 +1589,7 @@ def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""
return rendered_page
@ensure_csrf_cookie
def agenda_neue(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
def agenda(request, num=None, name=None, base=None, ext=None, owner=None, utc=""):
# Get current meeting if not specified
if num is None:
num = get_current_ietf_meeting_num()
@ -1605,7 +1605,7 @@ def agenda_neue(request, num=None, name=None, base=None, ext=None, owner=None, u
else:
return HttpResponseRedirect(f'{settings.PROCEEDINGS_V1_BASE_URL.format(meeting=meeting)}')
return render(request, "meeting/agenda-neue.html", {
return render(request, "meeting/agenda.html", {
"meetingData": {
"meetingNumber": num
}
@ -1662,10 +1662,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,
@ -1720,7 +1742,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
@ -1853,47 +1878,6 @@ def agenda_csv(schedule, filtered_assignments):
return response
@role_required('Area Director','Secretariat','IAB')
def agenda_by_room(request, num=None, name=None, owner=None):
meeting = get_meeting(num)
if name is None:
schedule = get_schedule(meeting)
else:
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base if schedule else None]
).prefetch_related('timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent')
ss_by_day = {}
for ss in assignments.order_by('timeslot__location__functional_name','timeslot__location__name','timeslot__time'):
# sorts by time within each day but days are not in order at this point
day = ss.timeslot.time.astimezone(meeting.tz()).date()
ss_by_day.setdefault(day, []).append(ss)
ss_by_day = OrderedDict((key, ss_by_day[key]) for key in sorted(ss_by_day)) # fix day ordering
with timezone.override(meeting.tz()):
return render(request,"meeting/agenda_by_room.html",{"meeting":meeting,"schedule":schedule,"ss_by_day":ss_by_day})
@role_required('Area Director','Secretariat','IAB')
def agenda_by_type(request, num=None, type=None, name=None, owner=None):
meeting = get_meeting(num)
if name is None:
schedule = get_schedule(meeting)
else:
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base if schedule else None]
).prefetch_related(
'timeslot', 'timeslot__location', 'session', 'session__group', 'session__group__parent'
).order_by('session__type__slug','timeslot__time','session__group__acronym')
if type:
assignments = assignments.filter(session__type__slug=type)
with timezone.override(meeting.tz()):
return render(request,"meeting/agenda_by_type.html",{"meeting":meeting,"schedule":schedule,"assignments":assignments})
@role_required('Area Director','Secretariat','IAB')
def agenda_by_type_ics(request,num=None,type=None):
meeting = get_meeting(num)
@ -1908,42 +1892,6 @@ def agenda_by_type_ics(request,num=None,type=None):
updated = meeting.updated()
return render(request,"meeting/agenda.ics",{"schedule":schedule,"updated":updated,"assignments":assignments},content_type="text/calendar")
def agenda_personalize(request, num):
meeting = get_ietf_meeting(num) # num may be None, which requests the current meeting
if meeting is None or meeting.schedule is None:
raise Http404('No such meeting')
# Select and prepare sessions that should be included
filtered_assignments = preprocess_assignments_for_agenda(
get_assignments_for_agenda(meeting.schedule),
meeting
)
tagger = AgendaKeywordTagger(assignments=filtered_assignments)
tagger.apply() # annotate assignments with filter_keywords attribute
tagger.apply_session_keywords() # annotate assignments with session_keyword attribute
# Now prep the filter UI
filter_organizer = AgendaFilterOrganizer(assignments=filtered_assignments)
is_current_meeting = (num is None) or (num == get_current_ietf_meeting_num())
return render(
request,
"meeting/agenda.html",
{
'personalize': True,
'schedule': meeting.schedule,
'updated': meeting.updated(),
'filtered_assignments': filtered_assignments,
'filter_categories': filter_organizer.get_filter_categories(),
'non_area_labels': filter_organizer.get_non_area_keywords(),
'display_timezone': meeting.time_zone,
'is_current_meeting': is_current_meeting,
'cache_time': 150 if is_current_meeting else 3600,
}
)
def session_draft_list(num, acronym):
try:
agendas = Document.objects.filter(type="agenda",
@ -2046,70 +1994,6 @@ def session_draft_pdf(request, num, acronym):
os.unlink(pdfn)
return HttpResponse(pdf_contents, content_type="application/pdf")
def week_view(request, num=None, name=None, owner=None):
meeting = get_meeting(num)
if name is None:
schedule = get_schedule(meeting)
else:
person = get_person_by_email(owner)
schedule = get_schedule_by_name(meeting, person, name)
if not schedule:
raise Http404
filtered_assignments = SchedTimeSessAssignment.objects.filter(
schedule__in=[schedule, schedule.base],
session__on_agenda=True,
)
filtered_assignments = preprocess_assignments_for_agenda(filtered_assignments, meeting)
AgendaKeywordTagger(assignments=filtered_assignments).apply()
items = []
for a in filtered_assignments:
# we don't HTML escape any of these as the week-view code is using createTextNode
item = {
"key": str(a.timeslot.pk),
"utc_time": a.timeslot.utc_start_time().strftime("%Y%m%dT%H%MZ"), # ISO8601 compliant
"duration": a.timeslot.duration.seconds,
"type": a.slot_type().name,
"filter_keywords": ",".join(a.filter_keywords),
}
if a.session:
if a.session.historic_group:
item["group"] = a.session.historic_group.acronym
if a.session.name:
item["name"] = a.session.name
elif a.slot_type().slug == "break":
item["name"] = a.timeslot.name
item["area"] = a.slot_type().slug
item["group"] = a.slot_type().slug
elif a.session.historic_group:
item["name"] = a.session.historic_group.name
if a.session.historic_group.state_id == "bof":
item["name"] += " BOF"
item["state"] = a.session.historic_group.state.name
if a.session.historic_group.historic_parent:
item["area"] = a.session.historic_group.historic_parent.acronym
if a.timeslot.show_location:
item["room"] = a.timeslot.get_location()
if a.session and a.session.agenda():
item["agenda"] = a.session.agenda().get_href()
if a.session.current_status == 'canceled':
item["name"] = "CANCELLED - " + item["name"]
items.append(item)
return render(request, "meeting/week-view.html", {
"items": json.dumps(items),
})
def ical_session_status(assignment):
if assignment.session.current_status == 'canceled':
return "CANCELLED"
@ -2446,6 +2330,7 @@ def session_details(request, num, acronym):
session.filtered_artifacts.sort(key=lambda d:['agenda','minutes','bluesheets'].index(d.document.type.slug))
session.filtered_slides = session.sessionpresentation_set.filter(document__type__slug='slides').order_by('order')
session.filtered_drafts = session.sessionpresentation_set.filter(document__type__slug='draft')
session.filtered_chatlog_and_polls = session.sessionpresentation_set.filter(document__type__slug__in=('chatlog', 'polls')).order_by('document__type__slug')
# TODO FIXME Deleted materials shouldn't be in the sessionpresentation_set
for qs in [session.filtered_artifacts,session.filtered_slides,session.filtered_drafts]:
qs = [p for p in qs if p.document.get_state_slug(p.document.type_id)!='deleted']
@ -3718,24 +3603,6 @@ def upcoming_json(request):
response = HttpResponse(json.dumps(data, indent=2, sort_keys=False), content_type='application/json;charset=%s'%settings.DEFAULT_CHARSET)
return response
def floor_plan(request, num=None, floor=None, ):
meeting = get_meeting(num)
schedule = meeting.schedule
floors = FloorPlan.objects.filter(meeting=meeting).order_by('order')
if floor:
floors = [ f for f in floors if xslugify(f.name) == floor ]
for floor in floors:
try:
floor.image.width
except FileNotFoundError:
raise Http404('Missing floorplan image for %s' % floor)
return render(request, 'meeting/floor-plan.html', {
"meeting": meeting,
"schedule": schedule,
"number": num,
"floors": floors,
})
def proceedings(request, num=None):
meeting = get_meeting(num)
@ -3978,6 +3845,85 @@ def api_add_session_attendees(request):
session.attended_set.get_or_create(person=user.person)
return HttpResponse("Done", status=200, content_type='text/plain')
@require_api_key
@role_required('Recording Manager')
@csrf_exempt
def api_upload_chatlog(request):
def err(code, text):
return HttpResponse(text, status=code, content_type='text/plain')
if request.method != 'POST':
return err(405, "Method not allowed")
apidata_post = request.POST.get('apidata')
if not apidata_post:
return err(400, "Missing apidata parameter")
try:
apidata = json.loads(apidata_post)
except json.decoder.JSONDecodeError:
return err(400, "Malformed post")
if not ( 'session_id' in apidata and type(apidata['session_id']) is int ):
return err(400, "Malformed post")
session_id = apidata['session_id']
if not ( 'chatlog' in apidata and type(apidata['chatlog']) is list and all([type(el) is dict for el in apidata['chatlog']]) ):
return err(400, "Malformed post")
session = Session.objects.filter(pk=session_id).first()
if not session:
return err(400, "Invalid session")
chatlog_sp = session.sessionpresentation_set.filter(document__type='chatlog').first()
if chatlog_sp:
doc = chatlog_sp.document
doc.rev = f"{(int(doc.rev)+1):02d}"
chatlog_sp.rev = doc.rev
chatlog_sp.save()
else:
doc = new_doc_for_session('chatlog', session)
if doc is None:
return err(400, "Could not find official timeslot for session")
filename = f"{doc.name}-{doc.rev}.json"
doc.uploaded_filename = filename
write_doc_for_session(session, 'chatlog', filename, json.dumps(apidata['chatlog']))
e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev)
doc.save_with_history([e])
return HttpResponse("Done", status=200, content_type='text/plain')
@require_api_key
@role_required('Recording Manager')
@csrf_exempt
def api_upload_polls(request):
def err(code, text):
return HttpResponse(text, status=code, content_type='text/plain')
if request.method != 'POST':
return err(405, "Method not allowed")
apidata_post = request.POST.get('apidata')
if not apidata_post:
return err(400, "Missing apidata parameter")
try:
apidata = json.loads(apidata_post)
except json.decoder.JSONDecodeError:
return err(400, "Malformed post")
if not ( 'session_id' in apidata and type(apidata['session_id']) is int ):
return err(400, "Malformed post")
session_id = apidata['session_id']
if not ( 'polls' in apidata and type(apidata['polls']) is list and all([type(el) is dict for el in apidata['polls']]) ):
return err(400, "Malformed post")
session = Session.objects.filter(pk=session_id).first()
if not session:
return err(400, "Invalid session")
polls_sp = session.sessionpresentation_set.filter(document__type='polls').first()
if polls_sp:
doc = polls_sp.document
doc.rev = f"{(int(doc.rev)+1):02d}"
polls_sp.rev = doc.rev
polls_sp.save()
else:
doc = new_doc_for_session('polls', session)
if doc is None:
return err(400, "Could not find official timeslot for session")
filename = f"{doc.name}-{doc.rev}.json"
doc.uploaded_filename = filename
write_doc_for_session(session, 'polls', filename, json.dumps(apidata['polls']))
e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev)
doc.save_with_history([e])
return HttpResponse("Done", status=200, content_type='text/plain')
@require_api_key
@role_required('Recording Manager', 'Secretariat')

View file

@ -2200,7 +2200,7 @@
},
{
"fields": {
"desc": "The IESG has not started processing this draft, or has stopped processing it without publicastion.",
"desc": "The IESG has not started processing this draft, or has stopped processing it without publication.",
"name": "I-D Exists",
"next_states": [
16,
@ -2405,6 +2405,58 @@
"model": "doc.state",
"pk": 164
},
{
"fields": {
"desc": "",
"name": "Active",
"next_states": [],
"order": 0,
"slug": "active",
"type": "chatlog",
"used": true
},
"model": "doc.state",
"pk": 165
},
{
"fields": {
"desc": "",
"name": "Deleted",
"next_states": [],
"order": 0,
"slug": "deleted",
"type": "chatlog",
"used": true
},
"model": "doc.state",
"pk": 166
},
{
"fields": {
"desc": "",
"name": "Active",
"next_states": [],
"order": 0,
"slug": "active",
"type": "polls",
"used": true
},
"model": "doc.state",
"pk": 167
},
{
"fields": {
"desc": "",
"name": "Deleted",
"next_states": [],
"order": 0,
"slug": "deleted",
"type": "polls",
"used": true
},
"model": "doc.state",
"pk": 168
},
{
"fields": {
"label": "State"
@ -2433,6 +2485,13 @@
"model": "doc.statetype",
"pk": "charter"
},
{
"fields": {
"label": "State"
},
"model": "doc.statetype",
"pk": "chatlog"
},
{
"fields": {
"label": "Conflict Review State"
@ -2538,6 +2597,13 @@
"model": "doc.statetype",
"pk": "minutes"
},
{
"fields": {
"label": "State"
},
"model": "doc.statetype",
"pk": "polls"
},
{
"fields": {
"label": "Proceedings Materials State"
@ -3345,7 +3411,7 @@
"has_session_materials": false,
"is_schedulable": false,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[]",
"matman_roles": "[\n \"chair\"\n]",
"need_parent": false,
"parent_types": [],
"req_subm_approval": true,
@ -3458,8 +3524,8 @@
"has_milestones": false,
"has_nonsession_materials": true,
"has_reviews": false,
"has_session_materials": false,
"is_schedulable": false,
"has_session_materials": true,
"is_schedulable": true,
"material_types": "[\n \"slides\"\n]",
"matman_roles": "[\n \"chair\",\n \"matman\"\n]",
"need_parent": false,
@ -3469,7 +3535,7 @@
"req_subm_approval": false,
"role_order": "[\n \"chair\",\n \"member\",\n \"matman\"\n]",
"session_purposes": "[\n \"coding\",\n \"presentation\",\n \"social\",\n \"tutorial\"\n]",
"show_on_agenda": false
"show_on_agenda": true
},
"model": "group.groupfeatures",
"pk": "team"
@ -10126,6 +10192,17 @@
"model": "name.doctypename",
"pk": "charter"
},
{
"fields": {
"desc": "",
"name": "Chat Log",
"order": 0,
"prefix": "chatlog",
"used": true
},
"model": "name.doctypename",
"pk": "chatlog"
},
{
"fields": {
"desc": "",
@ -10181,6 +10258,17 @@
"model": "name.doctypename",
"pk": "minutes"
},
{
"fields": {
"desc": "",
"name": "Polls",
"order": 0,
"prefix": "polls",
"used": true
},
"model": "name.doctypename",
"pk": "polls"
},
{
"fields": {
"desc": "",
@ -15998,7 +16086,7 @@
"fields": {
"command": "xym",
"switch": "--version",
"time": "2022-07-13T00:09:29.108Z",
"time": "2022-09-22T00:09:27.552Z",
"used": true,
"version": "xym 0.5"
},
@ -16009,7 +16097,7 @@
"fields": {
"command": "pyang",
"switch": "--version",
"time": "2022-07-13T00:09:29.475Z",
"time": "2022-09-22T00:09:27.867Z",
"used": true,
"version": "pyang 2.5.3"
},
@ -16020,7 +16108,7 @@
"fields": {
"command": "yanglint",
"switch": "--version",
"time": "2022-07-13T00:09:29.497Z",
"time": "2022-09-22T00:09:27.886Z",
"used": true,
"version": "yanglint SO 1.9.2"
},
@ -16031,9 +16119,9 @@
"fields": {
"command": "xml2rfc",
"switch": "--version",
"time": "2022-07-13T00:09:30.513Z",
"time": "2022-09-22T00:09:28.809Z",
"used": true,
"version": "xml2rfc 3.13.0"
"version": "xml2rfc 3.14.2"
},
"model": "utils.versioninfo",
"pk": 4

View file

@ -0,0 +1,35 @@
# Copyright The IETF Trust 2022, All Rights Reserved
from django.db import migrations
def forward(apps, schema_editor):
DocTypeName = apps.get_model("name", "DocTypeName")
DocTypeName.objects.create(
slug = "chatlog",
name = "Chat Log",
prefix = "chatlog",
desc = "",
order = 0,
used = True,
)
DocTypeName.objects.create(
slug = "polls",
name = "Polls",
prefix = "polls",
desc = "",
order = 0,
used = True,
)
def reverse(apps, schema_editor):
DocTypeName = apps.get_model("name", "DocTypeName")
DocTypeName.objects.filter(slug__in=("chatlog", "polls")).delete()
class Migration(migrations.Migration):
dependencies = [
('name', '0044_validating_draftsubmissionstatename'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -2292,7 +2292,7 @@ class rfc8713EligibilityTests(TestCase):
for combo in combinations(meetings,combo_len):
p = PersonFactory()
for m in combo:
MeetingRegistrationFactory(person=p, meeting=m)
MeetingRegistrationFactory(person=p, meeting=m, attended=True)
if combo_len<3:
self.ineligible_people.append(p)
else:
@ -2305,7 +2305,7 @@ class rfc8713EligibilityTests(TestCase):
self.other_date = datetime.date(2009,5,1)
self.other_people = PersonFactory.create_batch(1)
for date in (datetime.date(2009,3,1), datetime.date(2008,11,1), datetime.date(2008,7,1)):
MeetingRegistrationFactory(person=self.other_people[0],meeting__date=date, meeting__type_id='ietf')
MeetingRegistrationFactory(person=self.other_people[0],meeting__date=date, meeting__type_id='ietf', attended=True)
def test_is_person_eligible(self):
@ -2350,7 +2350,7 @@ class rfc8788EligibilityTests(TestCase):
for combo in combinations(meetings,combo_len):
p = PersonFactory()
for m in combo:
MeetingRegistrationFactory(person=p, meeting=m)
MeetingRegistrationFactory(person=p, meeting=m, attended=True)
if combo_len<3:
self.ineligible_people.append(p)
else:
@ -2398,7 +2398,7 @@ class rfc8989EligibilityTests(TestCase):
for combo in combinations(prev_five,combo_len):
p = PersonFactory()
for m in combo:
MeetingRegistrationFactory(person=p, meeting=m)
MeetingRegistrationFactory(person=p, meeting=m, attended=True) # not checkedin because this forces looking at older meetings
AttendedFactory(session__meeting=m, session__type_id='plenary',person=p)
if combo_len<3:
ineligible_people.append(p)
@ -2642,7 +2642,7 @@ class VolunteerTests(TestCase):
self.assertContains(r, 'NomCom is not accepting volunteers at this time', status_code=200)
nomcom.is_accepting_volunteers = True
nomcom.save()
MeetingRegistrationFactory(person=person, affiliation='mtg_affiliation')
MeetingRegistrationFactory(person=person, affiliation='mtg_affiliation', checkedin=True)
r = self.client.get(url)
self.assertContains(r, 'Volunteer for NomCom', status_code=200)
self.assertContains(r, 'mtg_affiliation')
@ -2714,7 +2714,7 @@ class VolunteerDecoratorUnitTests(TestCase):
('106', datetime.date(2019, 11, 16)),
]]
for m in meetings:
MeetingRegistrationFactory(meeting=m,person=meeting_person)
MeetingRegistrationFactory(meeting=m, person=meeting_person, attended=True)
AttendedFactory(session__meeting=m, session__type_id='plenary', person=meeting_person)
nomcom.volunteer_set.create(person=meeting_person)

View file

@ -0,0 +1,17 @@
# Copyright The IETF Trust 2022, All Rights Reserved
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('person', '0024_pronouns'),
]
operations = [
migrations.AlterField(
model_name='personalapikey',
name='endpoint',
field=models.CharField(choices=[('/api/appauth/authortools', '/api/appauth/authortools'), ('/api/appauth/bibxml', '/api/appauth/bibxml'), ('/api/iesg/position', '/api/iesg/position'), ('/api/meeting/session/video/url', '/api/meeting/session/video/url'), ('/api/notify/meeting/bluesheet', '/api/notify/meeting/bluesheet'), ('/api/notify/meeting/registration', '/api/notify/meeting/registration'), ('/api/notify/session/attendees', '/api/notify/session/attendees'), ('/api/notify/session/chatlog', '/api/notify/session/chatlog'), ('/api/notify/session/polls', '/api/notify/session/polls'), ('/api/v2/person/person', '/api/v2/person/person')], max_length=128),
),
]

View file

@ -7,7 +7,7 @@ import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('person', '0024_pronouns'),
('person', '0025_chat_and_polls_apikey'),
]
operations = [

View file

@ -368,7 +368,9 @@ PERSON_API_KEY_VALUES = [
("/api/meeting/session/video/url", "/api/meeting/session/video/url", "Recording Manager"),
("/api/notify/meeting/registration", "/api/notify/meeting/registration", "Robot"),
("/api/notify/meeting/bluesheet", "/api/notify/meeting/bluesheet", "Recording Manager"),
("/api/notify/session/attendees", "/api/notify/session/attendees", "Recording Manager"),
("/api/notify/session/attendees", "/api/notify/session/attendees", "Recording Manager"),
("/api/notify/session/chatlog", "/api/notify/session/chatlog", "Recording Manager"),
("/api/notify/session/polls", "/api/notify/session/polls", "Recording Manager"),
("/api/appauth/authortools", "/api/appauth/authortools", None),
("/api/appauth/bibxml", "/api/appauth/bibxml", None),
]

View file

@ -1,3 +1,3 @@
<ul>
<li><a href="{% url 'ietf.meeting.views.agenda' num=meeting.number ext='.txt' %}" target="_blank">View Agenda</a>.</li>
<li><a href="{% url 'ietf.meeting.views.agenda_plain' num=meeting.number ext='.txt' %}" target="_blank">View Agenda</a>.</li>
</ul>

View file

@ -889,6 +889,8 @@ MEETING_DOC_LOCAL_HREFS = {
"agenda": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}",
"minutes": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}",
"slides": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}",
"chatlog": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}",
"polls": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}",
"recording": "{doc.external_url}",
"bluesheets": "https://www.ietf.org/proceedings/{meeting.number}/bluesheets/{doc.uploaded_filename}",
"procmaterials": "/meeting/{meeting.number}/materials/{doc.name}-{doc.rev}",

View file

@ -227,13 +227,6 @@ th {
}
}
// Style the navbar user photo
.nav-link .user-photo {
object-fit: cover;
height: 40px;
width: 40px;
}
// Style the righthand navigation panel
#righthand-panel {
max-height: 80vh;

View file

@ -15,4 +15,5 @@ class MeetingRegistrationFactory(factory.django.DjangoModelFactory):
reg_type = 'onsite'
first_name = factory.LazyAttribute(lambda obj: obj.person.first_name())
last_name = factory.LazyAttribute(lambda obj: obj.person.last_name())
attended = True
attended = False
checkedin = False

View file

@ -22,12 +22,13 @@ from ietf.submit.models import Submission
from ietf.doc.factories import WgDraftFactory, WgRfcFactory
from ietf.doc.models import Document, DocAlias, State, RelatedDocument, NewRevisionDocEvent, DocumentAuthor
from ietf.group.factories import RoleFactory
from ietf.meeting.factories import MeetingFactory
from ietf.meeting.factories import MeetingFactory, AttendedFactory
from ietf.person.factories import PersonFactory
from ietf.person.models import Person, Email
from ietf.name.models import FormalLanguageName, DocRelationshipName, CountryName
from ietf.review.factories import ReviewRequestFactory, ReviewerSettingsFactory, ReviewAssignmentFactory
from ietf.stats.models import MeetingRegistration, CountryAlias
from ietf.stats.factories import MeetingRegistrationFactory
from ietf.stats.utils import get_meeting_registration_data
@ -123,11 +124,11 @@ class StatisticsTests(TestCase):
def test_meeting_stats(self):
# create some data for the statistics
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), number="96")
MeetingRegistration.objects.create(first_name='John', last_name='Smith', country_code='US', email="john.smith@example.us", meeting=meeting, attended=True)
MeetingRegistrationFactory(first_name='John', last_name='Smith', country_code='US', email="john.smith@example.us", meeting=meeting, attended=True)
CountryAlias.objects.get_or_create(alias="US", country=CountryName.objects.get(slug="US"))
MeetingRegistration.objects.create(first_name='Jaume', last_name='Guillaume', country_code='FR', email="jaume.guillaume@example.fr", meeting=meeting, attended=True)
p = MeetingRegistrationFactory(first_name='Jaume', last_name='Guillaume', country_code='FR', email="jaume.guillaume@example.fr", meeting=meeting, attended=False).person
CountryAlias.objects.get_or_create(alias="FR", country=CountryName.objects.get(slug="FR"))
AttendedFactory(session__meeting=meeting,person=p)
# check redirect
url = urlreverse(ietf.stats.views.meeting_stats)

View file

@ -7,6 +7,7 @@ import requests
from collections import defaultdict
from django.conf import settings
from django.db.models import Q
import debug # pyflakes:ignore
@ -320,8 +321,10 @@ def get_meeting_registration_data(meeting):
raise RuntimeError("Bad response from registrations API: %s, '%s'" % (response.status_code, response.content))
num_total = MeetingRegistration.objects.filter(
meeting_id=meeting.pk,
attended=True,
reg_type__in=['onsite', 'remote']).count()
reg_type__in=['onsite', 'remote']
).filter(
Q(attended=True) | Q(checkedin=True)
).count()
if meeting.attendees is None or num_total > meeting.attendees:
meeting.attendees = num_total
meeting.save()

View file

@ -821,8 +821,10 @@ def meeting_stats(request, num=None, stats_type=None):
if meeting and any(stats_type == t[0] for t in possible_stats_types):
attendees = MeetingRegistration.objects.filter(
meeting=meeting,
attended=True,
reg_type__in=['onsite', 'remote'])
reg_type__in=['onsite', 'remote']
).filter(
Q( attended=True) | Q( checkedin=True )
)
if stats_type == "country":
stats_title = "Number of attendees for {} {} per country".format(meeting.type.name, meeting.number)
@ -897,7 +899,10 @@ def meeting_stats(request, num=None, stats_type=None):
attendees = MeetingRegistration.objects.filter(
meeting__type="ietf",
attended=True,
reg_type__in=['onsite', 'remote']).select_related('meeting')
reg_type__in=['onsite', 'remote']
).filter(
Q( attended=True) | Q( checkedin=True )
).select_related('meeting')
if stats_type == "overview":
stats_title = "Number of attendees per meeting"

View file

@ -28,6 +28,7 @@
</style>
{% vite_hmr_client %}
{% block pagehead %}{% endblock %}
{% vite_asset 'client/embedded.js' %}
{% include "base/icons.html" %}
<script src="{% static 'ietf/js/ietf.js' %}"></script>
{% analytical_head_bottom %}

View file

@ -236,13 +236,7 @@
{% endif %}
<li>
<a class="dropdown-item {% if flavor != 'top' %}text-wrap link-primary{% endif %}"
href="{% url 'agenda-neue' %}">
Agenda (New)
</a>
</li>
<li>
<a class="dropdown-item {% if flavor != 'top' %}text-wrap link-primary{% endif %}"
href="{% url 'ietf.meeting.views.agenda' %}">
href="{% url 'agenda' %}">
Agenda
</a>
</li>
@ -254,7 +248,7 @@
</li>
<li>
<a class="dropdown-item {% if flavor != 'top' %}text-wrap link-primary{% endif %}"
href="{% url 'ietf.meeting.views.floor_plan' %}">
href="{% url 'floor-plan' %}">
Floor plan
</a>
</li>

View file

@ -5,18 +5,12 @@
<li class="nav-item dropdown">
{% if flavor == "top" %}
<a href="#"
class="nav-link dropdown-toggle{% if user.person.photo_thumb %} p-0{% endif %}"
class="nav-link dropdown-toggle"
role="button"
data-bs-toggle="dropdown"
aria-expanded="false">
{% if user.is_authenticated %}
{% if user.person.photo_thumb %}
<img class="user-photo rounded-circle ms-1" width="40" height="40"
src="{{ user.person.photo_thumb.url }}"
alt="Photo of {{ user.person.name }}">
{% else %}
{{ user.username|split:'@'|first }}
{% endif %}
{{ user.username|split:'@'|first }}
{% else %}
User
{% endif %}

View file

@ -0,0 +1,64 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2022, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load ietf_filters textfilters %}
{% block title %}{{ doc.title|default:"Untitled" }}{% endblock %}
{% block content %}
{% origin %}
{{ top|safe }}
{% include "doc/revisions_list.html" %}
<div id="timeline"></div>
{% if doc.rev != latest_rev %}
<div class="alert alert-warning my-3">The information below is for an old version of the document.</div>
{% endif %}
<table class="table table-sm table-borderless">
<tbody class="meta border-top">
<tr>
<th scope="row">
{% if doc.meeting_related %}Meeting{% endif %}
{{ doc.type.name }}
</th>
<td></td>
<td>
{% if doc.group %}
{{ doc.group.name }}
<a href="{{ doc.group.about_url }}">({{ doc.group.acronym }})</a>
{{ doc.group.type.name }}
{% endif %}
{% if snapshot %}<span class="badge rounded-pill bg-warning">Snapshot</span>{% endif %}
</td>
</tr>
<tr>
<th scope="row">Title</th>
<td class="edit"></td>
<th scope="row">{{ doc.title|default:'<span class="text-muted">(None)</span>' }}</th>
</tr>
<tr>
<th scope="row">Session</th>
<td class="edit">
</td>
<td>
<a href="{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}">Materials</a>
</td>
</tr>
<tr>
<th scope="row">Last updated</th>
<td class="edit"></td>
<td>{{ doc.time|date:"Y-m-d" }}</td>
</tr>
</tbody>
</table>
<div id="materials-content" class="card mt-5">
<div class="card-header">{{ doc.name }}-{{ doc.rev }}</div>
<div class="card-body">
<script id="chat-data" type="application/json">{{ content|safe }}</script>
<div class="vue-embed" data-component="ChatLog" data-component-id="chat">Loading...</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/d3.js' %}"></script>
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,64 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2022, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load ietf_filters textfilters %}
{% block title %}{{ doc.title|default:"Untitled" }}{% endblock %}
{% block content %}
{% origin %}
{{ top|safe }}
{% include "doc/revisions_list.html" %}
<div id="timeline"></div>
{% if doc.rev != latest_rev %}
<div class="alert alert-warning my-3">The information below is for an old version of the document.</div>
{% endif %}
<table class="table table-sm table-borderless">
<tbody class="meta border-top">
<tr>
<th scope="row">
{% if doc.meeting_related %}Meeting{% endif %}
{{ doc.type.name }}
</th>
<td></td>
<td>
{% if doc.group %}
{{ doc.group.name }}
<a href="{{ doc.group.about_url }}">({{ doc.group.acronym }})</a>
{{ doc.group.type.name }}
{% endif %}
{% if snapshot %}<span class="badge rounded-pill bg-warning">Snapshot</span>{% endif %}
</td>
</tr>
<tr>
<th scope="row">Title</th>
<td class="edit"></td>
<th scope="row">{{ doc.title|default:'<span class="text-muted">(None)</span>' }}</th>
</tr>
<tr>
<th scope="row">Session</th>
<td class="edit">
</td>
<td>
<a href="{% url 'ietf.meeting.views.session_details' num=session.meeting.number acronym=session.group.acronym %}">Materials</a>
</td>
</tr>
<tr>
<th scope="row">Last updated</th>
<td class="edit"></td>
<td>{{ doc.time|date:"Y-m-d" }}</td>
</tr>
</tbody>
</table>
<div id="materials-content" class="card mt-5">
<div class="card-header">{{ doc.name }}-{{ doc.rev }}</div>
<div class="card-body">
<script id="polls-data" type="application/json">{{ content|safe }}</script>
<div class="vue-embed" data-component="Polls" data-component-id="polls">Loading...</div>
</div>
</div>
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/d3.js' %}"></script>
<script src="{% static 'ietf/js/document_timeline.js' %}"></script>
{% endblock %}

View file

@ -1,79 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015-2021, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load ietf_filters %}
{% load textfilters %}
{% load htmlfilters agenda_custom_tags %}
{% load django_vite %}
{% block title %}
IETF {{ meetingData.meetingNumber }} Meeting Agenda
{% endblock %}
{% block pagehead %}
<!-- AGENDA VUE COMPONENT -->
<!-- [html-validate-disable-block void-style, attribute-empty-style] -->
{{ meetingData|json_script:"meeting-data" }}
{% vite_asset 'client/main.js' %}
{% endblock %}
{% block morecss %}
body {
font-family: 'Montserrat', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
@keyframes initspinner {
to { transform: rotate(360deg); }
}
#app-loading {
position: fixed;
top: 60px;
left: 0;
width: 100%;
height: calc(100% - 60px);
background-color: rgba(255,255,255,.75);
z-index: 2000000000;
backdrop-filter: blur(10px);
}
#app-loading:before {
content: '';
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
margin-top: -25px;
margin-left: -25px;
border-radius: 50%;
border-top: 2px solid #999;
border-right: 2px solid transparent;
animation: initspinner .6s linear infinite;
z-index: 2000000000;
}
#app-loading:after {
content: 'Loading meeting {{ meetingData.meetingNumber }}...';
position: absolute;
text-align: center;
top: 50%;
margin-top: -100px;
left: 0;
right: 0;
font-weight: 500;
color: #999;
z-index: 2000000000;
}
{% endblock %}
{% block precontent %}
<div class="meeting-switch">
<i class="bi bi-arrow-left-right me-2"></i>
<a href="{% url 'ietf.meeting.views.agenda' num=meetingData.meetingNumber %}">Switch to Legacy Agenda Display</a>
</div>
{% endblock %}
{% block content %}
{% origin %}
<div id="app"></div>
<div id="app-loading"></div>
{% endblock %}

View file

@ -4,531 +4,69 @@
{% load static %}
{% load ietf_filters %}
{% load textfilters %}
{% load htmlfilters agenda_custom_tags tz %}
{% load htmlfilters agenda_custom_tags %}
{% load django_vite %}
{% block title %}
IETF {{ schedule.meeting.number }} Meeting Agenda
{% if "-utc" in request.path %}(UTC){% endif %}
{% if personalize %}Personalization{% endif %}
IETF {{ meetingData.meetingNumber }} Meeting Agenda
{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
<!-- AGENDA VUE COMPONENT -->
<!-- [html-validate-disable-block void-style, attribute-empty-style] -->
{{ meetingData|json_script:"meeting-data" }}
{% vite_asset 'client/main.js' %}
{% endblock %}
{% block morecss %}#weekview iframe { height: 25em; }{% endblock %}
{% block precontent %}
<div class="meeting-switch">
<i class="bi bi-arrow-left-right me-2"></i>
<a href="{% url 'agenda-neue' num=schedule.meeting.number %}">Switch to New Agenda Display</a>
</div>
{% block morecss %}
body {
font-family: 'Montserrat', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
@keyframes initspinner {
to { transform: rotate(360deg); }
}
#app-loading {
position: fixed;
top: 60px;
left: 0;
width: 100%;
height: calc(100% - 60px);
background-color: rgba(255,255,255,.75);
z-index: 2000000000;
backdrop-filter: blur(10px);
}
#app-loading:before {
content: '';
box-sizing: border-box;
position: absolute;
top: 50%;
left: 50%;
width: 50px;
height: 50px;
margin-top: -25px;
margin-left: -25px;
border-radius: 50%;
border-top: 2px solid #999;
border-right: 2px solid transparent;
animation: initspinner .6s linear infinite;
z-index: 2000000000;
}
#app-loading:after {
content: 'Loading meeting {{ meetingData.meetingNumber }}...';
position: absolute;
text-align: center;
top: 50%;
margin-top: -100px;
left: 0;
right: 0;
font-weight: 500;
color: #999;
z-index: 2000000000;
}
{% endblock %}
{% block content %}
{% origin %}
{% if "-utc" in request.path %}
{% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="agenda-utc" title_extra="(UTC)" %}
{% elif personalize %}
{% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="select-sessions" title_extra="" %}
{% else %}
{% include "meeting/meeting_heading.html" with meeting=schedule.meeting updated=updated selected="agenda" title_extra="" %}
{% endif %}
{# the contents of #extra-nav will be moved into the RH nav panel #}
<div id="extra-nav" class="d-none">
<div class="d-flex flex-column px-2">
{% if now.date <= schedule.meeting.end_date %}
<a class="btn btn-sm btn-primary my-3 now-link" href="#">Jump to current session</a>
{% endif %}
<div class="d-flex">
{% include 'meeting/tz-display.html' with id_suffix="-rh" meeting_timezone=display_timezone minimal=True only %}
</div>
<div class="small text-muted">
Showing <span class="current-tz">{{ display_timezone|split:"_"|join:" "|split:"/"|join:" / " }}</span> time
</div>
</div>
</div>
{# cache this part -- it takes 3-6 seconds to generate #}
{% load cache %}
{% cache cache_time ietf_meeting_agenda_utc schedule.meeting.number request.path %}
<h2>
{% if personalize %}
Session selection
{% else %}
Agenda
{% endif %}
</h2>
{% if is_current_meeting %}
<p class="alert alert-info my-3">
<b>Note:</b> IETF agendas are subject to change, up to and during a meeting.
</p>
{% endif %}
{% if schedule.meeting.agenda_info_note %}
<p class="alert alert-info my-3">
{{ schedule.meeting.agenda_info_note|removetags:"h1"|safe }}
</p>
{% endif %}
{% include 'meeting/tz-display.html' with id_suffix="" meeting_timezone=display_timezone only %}
{% include "meeting/agenda_filter.html" with filter_categories=filter_categories customize_button_text="Filter this agenda view..." always_show=personalize %}
{% include "meeting/agenda_personalize_buttonlist.html" with meeting=schedule.meeting personalize=personalize only %}
<div class="input-group mb-3">
<button class="btn btn-outline-primary dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
{% if filter_categories|length < 3 %}disabled{% endif %}>
Download area agenda
</button>
<ul class="dropdown-menu">
{% for fc in filter_categories %}
{% if not forloop.last %}
{# skip the last group, it's the office hours/misc #}
{% for p in fc|dictsort:"label" %}
<li>
<a class="dropdown-item"
href="{% url "ietf.meeting.views.agenda_ical" num=schedule.meeting.number %}?show={{ p.keyword }}">
{{ p.label }}
</a>
</li>
{% endfor %}
{% endif %}
{% endfor %}
</ul>
<a class="btn btn-outline-primary {% if non_area_keywords|length == 0 %}disabled{% endif %}"
href="{% if non_area_keywords %}{% url 'ietf.meeting.views.agenda_ical' num=schedule.meeting.number %}?show={{ non_area_keywords|join:',' }}{% else %}#{% endif %}">
Download non-area events
</a>
</div>
<div id="weekview" class="d-none mt-3">
<h2>
Schedule
{% if schedule.meeting.agenda_warning_note %}
<span class="badge rounded-pill bg-danger">
{{ schedule.meeting.agenda_warning_note|removetags:"h1" |safe }}
</span>
{% endif %}
</h2>
<iframe title="Schedule" class="w-100 overflow-hidden border border-dark"></iframe>
</div>
<h2 class="mt-3">
{% if personalize %}Personalize{% endif %}
Detailed Agenda
{% if schedule.meeting.agenda_warning_note %}
<span class="badge rounded-pill bg-danger">
{{ schedule.meeting.agenda_warning_note|removetags:"h1" |safe }}
</span>
{% endif %}
</h2>
{% if personalize %}
<p>
Check boxes below to select individual sessions.
</p>
{% endif %}
<table id="agenda-table" class="table table-sm tablesorter">
<thead>
<tr>
{% if personalize %}<th scope="col"></th>{% endif %}
<th scope="col"></th>
<th scope="col" data-sort="loc"></th>
<th scope="col" data-sort="group"></th>
<th scope="col" data-sort="area"></th>
<th scope="col" data-sort="desc"></th>
</tr>
</thead>
<tbody>
{% for item in filtered_assignments %}
{% ifchanged item.timeslot.time|date:"Y-m-d" %}
<tr class="table-primary show-with-children">
<th scope="col" colspan="{% if personalize %}6{% else %}5{% endif %}"
id="slot-{{ item.timeslot.time|slugify }}"
class="nav-heading">
{{ item.timeslot.time|date:"l, F j, Y" }}
</th>
</tr>
{% endifchanged %}
{% if item|is_special_agenda_item %}
<tr id="row-{{ item.slug }}"
data-filter-keywords="{{ item.filter_keywords|join:',' }}"
data-slot-start-ts="{{ item.start_timestamp }}"
data-slot-end-ts="{{ item.end_timestamp }}">
{% if personalize %}
<td class="text-center">
{% if item.session_keyword %}
<label class="d-none"
aria-label="Select session"
for="{{ item.session_keyword }}-{{ item.slug }}">
</label>
<input type="checkbox"
class="pickview form-check-input"
title="Select session"
name="selected-sessions"
id="{{ item.session_keyword }}-{{ item.slug }}"
value="{{ item.session_keyword }}"
data-filter-keywords="{{ item.filter_keywords|join:',' }}"
data-filter-item="{{ item.session_keyword }}">
{% endif %}
</td>
{% endif %}
<td class="text-end">{% include "meeting/timeslot_start_end.html" %}</td>
<td colspan="3">
{% if item.timeslot.show_location and item.timeslot.location %}
{% location_anchor item.timeslot %}
{{ item.timeslot.get_html_location }}
{% end_location_anchor %}
{% endif %}
{% if item.timeslot.show_location and item.timeslot.get_html_location %}
{% with item.timeslot.location.floorplan as floor %}
{% if item.timeslot.location.floorplan %}
<div class="d-none d-sm-block float-end">
<a href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}#floor-{{ floor.name|xslugify }}"
class="float-end"
title="{{ floor.name }}">
<span class="badge rounded-pill bg-secondary">{{ floor.short }}</span>
</a>
</div>
{% endif %}
{% endwith %}
{% endif %}
</td>
<td>
{% agenda_anchor item.session %}
{% assignment_display_name item %}
{% end_agenda_anchor %}
{% if item.session.current_status == 'canceled' %}
<span class="badge rounded-pill bg-danger float-end">CANCELLED</span>
{% else %}
{% if item.slot_type.slug == 'other' %}
{% if item.session.agenda or item.session.remote_instructions or item.session.agenda_note %}
<div class="float-end ps-2">
{% include "meeting/session_buttons_include.html" with show_agenda=True item=item schedule=schedule %}
</div>
{% else %}
<div>
{% for slide in item.session.slides %}
<a href="{{ slide.get_href }}">{{ slide.title|clean_whitespace }}</a>
<br>
{% endfor %}
</div>
{% endif %}
{% endif %}
{% endif %}
</td>
</tr>
{% elif item|is_regular_agenda_item or item|is_plenary_agenda_item %}
{% if item|is_regular_agenda_item %}
{% ifchanged %}
<tr class="table-secondary session-label-row show-with-children"
data-slot-start-ts="{{ item.start_timestamp }}"
data-slot-end-ts="{{ item.end_timestamp }}">
{% if personalize %}<th scope="row" class="text-center"></th>{% endif %}
<th scope="row" class="text-end">{% include "meeting/timeslot_start_end.html" %}</th>
<th scope="row" colspan="4">
{{ item.timeslot.time|date:"l" }}
{{ item.timeslot.name|capfirst_allcaps }}
</th>
</tr>
{% endifchanged %}
{% endif %}
{% if item.session.historic_group %}
<tr id="row-{{ item.slug }}"
{% if item.slot_type.slug == 'plenary' %}class="{{ item.slot_type.slug }}danger"{% endif %}
data-filter-keywords="{{ item.filter_keywords|join:',' }}"
data-slot-start-ts="{{ item.start_timestamp }}"
data-slot-end-ts="{{ item.end_timestamp }}">
{% if personalize %}
<td class="text-center">
{% if item.session_keyword %}
<label class="d-none"
aria-label="Select session"
for="{{ item.session_keyword }}">
</label>
<input type="checkbox"
class="pickview form-check-input"
title="Select session"
name="selected-sessions"
id="{{ item.session_keyword }}"
value="{{ item.session_keyword }}"
data-filter-keywords="{{ item.filter_keywords|join:',' }}"
data-filter-item="{{ item.session_keyword }}">
{% endif %}
</td>
{% endif %}
{% if item.slot_type.slug == 'plenary' %}
<td class="text-end">
{% include "meeting/timeslot_start_end.html" %}</td>
<td colspan="3">
{% if item.timeslot.show_location and item.timeslot.location %}
{% location_anchor item.timeslot %}
{{ item.timeslot.get_html_location }}
{% end_location_anchor %}
{% endif %}
</td>
{% else %}
<td>
{% with item.timeslot.location.floorplan as floor %}
{% if item.timeslot.location.floorplan %}
<div class="d-none d-sm-block">
<a href="{% url 'ietf.meeting.views.floor_plan' num=schedule.meeting.number %}#floor-{{ floor.name|xslugify }}"
class="float-end"
title="{{ floor.name }}">
<span class="badge rounded-pill bg-secondary">{{ floor.short }}</span>
</a>
</div>
{% endif %}
{% endwith %}
</td>
<td>
{% if item.timeslot.show_location and item.timeslot.location %}
{% location_anchor item.timeslot %}
{{ item.timeslot.get_html_location }}
{% end_location_anchor %}
{% endif %}
</td>
<td>
{% if item.session.historic_group.historic_parent.acronym %}
<div class="d-none d-sm-block">{{ item.session.historic_group.historic_parent.acronym }}</div>
{% endif %}
</td>
<td>
<div class="d-none d-sm-block">
{% if item.session.historic_group %}
<a href="{% url 'ietf.group.views.group_about' acronym=item.session.historic_group.acronym %}">
{{ item.session.historic_group.acronym }}
</a>
{% else %}
{{ item.session.historic_group.acronym }}
{% endif %}
</div>
</td>
{% endif %}
<td>
{% if item.session.current_status == 'canceled' %}
<span class="badge rounded-pill bg-danger float-end">Cancelled</span>
{% else %}
<div class="float-end ps-2">
{% include "meeting/session_buttons_include.html" with show_agenda=True session=item.session meeting=schedule.meeting %}
</div>
{% endif %}
<div class="d-sm-none">
{% if item.session.historic_group %}
<a href="{% url 'ietf.group.views.group_about' acronym=item.session.historic_group.acronym %}">
{{ item.session.historic_group.acronym }}</a>
{% else %}
{{ item.session.historic_group.acronym }}
{% endif %}
{% if item.session.historic_group.historic_parent.acronym %}
<span class="text-muted">{{ item.session.historic_group.historic_parent.acronym }}</span>
{% endif %}
</div>
{% agenda_anchor item.session %}
{% assignment_display_name item %}
{% end_agenda_anchor %}
{% if item.session.historic_group.state_id == "bof" %}
<span class="badge rounded-pill bg-success float-end">BOF</span>
{% endif %}
{% if item.session.current_status == 'resched' %}
<div class="badge rounded-pill bg-danger float-end">
Rescheduled
{% if item.session.rescheduled_to %}
TO
<div class="timetooltip reschedtimetooltip">
<div data-start-time="{{ item.session.rescheduled_to.time|utc|date:"U" }}"
data-end-time="{{ item.session.rescheduled_to.end_time|utc|date:"U" }}"
{% if item.timeslot.time|date:"l" != item.session.rescheduled_to.time|date:"l" %} data-weekday="1"{% endif %}>
{% if "-utc" in request.path %}
{{ item.session.rescheduled_to.time|utc|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|utc|date:"G:i" }}
{% else %}
{{ item.session.rescheduled_to.time|date:"l G:i"|upper }}-{{ item.session.rescheduled_to.end_time|date:"G:i" }}
{% endif %}
</div>
</div>
{% endif %}
</div>
{% endif %}
{% if item.session.agenda_note|first_url|conference_url %}
<br>
<a href="{{ item.session.agenda_note|first_url }}">{{ item.session.agenda_note|slice:":23" }}
</a>
{% elif item.session.agenda_note %}
<br>
<span class="text-danger small">{{ item.session.agenda_note }}</span>
{% endif %}
</td>
</tr>
{% endif %}
{% endif %}
{% endfor %}
</tbody>
</table>
{% if personalize %}{# only show second copy of buttons for the personalize tab #}
{% include "meeting/agenda_personalize_buttonlist.html" with meeting=schedule.meeting personalize=personalize only %}
{% endif %}
{% endcache %}
<div id="app"></div>
<div id="app-loading"></div>
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/agenda_filter.js' %}"></script>
<script>
// Update the agenda display with specified filters
function update_agenda_display(filter_params) {
var agenda_rows=$('[id^="row-"]')
if (!agenda_filter.filtering_is_enabled(filter_params)) {
// When filtering is not enabled, show all sessions
agenda_rows.show();
return;
}
// if groups were selected for filtering, hide all rows by default
agenda_rows.filter(function(index, row) {
return !!$(row).attr('data-filter-keywords');
}).hide();
// loop through the has items and change the UI element and row visibilities accordingly
$.each(filter_params.show, function (i, v) {
// this is a regular item by wg: when present, show these rows
agenda_filter.rows_matching_filter_keyword(agenda_rows, v).show();
});
$.each(filter_params.hide, function (i, v) {
// this is a "negative" item by wg: when present, hide these rows
agenda_filter.rows_matching_filter_keyword(agenda_rows, v).hide();
});
// Now hide any session label rows with no visible sessions. Identify
// by matching on start/end timestamps.
$('tr.session-label-row').each(function(i, e) {
var start_ts = $(e).attr('data-slot-start-ts');
var end_ts = $(e).attr('data-slot-end-ts');
var visible_rows = agenda_rows.filter(
'[data-slot-start-ts="' + start_ts + '"]' +
'[data-slot-end-ts="' + end_ts + '"]' +
':visible'
);
if (visible_rows.length > 0) {
$(e).show();
} else {
$(e).hide();
}
})
}
function update_ical_links(filter_params) {
$(".ical-link").toggleClass("d-none", !agenda_filter.filtering_is_enabled(filter_params));
}
function update_weekview(filter_params) {
var weekview = $("#weekview");
if (agenda_filter.filtering_is_enabled(filter_params)) {
weekview.removeClass("d-none");
} else {
weekview.addClass("d-none");
}
update_weekview_display();
}
function update_weekview_display() {
var weekview = $("#weekview");
if (!weekview.hasClass('d-none')) {
var queryparams = window.location.search;
if (queryparams) {
queryparams += '&tz=' + encodeURIComponent(ietf_timezone.get_current_tz().toLowerCase());
} else {
queryparams = '?tz=' + encodeURIComponent(ietf_timezone.get_current_tz().toLowerCase());
}
var new_url = 'week-view.html' + queryparams;
var wv_iframe = $(weekview).children('iframe');
var wv_window = wv_iframe.contentWindow;
if (wv_iframe.src && wv_window.location.hostname && wv_window.history && wv_window.history.replaceState) {
wv_window.history.replaceState({}, '', new_url);
wv_window.redraw_weekview();
} else {
// either have not yet loaded the iframe or we do not support history replacement
$(wv_iframe).attr("src", new_url);
}
}
}
function update_view(filter_params) {
update_agenda_display(filter_params);
update_weekview(filter_params)
update_ical_links(filter_params)
}
</script>
<script src="{% static 'ietf/js/list.js' %}">
</script>
<script src="{% static 'ietf/js/moment.js' %}">
</script>
<script src="{% static 'ietf/js/timezone.js' %}">
</script>
<script src="{% static 'ietf/js/agenda_materials.js' %}">
</script>
<script src="{% static 'ietf/js/agenda_timezone.js' %}">
</script>
{% if personalize %}
<script src="{% static 'ietf/js/agenda_personalize.js' %}">
</script>
{% endif %}
<script>
{% if settings.DEBUG and settings.DEBUG_AGENDA %}
speedup = +urlParam('speedup');
if (speedup < 1) {
speedup = 1;
}
start_time = moment().utc();
if (urlParam('date')) {
offset_time = moment.tz(decodeURIComponent(urlParam('date')), "UTC");
} else {
offset_time = start_time;
}
if (speedup > 1 || offset_time !== start_time) {
moment.now = function () {
return (+new Date() - start_time) * speedup + offset_time;
}
}
{% else %}
speedup = 1;
{% endif %}
$(document).ready(function() {
// Methods/variables here that are not in ietf_timezone or agenda_filter are from agenda_timezone.js
meeting_timezone = '{{ display_timezone }}';
// First, initialize_moments(). This must be done before calling any of the update methods.
// It does not need timezone info, so safe to call before initializing ietf_timezone.
initialize_moments(); // fills in moments in the agenda data
// Now set up callbacks related to ietf_timezone. This must happen before calling initialize().
// In particular, set_current_tz_cb() must be called before the update methods are called.
set_current_tz_cb(ietf_timezone.get_current_tz); // give agenda_timezone access to this method
ietf_timezone.set_tz_change_callback(function(newtz) {
update_times(newtz);
update_weekview_display();
}
);
// With callbacks in place, call ietf_timezone.initialize(). This will call the tz_change callback
// after setting things up.
{% if "-utc" in request.path %}
ietf_timezone.initialize('UTC');
{% else %}
ietf_timezone.initialize(meeting_timezone);
{% endif %}
// Now make other setup calls from agenda_timezone.js
add_tooltips();
init_timers(speedup);
// Finally, set up the agenda filter UI. This does not depend on the timezone.
{% if personalize %}
agenda_filter.set_update_callback(function (e) {
handleFilterParamUpdate(e);
});
document.getElementById('agenda-table')
.addEventListener('click', handleTableClick);
{% else %}
agenda_filter.set_update_callback(update_view);
{% endif %}
agenda_filter.enable();
}
);
</script>
{% endblock %}

View file

@ -23,6 +23,6 @@ DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %}
\n
Session materials: {% absurl 'ietf.meeting.views.session_details' num=schedule.meeting.number acronym=item.session.group.acronym %}\n{% if schedule.meeting.get_number is not None %}
\n{# link agenda for ietf meetings #}
See in schedule: {% absurl 'ietf.meeting.views.agenda' num=schedule.meeting.number %}#row-{{ item.slug }}\n{% endif %}
See in schedule: {% absurl 'agenda' num=schedule.meeting.number %}#row-{{ item.slug }}\n{% endif %}
END:VEVENT
{% endif %}{% endfor %}END:VCALENDAR{% endcache %}{% endtimezone %}{% endautoescape %}

View file

@ -1,27 +0,0 @@
{% extends "base.html" %}
{% block morecss %}
.type-lead:after { content: " (DO NOT POST)"; color:red; }
.type-offagenda:after { content:" (not published on agenda)"; }
{% endblock %}
{% block title %}Agenda for {{ meeting }} by room{% endblock %}
{% block content %}
{% include "meeting/meeting_heading.html" with updated=meeting.updated selected="by-room" title_extra="By room" %}
<div class="daylist">
{% for day,sessions in ss_by_day.items %}
<h2 class="daylistentry mt-5">{{ day|date:'l, j F Y' }}</h2>
{% regroup sessions by timeslot.get_functional_location as room_list %}
<div class="roomlist">
{% for room in room_list %}
<strong class="roomlistentry">{{ room.grouper|default:"Location Unavailable" }}</strong>
<ul class="sessionlist">
{% for ss in room.list %}
<li class="sessionlistentry type-{{ ss.slot_type.slug }} {% if ss.schedule_id != meeting.schedule_id %}from-base-schedule{% endif %}">
{{ ss.timeslot.time|date:"H:i" }}-{{ ss.timeslot.end_time|date:"H:i" }} {{ ss.session.short_name }}
</li>
{% endfor %}
</ul>
{% endfor %}
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -1,49 +0,0 @@
{% extends "base.html" %}
{% block morecss %}
.type-lead:after { content: " (DO NOT POST)"; color:red; }
.type-offagenda:after { content:" (not published on agenda)"; }
{% endblock %}
{% block title %}Agenda for {{ meeting }} by Session Type{% endblock %}
{% block content %}
{% include "meeting/meeting_heading.html" with updated=meeting.updated selected="by-type" title_extra="By session type" %}
{% regroup assignments by session.type_id as type_list %}
<div class="typelist">
{% for type in type_list %}
<div class="typelistentry">
<h2 class="mt-5">{{ type.grouper|title }}</h2>
{% if schedule == meeting.schedule %}
<a class="btn btn-primary ical-link"
href="{% url "ietf.meeting.views.agenda_by_type_ics" num=meeting.number type=type.grouper %}">
Download to Calendar
</a>
{% endif %}
<div class="daylist">
{% regroup type.list by timeslot.time|date:"l Y-M-d" as daylist %}
{% for day in daylist %}
<div class="daylistentry">
<h3 class="mt-4">{{ day.grouper }}</h3>
<table class="table table-sm table-borderless sessiontable">
<tbody>
{% for ss in day.list %}
<tr {% if ss.schedule_id != meeting.schedule_id %}class="from-base-schedule"{% endif %}>
<td>{{ ss.timeslot.time|date:"H:i" }}-{{ ss.timeslot.end_time|date:"H:i" }}</td>
<td>{{ ss.timeslot.get_hidden_location }}</td>
<td class="type-{{ ss.session.type_id }}">{{ ss.session.short_name }}</td>
<td class="text-end">
{% if ss.session.type_id == 'regular' or ss.session.type_id == 'plenary' or ss.session.type_id == 'other' %}
<a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=ss.session.group.acronym %}">
Materials
</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endblock %}

View file

@ -1,24 +0,0 @@
{% comment %}
Buttons for the agenda_personalize.html template
Required parameters:
meeting - meeting being displayed
personalize - if True, show buttons relevant only for personalize tab
{% endcomment %}
{% load agenda_custom_tags %}
<div class="mb-3 buttonlist">
<a class="btn btn-sm btn-outline-primary ical-link agenda-link filterable"
href="{% webcal_url 'ietf.meeting.views.agenda_ical' num=meeting.number %}">
Subscribe to filtered agenda
</a>
<a class="btn btn-sm btn-outline-primary ical-link agenda-link filterable"
href="{% url "ietf.meeting.views.agenda_ical" num=meeting.number %}">
Download .ics of filtered agenda
</a>
{% if personalize %}
<a class="btn btn-sm btn-outline-primary agenda-link filterable"
href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">
View personal agenda
</a>
{% endif %}
</div>

View file

@ -1,107 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load ietf_filters %}
{% load textfilters %}
{% load static %}
{% block title %}
IETF {{ meeting.number }} meeting agenda
{% if "-utc" in request.path %}(UTC){% endif %}
{% endblock %}
{% block bodyAttrs %}onload="automaticarrow(); checkParams();" onresize="checkParams();"{% endblock %}
{% block precontent %}
<div class="meeting-switch">
<i class="bi bi-arrow-left-right me-2"></i>
<a href="{% url 'floor-plan-neue' num=schedule.meeting.number %}">Switch to New Agenda Display</a>
</div>
{% endblock %}
{% block content %}
{% origin %}
{% include "meeting/meeting_heading.html" with selected="floor-plan" title_extra="Floor Plan" %}
{% for floor in floors %}
<h2 class="mt-4" id="floor-{{ floor.name|xslugify }}">{{ floor.name }}</h2>
<div class="row rooms">
<div class="col-sm-2">
{% for f in floors %}
{% for room in f.room_set.all %}
<a href="javascript: setarrow('room-{{ room.name|xslugify }}')">{{ room.name }}</a>
<br>
{% endfor %}
{% endfor %}
</div>
<div class="col-sm-2">
{% for f in floors %}
{% for room in f.room_set.all %}
{% if room.functional_display_name %}
<a href="javascript: setarrow('room-{{ room.name|xslugify }}')">{{ room.functional_display_name }}</a>
<br>
{% endif %}
{% endfor %}
{% endfor %}
</div>
<div class="col-sm-8">
<div class="floor-plan position-relative">
{% if floor.image %}
<img id="floor-{{ floor.name|xslugify }}-image"
alt="{{ floor.name }} Map"
class="img-fluid w-100"
src="{{ floor.image.url }}">
{# We need as many of these as we can have individual rooms combining into one #}
<div id="floor-{{ floor.name|xslugify }}-arrowdiv0"
class="position-absolute" hidden>
<img alt="Location arrow" src="{% static 'ietf/images/arrow-ani.webp' %}">
</div>
<div id="floor-{{ floor.name|xslugify }}-arrowdiv1"
class="position-absolute" hidden>
<img alt="Location arrow" src="{% static 'ietf/images/arrow-ani.webp' %}">
</div>
<div id="floor-{{ floor.name|xslugify }}-arrowdiv2"
class="position-absolute" hidden>
<img alt="Location arrow" src="{% static 'ietf/images/arrow-ani.webp' %}">
</div>
<div id="floor-{{ floor.name|xslugify }}-arrowdiv3"
class="position-absolute" hidden>
<img alt="Location arrow" src="{% static 'ietf/images/arrow-ani.webp' %}">
</div>
{% else %}
No floor image available yet.
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/room_params.js' %}"></script>
<script>
// These must match the 'arrowdiv' divs above
var arrowsuffixlist = [ '0', '1', '2', '3' ];
var floorlist = [{% for floor in floors %}{% if not forloop.first %}, {%endif%}'floor-{{floor.name|xslugify}}'{% endfor %}];
function roommap(nm)
{
var c = findroom(nm);
if (c) return nm;
var m = suffixmap(nm);
// console.log("m=" + m);
return m;
}
function findroom(nm)
{
var left = 0, top = 0, right = 0, bottom = 0, floor="", width=0;
if (0) { }
{% for room in meeting.room_set.all %}{% if room.floorplan %}
else if (nm == 'room-{{room.name|xslugify}}') { left = {{room.left}}; top = {{room.top}}; right = {{room.right}}; bottom = {{room.bottom}}; floor='floor-{{room.floorplan.name|xslugify}}'; width={{room.floorplan.image.width}}; }{% endif %}{% endfor %}
{% for room in meeting.room_set.all %}{% if room.functional_display_name %}{% if room.floorplan %}
else if (nm == '{{room.functional_name|xslugify}}') { left = {{room.left}}; top = {{room.top}}; right = {{room.right}}; bottom = {{room.bottom}}; floor='floor-{{room.floorplan.name|xslugify}}'; width={{room.floorplan.image.width}}; }{% endif %}{% endif %}{% endfor %}
else return null;
// console.log("nm=" + nm + ",left=" + left + ",top=" + top + ",r=" + right + ",b=" + bottom);
return [left, top, right, bottom, floor, width];
}
</script>
{% endblock %}

View file

@ -1,85 +0,0 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% origin %}
{# assumes meeting is in context #}
{% load origin %}
{% load ietf_filters htmlfilters %}
{% origin %}
<h1>
IETF {{ meeting.number }} meeting agenda
{% if personalize %}personalization{% endif %}
{% if schedule.meeting.agenda_warning_note %}
<span class="badge rounded-pill bg-danger">
{{ schedule.meeting.agenda_warning_note|removetags:"h1" |safe }}
</span>
{% endif %}
{% if title_extra %}
<br>
<small class="text-muted">{{ title_extra }}</small>
{% endif %}
</h1>
<div class="lead row">
<div class="{% if updated %}col-6{% else %}col-12{% endif %}">
{{ meeting.city|default:"Location TBD" }}, {{ meeting.date|date:"F j" }}{% if meeting.date.month != meeting.end_date.month %} - {{ meeting.end_date|date:"F " }}{% else %}-{% endif %}{{ meeting.end_date|date:"j, Y" }}
</div>
{% if updated %}
<div class="col-6 text-end">
Updated {{ updated|date:"Y-m-d \a\t G:i (T)" }}
</div>
{% endif %}
</div>
{% if schedule != meeting.schedule %}
<div class="alert alert-danger my-3">
This is schedule <b>{{ schedule.owner.email }}/{{ schedule.name }}</b>, not the official schedule.
</div>
{% endif %}
{# a tags with the agenda-link filterable classes will be updated with show/hide parameters #}
<ul class="nav nav-tabs my-3">
<li class="nav-item">
<a class="nav-link agenda-link filterable {% if selected == "agenda" %}active{% endif %}"
href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">
Agenda
</a>
</li>
{% if user|has_role:"Secretariat,Area Director,IAB" %}
{% if schedule != meeting.schedule %}
<li class="nav-item">
<a class="nav-link {% if selected == "by-room" %}active{% endif %}"
href="{% url 'ietf.meeting.views.agenda_by_room' num=meeting.number name=schedule.name owner=schedule.owner.email %}">
By
room
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if selected == "by-type" %}active{% endif %}"
href="{% url 'ietf.meeting.views.agenda_by_type' num=meeting.number name=schedule.name owner=schedule.owner.email %}">
By
type
</a>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link {% if selected == "by-room" %}active{% endif %}"
href="{% url 'ietf.meeting.views.agenda_by_room' num=meeting.number %}">
By room
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if selected == "by-type" %}active{% endif %}"
href="{% url 'ietf.meeting.views.agenda_by_type' num=meeting.number %}">
By type
</a>
</li>
{% endif %}
{% endif %}
<li class="nav-item">
<a class="nav-link {% if selected == "floor-plan" %}active{% endif %}"
href="{% url 'ietf.meeting.views.floor_plan' num=meeting.number %}">
Floor plan
</a>
</li>
<li class="nav-item">
<a class="nav-link"
href="{% url 'ietf.meeting.views.agenda' num=meeting.number ext='.txt' %}">Plaintext</a>
</li>
</ul>

View file

@ -1,9 +0,0 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% block title %}IETF {{ meeting.number }} Meeting Agenda{% endblock %}
{% block content %}
{% origin %}
{% include "meeting/meeting_heading.html" with title_extra="" selected="" %}
<div class="alert alert-warning my-3">There is no agenda available yet.</div>
{% endblock %}

View file

@ -37,7 +37,7 @@
<a href="{% url 'ietf.meeting.views.session_details' num=meeting.number acronym=meeting.responsible_group.acronym %}">{{ meeting.number }}</a>
{% if meeting.interim_meeting_cancelled %}<span class="badge rounded-pill bg-warning">Cancelled</span>{% endif %}
{% else %}
<a href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">IETF-{{ meeting.number }}</a>
<a href="{% url 'agenda' num=meeting.number %}">IETF-{{ meeting.number }}</a>
{% endif %}
</td>
<td class="d-none d-sm-table-cell">

View file

@ -11,7 +11,7 @@ This renders the list of links below the title on the meeting proceedings page.
<a href="{% url 'ietf.meeting.views.proceedings_attendees' num=meeting.number %}">Participants</a>
</div>
<div class="proceedings-row">
<a href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">Meeting Agenda</a>
<a href="{% url 'agenda' num=meeting.number %}">Meeting Agenda</a>
</div>
<div class="proceedings-row">
<a href="{% url 'ietf.meeting.views.proceedings_progress_report' num=meeting.number %}">Activity Report</a>

View file

@ -13,9 +13,9 @@ This renders the title block for the meeting proceedings page.
{% if attendance is not None %}
<div class="proceedings-info lead">
{% if attendance.onsite > 0 %}
{{ attendance.onsite }} onsite participant{{ attendance.onsite|pluralize }}{% if attendance.online > 0 %},{% endif %}
{{ attendance.onsite }} onsite participant{{ attendance.onsite|pluralize }}{% if attendance.remote > 0 %},{% endif %}
{% endif %}
{% if attendance.online > 0 %}{{ attendance.online }} online participant{{ attendance.online|pluralize }}{% endif %}
{% if attendance.remote > 0 %}{{ attendance.remote }} online participant{{ attendance.remote|pluralize }}{% endif %}
</div>
{% endif %}
</div>

View file

@ -1,379 +0,0 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load textfilters %}
{% load ietf_filters tz %}
{% origin %}
{% if item and item|should_show_agenda_session_buttons %}
{% with slug=item.slug %}
{% with session=item.session %}
{% with timeslot=item.timeslot %}
{% with meeting=schedule.meeting %}
{% if session.agenda and show_agenda %}
{# Note: if called with show_agenda=True, calling template must load agenda_materials.js, needed by session_agenda_include.html #}
{% include "meeting/session_agenda_include.html" with slug=slug session=session timeslot=timeslot only %}
{% endif %}
<div class="d-flex">
{% if timeslot.location.video_stream_url or timeslot.location.onsite_tool_url %}
<div id="session-meetecho-buttons-{{ session.pk }}"
role="group"
class="btn-group btn-group-sm d-none d-lg-flex me-1">
{# Video stream (meetecho) #}
{% if timeslot.location.video_stream_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ timeslot.location.video_stream_url|format:session }}"
aria-label="Full Client with Video"
title="Full Client with Video">
<i class="bi bi-camera-video"></i>
</a>
{% endif %}
{# Onsite tool (meetecho_onsite) #}
{% if timeslot.location.onsite_tool_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ timeslot.location.onsite_tool_url|format:session }}"
aria-label="Onsite tool"
title="Onsite tool">
<i class="bi bi-phone"></i>
</a>
{% endif %}
</div>
{% endif %}
<div id="session-buttons-{{ session.pk }}"
role="group"
class="btn-group btn-group-sm d-none d-lg-flex">
{% with acronym=session.historic_group.acronym %}
{% if session.agenda and show_agenda %}
{# agenda pop-up button #}
<button class="btn btn-outline-primary"
data-bs-toggle="modal" type="button"
data-bs-target="#modal-{{ slug }}"
aria-label="Show meeting materials"
title="Show meeting materials">
<i class="bi bi-arrows-fullscreen"></i>
</button>
{# materials tar file #}
<a class="btn btn-outline-primary"
role="button"
href="{% url 'ietf.meeting.views.session_draft_tarfile' num=meeting.number acronym=acronym %}"
aria-label="Download meeting materials as .tar archive"
title="Download meeting materials as .tar archive">
<i class="bi bi-file-zip"></i>
</a>
{# materials PDF file #}
<a class="btn btn-outline-primary"
role="button"
href="{% url 'ietf.meeting.views.session_draft_pdf' num=meeting.number acronym=acronym %}"
aria-label="Download meeting materials as PDF file"
title="Download meeting materials as PDF file">
<i class="bi bi-file-pdf"></i>
</a>
{% endif %}
{# HedgeDoc #}
{% if use_codimd %}
<a class="btn btn-outline-primary"
role="button"
href="{{ session.notes_url }}"
aria-label="Notepad for note-takers"
title="Notepad for note-takers">
<i class="bi bi-journal-text"></i>
</a>
{% endif %}
{# show stream buttons up till end of session, then show archive buttons #}
{% if now < timeslot.utc_end_time %}
{# Chat #}
<a class="btn btn-outline-primary"
role="button"
href="{{ session.chat_room_url }}"
aria-label="Chat room for {{ session.chat_room_name }}"
title="Chat room for {{ session.chat_room_name }}">
<i class="bi bi-chat"></i>
</a>
{# Audio stream #}
{% if timeslot.location.audio_stream_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ timeslot.location.audio_stream_url|format:session }}"
aria-label="Audio stream"
title="Audio stream">
<i class="bi bi-headphones"></i>
</a>
{% endif %}
{# Remote call-in #}
{% if session.agenda_note|first_url|conference_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ session.agenda_note|first_url }}"
aria-label="Online conference"
title="Online conference">
<i class="bi bi-people"></i>
</a>
{% elif session.remote_instructions|first_url|conference_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ session.remote_instructions|first_url }}"
aria-label="Online conference"
title="Online conference">
<i class="bi bi-people"></i>
</a>
{% elif timeslot.location.webex_url %}
<a class="btn btn-outline-primary"
role="button"
href="{{ timeslot.location.webex_url|format:session }}"
aria-label="Webex session"
title="Webex session">
<i class="bi bi-people"></i>
</a>
{% endif %}
{# iCalendar item #}
<a class="btn btn-outline-primary"
role="button"
href="{% url 'ietf.meeting.views.agenda_ical' num=meeting.number session_id=session.id %}"
aria-label="icalendar entry for {{ acronym }} session on {{ timeslot.time|utc|date:'Y-m-d H:i' }} UTC"
title="icalendar entry for {{ acronym }} session on {{ timeslot.time|utc|date:'Y-m-d H:i' }} UTC">
<i class="bi bi-calendar"></i>
</a>
{% else %}
{# Chat logs #}
{% if meeting.number|add:"0" >= 60 %}
<a class="btn btn-outline-primary"
role="button"
href="{{ session.chat_archive_url }}"
aria-label="Chat logs for {{ session.chat_room_name }}"
title="Chat logs for {{ session.chat_room_name }}">
<i class="bi bi-file-text"></i>
</a>
{% endif %}
{# Recordings #}
{% if meeting.number|add:"0" >= 80 %}
{% with session.recordings as recordings %}
{% if recordings %}
{# There's no guaranteed order, so this is a bit messy: #}
{# First, the audio recordings, if any #}
{% for r in recordings %}
{% if r.get_href and 'audio' in r.get_href %}
<a class="btn btn-outline-primary"
role="button"
href="{{ r.get_href }}"
aria-label="{{ r.title }}"
title="{{ r.title }}">
<i class="bi bi-file-play"></i>
</a>
{% endif %}
{% endfor %}
{# Then the youtube recordings #}
{% for r in recordings %}
{% if r.get_href and 'youtu' in r.get_href %}
<a class="btn btn-outline-primary"
role="button"
href="{{ r.get_href }}"
aria-label="{{ r.title }}"
title="{{ r.title }}">
<i class="bi bi-file-slides"></i>
</a>
{% endif %}
{% endfor %}
{# Finally, any other recordings #}
{% for r in recordings %}
{% if r.get_href and not 'audio' in r.get_href and not 'youtu' in r.get_href %}
<a class="btn btn-outline-primary"
role="button"
href="{{ r.get_href }}"
aria-label="{{ r.title }}"
title="{{ r.title }}">
<i class="bi bi-file-play"></i>
</a>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
{% if timeslot.location.video_stream_url %}
<a class="btn btn-outline-primary"
role="button"
href="https://www.meetecho.com/ietf{{ meeting.number }}/recordings#{{ acronym.upper }}"
aria-label="Session recording"
title="Session recording">
<i class="bi bi-file-slides"></i>
</a>
{% endif %}
{% endif %}
{% endif %}
{% endwith %}
</div>
<div class="dropdown d-lg-none">
<button class="btn btn-outline-primary btn-sm dropdown-toggle"
type="button"
aria-label="Info"
id="session-buttons-dropdown-{{ session.pk }}"
data-bs-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<i class="bi bi-info-lg"></i>
</button>
<ul class="dropdown-menu"
aria-labelledby="session-buttons-dropdown-{{ session.pk }}">
{% with acronym=session.historic_group.acronym %}
{% if session.agenda and show_agenda %}
{# agenda pop-up button #}
<li>
<button class="dropdown-item" type="button"
data-bs-toggle="modal"
data-bs-target="#modal-{{ slug }}">
<i class="bi bi-arrows-fullscreen"></i> Show meeting materials
</button>
</li>
{# materials tar file #}
<li>
<a class="dropdown-item"
href="{% url 'ietf.meeting.views.session_draft_tarfile' num=meeting.number acronym=acronym %}">
<i class="bi bi-file-zip"></i> Meeting materials archive
</a>
</li>
{# materials PDF file #}
<li>
<a class="dropdown-item"
href="{% url 'ietf.meeting.views.session_draft_pdf' num=meeting.number acronym=acronym %}">
<i class="bi bi-file-pdf"></i> Meeting materials PDF
</a>
</li>
{% endif %}
{# HedgeDoc #}
{% if use_codimd %}
<li>
<a class="dropdown-item" href="{{ session.notes_url }}">
<i class="bi bi-journal-text"></i> Notepad for note-takers
</a>
</li>
{% endif %}
{# show stream buttons up till end of session, then show archive buttons #}
{% if now < timeslot.utc_end_time %}
{# Chat #}
<li>
<a class="dropdown-item"
href="{{ session.chat_room_url }}">
<i class="bi bi-chat"></i> Chat room
</a>
</li>
{# Video stream (meetecho) #}
{% if timeslot.location.video_stream_url %}
<li>
<a class="dropdown-item"
href="{{ timeslot.location.video_stream_url|format:session }}">
<i class="bi bi-camera-video"></i> Video stream
</a>
</li>
{% endif %}
{# Onsite tool (meetecho_onsite) #}
{% if timeslot.location.onsite_tool_url %}
<li>
<a class="dropdown-item"
href="{{ timeslot.location.onsite_tool_url|format:session }}">
<i class="bi bi-phone"></i> Onsite tool
</a>
</li>
{% endif %}
{# Audio stream #}
{% if timeslot.location.audio_stream_url %}
<li>
<a class="dropdown-item"
href="{{ timeslot.location.audio_stream_url|format:session }}">
<i class="bi bi-headphones"></i> Audio stream
</a>
</li>
{% endif %}
{# Remote call-in #}
{% if session.agenda_note|first_url|conference_url %}
<li>
<a class="dropdown-item" href="{{ session.agenda_note|first_url }}">
<i class="bi bi-people"></i> Online conference
</a>
</li>
{% elif session.remote_instructions|first_url|conference_url %}
<li>
<a class="dropdown-item"
href="{{ session.remote_instructions|first_url }}">
<i class="bi bi-people"></i> Online conference
</a>
</li>
{% elif timeslot.location.webex_url %}
<li>
<a class="dropdown-item"
href="{{ timeslot.location.webex_url|format:session }}">
<i class="bi bi-people"></i> Webex session
</a>
</li>
{% endif %}
{# iCalendar item #}
<li>
<a class="dropdown-item"
href="{% url 'ietf.meeting.views.agenda_ical' num=meeting.number session_id=session.id %}">
<i class="bi bi-calendar"></i> Add to calendar
</a>
</li>
{% else %}
{# Chat logs #}
{% if meeting.number|add:"0" >= 60 %}
<li>
<a class="dropdown-item"
href="{{ session.chat_archive_url }}">
<i class="bi bi-file-text"></i> Chat logs
</a>
</li>
{% endif %}
{# Recordings #}
{% if meeting.number|add:"0" >= 80 %}
{% with session.recordings as recordings %}
{% if recordings %}
{# There's no guaranteed order, so this is a bit messy: #}
{# First, the audio recordings, if any #}
{% for r in recordings %}
{% if r.get_href and 'audio' in r.get_href %}
<li>
<a class="dropdown-item" href="{{ r.get_href }}">
<i class="bi bi-file-play"></i> {{ r.title }}
</a>
</li>
{% endif %}
{% endfor %}
{# Then the youtube recordings #}
{% for r in recordings %}
{% if r.get_href and 'youtu' in r.get_href %}
<li>
<a class="dropdown-item" href="{{ r.get_href }}">
<i class="bi bi-file-slides"></i> {{ r.title }}
</a>
</li>
{% endif %}
{% endfor %}
{# Finally, any other recordings #}
{% for r in recordings %}
{% if r.get_href and not 'audio' in r.get_href and not 'youtu' in r.get_href %}
<li>
<a class="dropdown-item" href="{{ r.get_href }}">
<i class="bi bi-file-play"></i> {{ r.title }}
</a>
</li>
{% endif %}
{% endfor %}
{% endif %}
{% endwith %}
{% if timeslot.location.video_stream_url %}
<li>
<a class="dropdown-item"
href="https://www.meetecho.com/ietf{{ meeting.number }}/recordings#{{ acronym.upper }}">
<i class="bi bi-file-slides"></i> Session recording
</a>
</li>
{% endif %}
{% endif %}
{% endif %}
{% endwith %}
</ul>
</div>
</div>
{% endwith %}
{% endwith %}
{% endwith %}
{% endwith %}
{% endif %}

View file

@ -21,10 +21,6 @@
{# see note in the included templates re: show_agenda parameter and required JS import #}
{% if meeting.type.slug == 'interim' %}
{% include "meeting/interim_session_buttons.html" with show_agenda=False show_empty=False %}
{% else %}
{% with schedule=meeting.schedule %}
{% include "meeting/session_buttons_include.html" with show_agenda=False %}
{% endwith %}
{% endif %}
</div>
{% endif %}
@ -114,6 +110,23 @@
Upload bluesheets
</a>
{% endif %}
{% if session.filtered_chatlog_and_polls %}
<h3 class="mt-4">Chatlog and polls</h3>
<table class="table table-sm table-striped chatlog-and-polls"
id="chatlog_and_polls_{{ session.pk }}">
<tbody data-session="{{ session.pk }}">
{% for pres in session.filtered_chatlog_and_polls %}
<tr data-name="{{ pres.document.name }}">
{% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %}
<td>
<a href="{{ pres.document.get_href }}">{{ pres.document.title }}</a>
<a href="{{ url }}">({{ pres.document.name }})</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
<h3 class="mt-4">Slides</h3>
<table class="table table-sm table-striped slides"
id="slides_{{ session.pk }}">

View file

@ -1,7 +0,0 @@
<div class="timetooltip">
<div class="time"
data-start-time="{{ item.start_timestamp }}"
data-end-time="{{ item.end_timestamp }}">
{{ item.timeslot.time|date:"H:i" }}<span class="d-lg-none"><br></span>-{{ item.timeslot.end_time|date:"H:i" }}
</div>
</div>

View file

@ -58,7 +58,7 @@
<td>ietf</td>
<td>
<a class="ietf-meeting-link"
href="{% url 'ietf.meeting.views.agenda' num=meeting.number %}">
href="{% url 'agenda' num=meeting.number %}">
IETF {{ meeting.number }}
</a>
</td>
@ -124,7 +124,7 @@
ietf_meeting_number: '{{ meeting.number }}',
start_moment: moment.tz('{{meeting.date}}', '{{ meeting.time_zone }}').startOf('day'),
end_moment: moment.tz('{{meeting.end}}', '{{ meeting.time_zone }}').endOf('day'),
url: '{% url 'ietf.meeting.views.agenda' num=meeting.number %}'
url: '{% url 'agenda' num=meeting.number %}'
}{% if not forloop.last %}, {% endif %}
{% endwith %}
{% else %} {# if it's not a Meeting, it's a Session #}

View file

@ -26,6 +26,6 @@ CLASS:PUBLIC
DTSTART;VALUE=DATE{% if meeting.time_zone %};TZID={{ meeting.time_zone|ics_esc }}{% endif %}:{{ meeting.date|date:"Ymd" }}
DTEND;VALUE=DATE{% if meeting.time_zone %};TZID={{ meeting.time_zone|ics_esc }}{% endif %}:{{ meeting.end_date|date:"Ymd" }}
DTSTAMP:{{ meeting.cached_updated|date:"Ymd" }}T{{ meeting.cached_updated|date:"His" }}Z
URL:{{ request.scheme }}://{{ request.get_host }}{% url 'ietf.meeting.views.agenda' num=meeting.number %}
URL:{{ request.scheme }}://{{ request.get_host }}{% url 'agenda' num=meeting.number %}
END:VEVENT
{% endfor %}END:VCALENDAR{% endautoescape %}

View file

@ -1,40 +0,0 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load static %}
{# FIXME: the weekview only renders correctly in quirks mode, i.e., not in HTML5 with "<!DOCTYPE html>" in the next line; it should be rewritten with fullcalendar #}
{# <!DOCTYPE html> #}
<html lang="en">
{% origin %}
<head>
<title>Weekview</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="{% static 'ietf/js/agenda_filter.js' %}"></script>
<script src="{% static 'ietf/js/moment.js' %}"></script>
<script src="{% static 'ietf/js/week-view.js' %}"></script>
<script>
var all_items = {{ items | safe }};
//===========================================================================
// Set up events for drawing the calendar
function redraw_weekview() {
var query_params = agenda_filter.parse_query_params(window.location.search);
var timezone_name = query_params.tz || 'utc';
items = prepare_items(all_items, timezone_name);
draw_calendar(items, agenda_filter.get_filter_params(query_params));
}
window.addEventListener("resize", redraw_weekview, false);
window.addEventListener("load", redraw_weekview, false);
window.addEventListener("hashchange", redraw_weekview, false);
</script>
</head>
<body>
<p>
Error loading calendar.
</p>
</body>
</html>

View file

@ -192,7 +192,7 @@
{% endif %}
<td>
<a href="{% url 'ietf.person.views.profile' email_or_name=np.nominee.name %}">
{{ np.nominee }}
{{ np.nominee.email.name_and_email }}
</a>
</td>
<td>

View file

@ -183,11 +183,11 @@ def forward(apps, schema_editor):
class Migration(migrations.Migration):
dependencies = [
('doc', '0045_use_timezone_now_for_doc_models'),
('doc', '0046_use_timezone_now_for_doc_models'),
('group', '0059_use_timezone_now_for_group_models'),
('meeting', '0058_meeting_time_zone_not_blank'),
('message', '0012_use_timezone_now_for_message_models'),
('person', '0025_use_timezone_now_for_person_models'),
('person', '0026_use_timezone_now_for_person_models'),
('review', '0029_use_timezone_now_for_review_models'),
('submit', '0011_use_timezone_now_for_submit_models'),
('utils', '0001_initial'),

View file

@ -859,9 +859,9 @@ class IetfTestRunner(DiscoverRunner):
# django-bootstrap5 seems to still generate 'checked="checked"', ignore:
"attribute-boolean-style": "off",
# self-closing style tags are valid in HTML5. Both self-closing and non-self-closing tags are accepted. (vite generates self-closing link tags)
# "void-style": "off",
"void-style": "off",
# Both attributes without value and empty strings are equal and valid. (vite generates empty value attributes)
# "attribute-empty-style": "off"
"attribute-empty-style": "off",
# For fragments, don't check that elements are in the proper ancestor element
"element-required-ancestor": "off",
},

View file

@ -51,7 +51,7 @@ test.describe('past - desktop', () => {
// Visit agenda page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/agenda-neue`)
page.goto(`/meeting/${meetingData.meeting.number}/agenda`)
])
// Wait for page to be ready
@ -191,7 +191,7 @@ test.describe('past - desktop', () => {
if (event.location?.short) {
// Has floor badge
await expect(row.locator('.agenda-table-cell-room > a')).toContainText(event.room)
await expect(row.locator('.agenda-table-cell-room > a')).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/floor-plan-neue?room=${xslugify(event.room)}`)
await expect(row.locator('.agenda-table-cell-room > a')).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/floor-plan?room=${xslugify(event.room)}`)
await expect(row.locator('.agenda-table-cell-room > .badge')).toContainText(event.location.short)
} else {
// No floor badge
@ -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/)
@ -441,14 +450,17 @@ test.describe('past - desktop', () => {
await expect(page.locator('.agenda-eventdetails .detail-text > iframe')).toHaveAttribute('src', materialsInfo.minutes.url)
// Footer Buttons
const hedgeDocLink = `https://notes.ietf.org/notes-ietf-${meetingData.meeting.number}-${event.type === 'plenary' ? 'plenary' : event.acronym}`
const detailsUrl = `/meeting/${meetingData.meeting.number}/session/${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')
await expect(footerBtnsLocator.nth(1)).toHaveAttribute('href', `/meeting/${meetingData.meeting.number}/agenda/${event.acronym}-drafts.pdf`)
await expect(footerBtnsLocator.last()).toContainText('Notepad')
await expect(footerBtnsLocator.last()).toHaveAttribute('href', hedgeDocLink)
await expect(footerBtnsLocator.nth(2)).toContainText('Notepad')
await expect(footerBtnsLocator.nth(2)).toHaveAttribute('href', hedgeDocLink)
await expect(footerBtnsLocator.last()).toContainText(`${event.groupAcronym} materials page`)
await expect(footerBtnsLocator.last()).toHaveAttribute('href', detailsUrl)
// Clicking X should close the dialog
await page.locator('.agenda-eventdetails .n-card-header__extra > .detail-header > button').click()
})
@ -483,7 +495,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.')
@ -1079,7 +1091,7 @@ test.describe('future - desktop', () => {
// Visit agenda page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/agenda-neue`)
page.goto(`/meeting/${meetingData.meeting.number}/agenda`)
])
// Wait for page to be ready
@ -1247,7 +1259,7 @@ test.describe('live - desktop', () => {
// Visit agenda page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/agenda-neue`)
page.goto(`/meeting/${meetingData.meeting.number}/agenda`)
])
// Wait for page to be ready
@ -1328,7 +1340,7 @@ test.describe('past - small screens', () => {
// Visit agenda page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/agenda-neue`)
page.goto(`/meeting/${meetingData.meeting.number}/agenda`)
])
// Wait for page to be ready

View file

@ -12,7 +12,7 @@ seedrandom(TEST_SEED.toString(), { global: true })
faker.seed(TEST_SEED)
// ====================================================================
// FLOOR-PLAN-NEUE | All Viewports
// FLOOR-PLAN | All Viewports
// ====================================================================
test.describe('floor-plan', () => {
@ -42,7 +42,7 @@ test.describe('floor-plan', () => {
// Visit floor plan page and await Meeting Data API call to complete
await Promise.all([
page.waitForResponse(`**/api/meeting/${meetingData.meeting.number}/agenda-data`),
page.goto(`/meeting/${meetingData.meeting.number}/floor-plan-neue`)
page.goto(`/meeting/${meetingData.meeting.number}/floor-plan`)
])
// Wait for page to be ready

View file

@ -12,7 +12,8 @@ export default defineConfig(({ command, mode }) => {
manifest: true,
rollupOptions: {
input: {
main: 'client/main.js'
main: 'client/main.js',
embedded: 'client/embedded.js'
}
}
},