chore: merge main into feat/tzaware
This commit is contained in:
commit
c5619948c3
39
.github/workflows/build.yml
vendored
39
.github/workflows/build.yml
vendored
|
@ -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 }}
|
||||
|
|
2
.github/workflows/ci-run-tests.yml
vendored
2
.github/workflows/ci-run-tests.yml
vendored
|
@ -81,7 +81,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
|
|
37
README.md
37
README.md
|
@ -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
|
||||
|
||||
|
|
|
@ -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
42
client/Embedded.vue
Normal 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>
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
th.agenda-table-head-check(v-if='pickerModeActive')
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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
68
client/agenda/agenda.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
98
client/components/ChatLog.vue
Normal file
98
client/components/ChatLog.vue
Normal 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>
|
79
client/components/Polls.vue
Normal file
79
client/components/Polls.vue
Normal 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
13
client/embedded.js
Normal 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)
|
||||
}
|
|
@ -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
9
client/shared/store.js
Normal 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)
|
||||
})
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
34
ietf/doc/migrations/0045_docstates_chatlogs_polls.py
Normal file
34
ietf/doc/migrations/0045_docstates_chatlogs_polls.py
Normal 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),
|
||||
]
|
|
@ -7,7 +7,7 @@ import django.utils.timezone
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('doc', '0044_procmaterials_states'),
|
||||
('doc', '0045_docstates_chatlogs_polls'),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -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'),
|
||||
]
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 "")))
|
||||
|
||||
|
||||
|
|
|
@ -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
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
35
ietf/name/migrations/0045_polls_and_chatlogs.py
Normal file
35
ietf/name/migrations/0045_polls_and_chatlogs.py
Normal 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)
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
17
ietf/person/migrations/0025_chat_and_polls_apikey.py
Normal file
17
ietf/person/migrations/0025_chat_and_polls_apikey.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -7,7 +7,7 @@ import django.utils.timezone
|
|||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('person', '0024_pronouns'),
|
||||
('person', '0025_chat_and_polls_apikey'),
|
||||
]
|
||||
|
||||
operations = [
|
|
@ -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),
|
||||
]
|
||||
|
|
|
@ -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>
|
|
@ -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}",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
64
ietf/templates/doc/document_chatlog.html
Normal file
64
ietf/templates/doc/document_chatlog.html
Normal 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 %}
|
64
ietf/templates/doc/document_polls.html
Normal file
64
ietf/templates/doc/document_polls.html
Normal 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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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 }}">
|
||||
|
|
|
@ -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>
|
|
@ -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 #}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue