Merge branch 'feat/tzaware' into jennifer/yet-more-tz-fixes
# Conflicts: # ietf/ietfauth/utils.py # ietf/meeting/tests_views.py # ietf/stats/tests.py
This commit is contained in:
commit
da70acfdff
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'
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@humanwhocodes-config-array-npm-0.10.7-81c0f4b5e6-009d64be8d.zip
vendored
Normal file
BIN
.yarn/cache/@humanwhocodes-config-array-npm-0.10.7-81c0f4b5e6-009d64be8d.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-core-npm-3.2.40-f464645db3-2683bf13ef.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-core-npm-3.2.40-f464645db3-2683bf13ef.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-dom-npm-3.2.40-df00f2f10c-d928a16ebd.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-dom-npm-3.2.40-df00f2f10c-d928a16ebd.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-sfc-npm-3.2.40-7a8032e16c-96cbfd078a.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-sfc-npm-3.2.40-7a8032e16c-96cbfd078a.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-compiler-ssr-npm-3.2.40-76cabde3f1-026461fcee.zip
vendored
Normal file
BIN
.yarn/cache/@vue-compiler-ssr-npm-3.2.40-76cabde3f1-026461fcee.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-runtime-core-npm-3.2.40-62158afe79-47c4918c12.zip
vendored
Normal file
BIN
.yarn/cache/@vue-runtime-core-npm-3.2.40-62158afe79-47c4918c12.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-runtime-dom-npm-3.2.40-1bb705cd21-4615d00cc4.zip
vendored
Normal file
BIN
.yarn/cache/@vue-runtime-dom-npm-3.2.40-1bb705cd21-4615d00cc4.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-server-renderer-npm-3.2.40-b8eff98a5b-f9d53aefca.zip
vendored
Normal file
BIN
.yarn/cache/@vue-server-renderer-npm-3.2.40-b8eff98a5b-f9d53aefca.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@vue-test-utils-npm-2.1.0-c3d34439bb-4612568209.zip
vendored
Normal file
BIN
.yarn/cache/@vue-test-utils-npm-2.1.0-c3d34439bb-4612568209.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/bootstrap-npm-5.2.2-8effda7631-14e6df28fe.zip
vendored
Normal file
BIN
.yarn/cache/bootstrap-npm-5.2.2-8effda7631-14e6df28fe.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/caniuse-lite-npm-1.0.30001414-91010a4bbf-97210cfd15.zip
vendored
Normal file
BIN
.yarn/cache/caniuse-lite-npm-1.0.30001414-91010a4bbf-97210cfd15.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/eventemitter2-npm-6.4.7-ad2467adaa-1b36a77e13.zip
vendored
Normal file
BIN
.yarn/cache/eventemitter2-npm-6.4.7-ad2467adaa-1b36a77e13.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/html-validate-npm-7.5.0-ad3a30b92f-04d73c5173.zip
vendored
Normal file
BIN
.yarn/cache/html-validate-npm-7.5.0-ad3a30b92f-04d73c5173.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/js-sdsl-npm-4.1.5-66fcf4f580-695f657ddc.zip
vendored
Normal file
BIN
.yarn/cache/js-sdsl-npm-4.1.5-66fcf4f580-695f657ddc.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/luxon-npm-3.0.4-402f9149bc-d0908c3951.zip
vendored
Normal file
BIN
.yarn/cache/luxon-npm-3.0.4-402f9149bc-d0908c3951.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/sass-npm-1.55.0-594c748a96-7d769ed08e.zip
vendored
Normal file
BIN
.yarn/cache/sass-npm-1.55.0-594c748a96-7d769ed08e.zip
vendored
Normal file
Binary file not shown.
BIN
.yarn/cache/vue-npm-3.2.39-cde0f41ddb-f096a3f0a5.zip
vendored
BIN
.yarn/cache/vue-npm-3.2.39-cde0f41ddb-f096a3f0a5.zip
vendored
Binary file not shown.
BIN
.yarn/cache/vue-npm-3.2.40-ee1b0f06d2-fb5ca87c16.zip
vendored
Normal file
BIN
.yarn/cache/vue-npm-3.2.40-ee1b0f06d2-fb5ca87c16.zip
vendored
Normal file
Binary file not shown.
29
README.md
29
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):
|
||||
> :warning: All commands below **MUST** be run from the `./playwright` directory, unless noted otherwise.
|
||||
|
||||
1. Run **once** to install dependencies on your system:
|
||||
```sh
|
||||
yarn cypress
|
||||
npm install
|
||||
npx playwright install --with-deps
|
||||
```
|
||||
To run the tests visually **(CANNOT run in docker)**:
|
||||
|
||||
2. Run in a **separate process**, from the **project root directory**:
|
||||
```sh
|
||||
yarn cypress:open
|
||||
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
|
||||
```
|
||||
> It can take a few seconds before the tests start or the GUI opens.
|
||||
|
||||
#### 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`')
|
||||
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.length < 1')
|
||||
.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='slide of state.materials.slides'
|
||||
:key='slide.id'
|
||||
:href='slide.url'
|
||||
v-for='deck of state.materials.slides.decks'
|
||||
:key='deck.id'
|
||||
:href='deck.url'
|
||||
target='_blank'
|
||||
)
|
||||
i.bi.me-2(:class='`bi-filetype-` + slide.ext')
|
||||
span {{slide.title}}
|
||||
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)
|
||||
})
|
||||
})
|
|
@ -4,3 +4,5 @@ coverage:
|
|||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
github_checks:
|
||||
annotations: false
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -225,7 +225,7 @@ def state_age_colored(doc):
|
|||
else:
|
||||
title = ""
|
||||
return mark_safe(
|
||||
'<span class="badge %s" %s><i class="bi bi-clock-fill"></i> %d</span>'
|
||||
'<span class="badge rounded-pill %s" %s><i class="bi bi-clock-fill"></i> %d</span>'
|
||||
% (class_name, title, days)
|
||||
)
|
||||
else:
|
||||
|
|
|
@ -642,10 +642,10 @@ def action_holder_badge(action_holder):
|
|||
''
|
||||
|
||||
>>> action_holder_badge(DocumentActionHolderFactory(time_added=timezone.now() - datetime.timedelta(days=16)))
|
||||
'<span class="badge bg-danger" title="In state for 16 days; goal is <15 days."><i class="bi bi-clock-fill"></i> 16</span>'
|
||||
'<span class="badge rounded-pill bg-danger" title="In state for 16 days; goal is <15 days."><i class="bi bi-clock-fill"></i> 16</span>'
|
||||
|
||||
>>> action_holder_badge(DocumentActionHolderFactory(time_added=timezone.now() - datetime.timedelta(days=30)))
|
||||
'<span class="badge bg-danger" title="In state for 30 days; goal is <15 days."><i class="bi bi-clock-fill"></i> 30</span>'
|
||||
'<span class="badge rounded-pill bg-danger" title="In state for 30 days; goal is <15 days."><i class="bi bi-clock-fill"></i> 30</span>'
|
||||
|
||||
>>> settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS = old_limit
|
||||
"""
|
||||
|
@ -653,7 +653,7 @@ def action_holder_badge(action_holder):
|
|||
age = (timezone.now() - action_holder.time_added).days
|
||||
if age > age_limit:
|
||||
return mark_safe(
|
||||
'<span class="badge bg-danger" title="In state for %d day%s; goal is <%d days."><i class="bi bi-clock-fill"></i> %d</span>'
|
||||
'<span class="badge rounded-pill bg-danger" title="In state for %d day%s; goal is <%d days."><i class="bi bi-clock-fill"></i> %d</span>'
|
||||
% (age, "s" if age != 1 else "", age_limit, age)
|
||||
)
|
||||
else:
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -41,6 +41,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,8 +642,6 @@ 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",):
|
||||
can_manage_material = can_manage_materials(request.user, doc.group)
|
||||
presentations = doc.future_presentations()
|
||||
|
@ -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 "")))
|
||||
|
||||
|
||||
|
|
|
@ -94,7 +94,7 @@ class SearchForm(forms.Form):
|
|||
("ad", "AD"), ("-ad", "AD (desc)"), ),
|
||||
required=False, widget=forms.HiddenInput)
|
||||
|
||||
doctypes = forms.ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug='draft').order_by('name'), required=False)
|
||||
doctypes = forms.ModelMultipleChoiceField(queryset=DocTypeName.objects.filter(used=True).exclude(slug__in=('draft','liai-att')).order_by('name'), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SearchForm, self).__init__(*args, **kwargs)
|
||||
|
@ -479,7 +479,7 @@ def ad_workload(request):
|
|||
|
||||
doctypes = list(
|
||||
DocTypeName.objects.filter(used=True)
|
||||
.exclude(slug="draft")
|
||||
.exclude(slug__in=("draft", "liai-att"))
|
||||
.values_list("pk", flat=True)
|
||||
)
|
||||
|
||||
|
@ -493,10 +493,10 @@ def ad_workload(request):
|
|||
for id, (g, uig) in enumerate(
|
||||
[
|
||||
("Publication Requested Internet-Draft", False),
|
||||
("Waiting for Writeup Internet-Draft", False),
|
||||
("AD Evaluation Internet-Draft", False),
|
||||
("In Last Call Internet-Draft", None),
|
||||
("IESG Evaluation - Defer Internet-Draft", None),
|
||||
("In Last Call Internet-Draft", True),
|
||||
("Waiting for Writeup Internet-Draft", False),
|
||||
("IESG Evaluation - Defer Internet-Draft", False),
|
||||
("IESG Evaluation Internet-Draft", True),
|
||||
("Waiting for AD Go-Ahead Internet-Draft", False),
|
||||
("Approved-announcement to be sent Internet-Draft", True),
|
||||
|
@ -528,9 +528,9 @@ def ad_workload(request):
|
|||
for id, (g, uig) in enumerate(
|
||||
[
|
||||
("Publication Requested Status Change", False),
|
||||
("Waiting for Writeup Status Change", False),
|
||||
("AD Evaluation Status Change", False),
|
||||
("In Last Call Status Change", None),
|
||||
("In Last Call Status Change", True),
|
||||
("Waiting for Writeup Status Change", False),
|
||||
("IESG Evaluation Status Change", True),
|
||||
("Waiting for AD Go-Ahead Status Change", False),
|
||||
]
|
||||
|
@ -675,7 +675,7 @@ def docs_for_ad(request, name):
|
|||
form = SearchForm({'by':'ad','ad': ad.id,
|
||||
'rfcs':'on', 'activedrafts':'on', 'olddrafts':'on',
|
||||
'sort': 'status',
|
||||
'doctypes': list(DocTypeName.objects.filter(used=True).exclude(slug='draft').values_list("pk", flat=True))})
|
||||
'doctypes': list(DocTypeName.objects.filter(used=True).exclude(slug__in=('draft','liai-att')).values_list("pk", flat=True))})
|
||||
results, meta = prepare_document_table(request, retrieve_search_results(form), form.data, max_results=500)
|
||||
results.sort(key=ad_dashboard_sort_key)
|
||||
del meta["headers"][-1]
|
||||
|
|
|
@ -35,6 +35,7 @@ import debug # pyflakes:ignore
|
|||
from ietf.group.factories import GroupFactory, RoleFactory
|
||||
from ietf.group.models import Group, Role, RoleName
|
||||
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.mailinglists.models import Subscribed
|
||||
from ietf.meeting.factories import MeetingFactory
|
||||
from ietf.nomcom.factories import NomComFactory
|
||||
|
@ -1006,3 +1007,11 @@ class OpenIDConnectTests(TestCase):
|
|||
# handler, causing later logging to become visible even if that wasn't intended.
|
||||
# Fail here if that happens.
|
||||
self.assertEqual(logging.root.handlers, [])
|
||||
|
||||
|
||||
class UtilsTests(TestCase):
|
||||
def test_has_role_empty_role_names(self):
|
||||
"""has_role is False if role_names is empty"""
|
||||
role = RoleFactory(name_id='secr', group__acronym='secretariat')
|
||||
self.assertTrue(has_role(role.person.user, ['Secretariat']), 'Test is broken')
|
||||
self.assertFalse(has_role(role.person.user, []), 'has_role() should return False when role_name is empty')
|
|
@ -5,7 +5,6 @@
|
|||
# various authentication and authorization utilities
|
||||
|
||||
import oidc_provider.lib.claims
|
||||
from oidc_provider.models import Client as ClientRecord
|
||||
|
||||
|
||||
from functools import wraps
|
||||
|
@ -16,7 +15,6 @@ from django.core.exceptions import PermissionDenied
|
|||
from django.db.models import Q
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import available_attrs
|
||||
from django.utils.http import urlquote
|
||||
|
||||
|
@ -95,7 +93,7 @@ def has_role(user, role_names, *args, **kwargs):
|
|||
"Robot": Q(person=person, name="robot", group__acronym="secretariat"),
|
||||
}
|
||||
|
||||
filter_expr = Q()
|
||||
filter_expr = Q(pk__in=[]) # ensure empty set is returned if no other terms are added
|
||||
for r in role_names:
|
||||
filter_expr |= role_qs[r]
|
||||
|
||||
|
@ -166,7 +164,7 @@ def is_authorized_in_doc_stream(user, doc):
|
|||
docman_roles = GroupFeatures.objects.get(type_id="ietf").docman_roles
|
||||
group_req = Q(group__acronym=doc.stream.slug)
|
||||
else:
|
||||
group_req = Q()
|
||||
group_req = Q() # no group constraint for other cases
|
||||
|
||||
return Role.objects.filter(Q(name__in=docman_roles, person__user=user) & group_req).exists()
|
||||
|
||||
|
@ -280,14 +278,6 @@ class OidcExtraScopeClaims(oidc_provider.lib.claims.ScopeClaims):
|
|||
reg.save()
|
||||
info = {}
|
||||
if regs:
|
||||
# maybe register attendance if logged in to follow a meeting
|
||||
if meeting.start_datetime() <= timezone.now() <= meeting.end_datetime():
|
||||
client = ClientRecord.objects.get(client_id=self.client.client_id)
|
||||
if client.name == 'Meetecho':
|
||||
for reg in regs:
|
||||
if not reg.attended:
|
||||
reg.attended = True
|
||||
reg.save()
|
||||
# fill in info to return
|
||||
ticket_types = set([])
|
||||
reg_types = set([])
|
||||
|
|
|
@ -187,7 +187,7 @@ def ajax_search(request):
|
|||
if not q:
|
||||
objs = IprDisclosureBase.objects.none()
|
||||
else:
|
||||
query = Q()
|
||||
query = Q() # all objects returned if no other terms in the queryset
|
||||
for t in q:
|
||||
query &= Q(title__icontains=t)
|
||||
|
||||
|
|
|
@ -258,18 +258,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
|
||||
|
@ -469,7 +486,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 = 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 = 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()
|
||||
|
@ -6557,7 +6243,7 @@ class SessionTests(TestCase):
|
|||
group_type_without_meetings = 'editorial'
|
||||
self.assertFalse(GroupFeatures.objects.get(pk=group_type_without_meetings).has_meetings)
|
||||
|
||||
area = GroupFactory(type_id='area')
|
||||
area = GroupFactory(type_id='area', acronym='area')
|
||||
requested_session = SessionFactory(meeting=meeting,group__parent=area,status_id='schedw',add_to_schedule=False)
|
||||
conflicting_session = SessionFactory(meeting=meeting,group__parent=area,status_id='schedw',add_to_schedule=False)
|
||||
ConstraintFactory(name_id='key_participant',meeting=meeting,source=requested_session.group,target=conflicting_session.group)
|
||||
|
@ -6597,7 +6283,29 @@ class SessionTests(TestCase):
|
|||
status_id='schedw',
|
||||
add_to_schedule=False,
|
||||
)
|
||||
|
||||
# bof sessions should be shown
|
||||
bof_session = SessionFactory(
|
||||
meeting=meeting,
|
||||
group__parent=area,
|
||||
group__state_id='bof',
|
||||
status_id='schedw',
|
||||
add_to_schedule=False,
|
||||
)
|
||||
# proposed WG sessions should be shown
|
||||
proposed_wg_session = SessionFactory(
|
||||
meeting=meeting,
|
||||
group__parent=area,
|
||||
group__state_id='proposed',
|
||||
status_id='schedw',
|
||||
add_to_schedule=False,
|
||||
)
|
||||
# rg sessions should be shown under 'irtf' heading
|
||||
rg_session = SessionFactory(
|
||||
meeting=meeting,
|
||||
group__type_id='rg',
|
||||
status_id='schedw',
|
||||
add_to_schedule=False,
|
||||
)
|
||||
def _sreq_edit_link(sess):
|
||||
return urlreverse(
|
||||
'ietf.secr.sreq.views.edit',
|
||||
|
@ -6630,6 +6338,19 @@ class SessionTests(TestCase):
|
|||
self.assertContains(r, _sreq_edit_link(has_meetings_not_meeting)) # link to the session request
|
||||
self.assertNotContains(r, not_has_meetings.group.acronym)
|
||||
self.assertNotContains(r, _sreq_edit_link(not_has_meetings)) # no link to the session request
|
||||
self.assertContains(r, bof_session.group.acronym)
|
||||
self.assertContains(r, _sreq_edit_link(bof_session)) # link to the session request
|
||||
self.assertContains(r, proposed_wg_session.group.acronym)
|
||||
self.assertContains(r, _sreq_edit_link(proposed_wg_session)) # link to the session request
|
||||
self.assertContains(r, rg_session.group.acronym)
|
||||
self.assertContains(r, _sreq_edit_link(rg_session)) # link to the session request
|
||||
# check headings - note that the special types (has_meetings, etc) do not have a group parent
|
||||
# so they show up in 'other'
|
||||
q = PyQuery(r.content)
|
||||
self.assertEqual(len(q('h2#area')), 1)
|
||||
self.assertEqual(len(q('h2#other-groups')), 1)
|
||||
self.assertEqual(len(q('h2#irtf')), 1) # rg group has irtf group as parent
|
||||
|
||||
|
||||
def test_request_minutes(self):
|
||||
meeting = MeetingFactory(type_id='ietf')
|
||||
|
|
|
@ -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),
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue