diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ef8822f5b..5e27af9fe 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,8 +1,8 @@ blank_issues_enabled: false contact_links: - - name: Help / Questions + - name: Help and questions url: https://github.com/ietf-tools/datatracker/discussions/categories/help-questions about: Need help? Have a question on setting up the project or its usage? - - name: Discuss New Ideas + - name: Discuss new ideas url: https://github.com/ietf-tools/datatracker/discussions/categories/ideas about: Submit ideas for new features or improvements to be discussed. diff --git a/.github/ISSUE_TEMPLATE/new-feature.yml b/.github/ISSUE_TEMPLATE/new-feature.yml index cf6717689..ddf0b575e 100644 --- a/.github/ISSUE_TEMPLATE/new-feature.yml +++ b/.github/ISSUE_TEMPLATE/new-feature.yml @@ -1,16 +1,16 @@ -name: New Feature / Enhancement -description: Propose a new idea to be implemented +name: Suggest new feature or enhancement +description: Propose a new idea to be implemented. labels: ["enhancement"] body: - type: markdown attributes: value: | - Thanks for taking the time to propose a new feature / enhancement idea. + Thanks for taking the time to propose a new feature or enhancement idea. - type: textarea id: description attributes: label: Description - description: Include as much info as possible, including mockups / screenshots if available. + description: Include as much info as possible, including mockups or screenshots if available. placeholder: Description validations: required: true diff --git a/.github/ISSUE_TEMPLATE/report-a-bug.yml b/.github/ISSUE_TEMPLATE/report-a-bug.yml index ad980cc23..d5b725d72 100644 --- a/.github/ISSUE_TEMPLATE/report-a-bug.yml +++ b/.github/ISSUE_TEMPLATE/report-a-bug.yml @@ -1,5 +1,5 @@ -name: Report a Bug -description: Something isn't right? File a bug report +name: Report a Datatracker bug +description: Something in the datatracker's behavior isn't right? File a bug report. Don't use this to report RFC errata or issues with the content of Internet-Drafts. labels: ["bug"] body: - type: markdown @@ -10,7 +10,7 @@ body: id: description attributes: label: Describe the issue - description: Include as much info as possible, including the current behavior, expected behavior, screenshots, etc. If this is a display / UX issue, make sure to list the browser(s) you're experiencing the issue on. + description: Include as much info as possible, including the current behavior, expected behavior, screenshots, etc. If this is a display or user interface issue, make sure to list the browser(s) you're experiencing the issue on. placeholder: Description validations: required: true @@ -26,3 +26,7 @@ body: attributes: value: | If you are having trouble logging into the datatracker, please do not open an issue here. Instead, please send email to support@ietf.org providing your name and username. + - type: markdown + attributes: + value: | + **Please do not report issues with the content of Internet-Drafts or RFCs using this repository. Send email to the relevant group instead. Some Internet-Drafts have their own github repositories where issues can be reported. See the datatracker's page for the I-D for links and email addresses. Errata for published RFCs are submitted at https://www.rfc-editor.org/errata.php#reportnew** diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5bbffc88e..cff6bfbd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -160,7 +160,7 @@ jobs: coverage xml - name: Upload Coverage Results to Codecov - uses: codecov/codecov-action@v2.1.0 + uses: codecov/codecov-action@v3.1.1 with: files: coverage.xml @@ -305,7 +305,7 @@ jobs: - name: Download a Coverage Results if: ${{ github.event.inputs.skiptests == 'false' }} - uses: actions/download-artifact@v3.0.0 + uses: actions/download-artifact@v3.0.2 with: name: coverage @@ -426,7 +426,7 @@ jobs: - uses: actions/checkout@v3 - name: Download a Release Artifact - uses: actions/download-artifact@v3.0.0 + uses: actions/download-artifact@v3.0.2 with: name: release-${{ env.PKG_VERSION }} diff --git a/.github/workflows/ci-run-tests.yml b/.github/workflows/ci-run-tests.yml index 17346ec65..172d10356 100644 --- a/.github/workflows/ci-run-tests.yml +++ b/.github/workflows/ci-run-tests.yml @@ -50,7 +50,7 @@ jobs: coverage xml - name: Upload Coverage Results to Codecov - uses: codecov/codecov-action@v2.1.0 + uses: codecov/codecov-action@v3.1.1 with: files: coverage.xml diff --git a/.pnp.cjs b/.pnp.cjs index 0e5e1e98d..414b8bbfe 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -34,14 +34,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageLocation": "./",\ "packageDependencies": [\ ["@faker-js/faker", "npm:7.6.0"],\ - ["@fullcalendar/bootstrap5", "npm:5.11.4"],\ - ["@fullcalendar/core", "npm:5.11.4"],\ - ["@fullcalendar/daygrid", "npm:5.11.4"],\ - ["@fullcalendar/interaction", "npm:5.11.4"],\ - ["@fullcalendar/list", "npm:5.11.4"],\ - ["@fullcalendar/luxon2", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.11.4"],\ - ["@fullcalendar/timegrid", "npm:5.11.4"],\ - ["@fullcalendar/vue3", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.11.4"],\ + ["@fullcalendar/bootstrap5", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@fullcalendar/daygrid", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/interaction", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/list", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/luxon2", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/timegrid", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/vue3", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ ["@parcel/optimizer-data-url", "npm:2.8.3"],\ ["@parcel/transformer-inline-string", "npm:2.8.3"],\ ["@parcel/transformer-sass", "npm:2.8.3"],\ @@ -260,89 +260,123 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@fullcalendar/bootstrap5", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-bootstrap5-npm-5.11.4-c3e252aaf4-26f838f304.zip/node_modules/@fullcalendar/bootstrap5/",\ + ["npm:6.1.4", {\ + "packageLocation": "./.yarn/cache/@fullcalendar-bootstrap5-npm-6.1.4-0bc121fab1-e4a5dd281d.zip/node_modules/@fullcalendar/bootstrap5/",\ "packageDependencies": [\ - ["@fullcalendar/bootstrap5", "npm:5.11.4"],\ - ["@fullcalendar/common", "npm:5.11.4"],\ - ["tslib", "npm:2.4.0"]\ + ["@fullcalendar/bootstrap5", "npm:6.1.4"]\ ],\ - "linkType": "HARD"\ - }]\ - ]],\ - ["@fullcalendar/common", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-common-npm-5.11.4-b6ba4b8756-8fc0e05539.zip/node_modules/@fullcalendar/common/",\ + "linkType": "SOFT"\ + }],\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4", {\ + "packageLocation": "./.yarn/__virtual__/@fullcalendar-bootstrap5-virtual-e9419af0e8/0/cache/@fullcalendar-bootstrap5-npm-6.1.4-0bc121fab1-e4a5dd281d.zip/node_modules/@fullcalendar/bootstrap5/",\ "packageDependencies": [\ - ["@fullcalendar/common", "npm:5.11.4"],\ - ["tslib", "npm:2.4.0"]\ + ["@fullcalendar/bootstrap5", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@types/fullcalendar__core", null]\ + ],\ + "packagePeers": [\ + "@fullcalendar/core",\ + "@types/fullcalendar__core"\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@fullcalendar/core", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-core-npm-5.11.4-2234b9e7f4-11652a58dc.zip/node_modules/@fullcalendar/core/",\ + ["npm:6.1.4", {\ + "packageLocation": "./.yarn/cache/@fullcalendar-core-npm-6.1.4-a304b2f512-3c659bbaf8.zip/node_modules/@fullcalendar/core/",\ "packageDependencies": [\ - ["@fullcalendar/core", "npm:5.11.4"],\ - ["@fullcalendar/common", "npm:5.11.4"],\ - ["preact", "npm:10.7.2"],\ - ["tslib", "npm:2.4.0"]\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["preact", "npm:10.7.2"]\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@fullcalendar/daygrid", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-daygrid-npm-5.11.4-821caf4780-a25d83cfe5.zip/node_modules/@fullcalendar/daygrid/",\ + ["npm:6.1.4", {\ + "packageLocation": "./.yarn/cache/@fullcalendar-daygrid-npm-6.1.4-51af4b5615-24b3c6e521.zip/node_modules/@fullcalendar/daygrid/",\ "packageDependencies": [\ - ["@fullcalendar/daygrid", "npm:5.11.4"],\ - ["@fullcalendar/common", "npm:5.11.4"],\ - ["tslib", "npm:2.4.0"]\ + ["@fullcalendar/daygrid", "npm:6.1.4"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4", {\ + "packageLocation": "./.yarn/__virtual__/@fullcalendar-daygrid-virtual-c7610fd32f/0/cache/@fullcalendar-daygrid-npm-6.1.4-51af4b5615-24b3c6e521.zip/node_modules/@fullcalendar/daygrid/",\ + "packageDependencies": [\ + ["@fullcalendar/daygrid", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@types/fullcalendar__core", null]\ + ],\ + "packagePeers": [\ + "@fullcalendar/core",\ + "@types/fullcalendar__core"\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@fullcalendar/interaction", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-interaction-npm-5.11.4-ba2c965da3-88231b9254.zip/node_modules/@fullcalendar/interaction/",\ + ["npm:6.1.4", {\ + "packageLocation": "./.yarn/cache/@fullcalendar-interaction-npm-6.1.4-a5a798ee1e-5e282ba36b.zip/node_modules/@fullcalendar/interaction/",\ "packageDependencies": [\ - ["@fullcalendar/interaction", "npm:5.11.4"],\ - ["@fullcalendar/common", "npm:5.11.4"],\ - ["tslib", "npm:2.4.0"]\ + ["@fullcalendar/interaction", "npm:6.1.4"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4", {\ + "packageLocation": "./.yarn/__virtual__/@fullcalendar-interaction-virtual-a72554b59a/0/cache/@fullcalendar-interaction-npm-6.1.4-a5a798ee1e-5e282ba36b.zip/node_modules/@fullcalendar/interaction/",\ + "packageDependencies": [\ + ["@fullcalendar/interaction", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@types/fullcalendar__core", null]\ + ],\ + "packagePeers": [\ + "@fullcalendar/core",\ + "@types/fullcalendar__core"\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@fullcalendar/list", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-list-npm-5.11.4-4791653eeb-e2cec5e89c.zip/node_modules/@fullcalendar/list/",\ + ["npm:6.1.4", {\ + "packageLocation": "./.yarn/cache/@fullcalendar-list-npm-6.1.4-9af3788481-0338a8bb15.zip/node_modules/@fullcalendar/list/",\ "packageDependencies": [\ - ["@fullcalendar/list", "npm:5.11.4"],\ - ["@fullcalendar/common", "npm:5.11.4"],\ - ["tslib", "npm:2.4.0"]\ + ["@fullcalendar/list", "npm:6.1.4"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4", {\ + "packageLocation": "./.yarn/__virtual__/@fullcalendar-list-virtual-0c2dba2b68/0/cache/@fullcalendar-list-npm-6.1.4-9af3788481-0338a8bb15.zip/node_modules/@fullcalendar/list/",\ + "packageDependencies": [\ + ["@fullcalendar/list", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@types/fullcalendar__core", null]\ + ],\ + "packagePeers": [\ + "@fullcalendar/core",\ + "@types/fullcalendar__core"\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@fullcalendar/luxon2", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-luxon2-npm-5.11.4-e4b0003255-503e3e32d2.zip/node_modules/@fullcalendar/luxon2/",\ + ["npm:6.1.4", {\ + "packageLocation": "./.yarn/cache/@fullcalendar-luxon2-npm-6.1.4-09e1c45826-577283ad7c.zip/node_modules/@fullcalendar/luxon2/",\ "packageDependencies": [\ - ["@fullcalendar/luxon2", "npm:5.11.4"]\ + ["@fullcalendar/luxon2", "npm:6.1.4"]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.11.4", {\ - "packageLocation": "./.yarn/__virtual__/@fullcalendar-luxon2-virtual-a083616d6e/0/cache/@fullcalendar-luxon2-npm-5.11.4-e4b0003255-503e3e32d2.zip/node_modules/@fullcalendar/luxon2/",\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4", {\ + "packageLocation": "./.yarn/__virtual__/@fullcalendar-luxon2-virtual-98927d48fd/0/cache/@fullcalendar-luxon2-npm-6.1.4-09e1c45826-577283ad7c.zip/node_modules/@fullcalendar/luxon2/",\ "packageDependencies": [\ - ["@fullcalendar/luxon2", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.11.4"],\ - ["@fullcalendar/common", "npm:5.11.4"],\ + ["@fullcalendar/luxon2", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@types/fullcalendar__core", null],\ ["@types/luxon", null],\ - ["luxon", "npm:3.2.1"],\ - ["tslib", "npm:2.4.0"]\ + ["luxon", "npm:3.2.1"]\ ],\ "packagePeers": [\ + "@fullcalendar/core",\ + "@types/fullcalendar__core",\ "@types/luxon",\ "luxon"\ ],\ @@ -350,35 +384,48 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { }]\ ]],\ ["@fullcalendar/timegrid", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-timegrid-npm-5.11.4-64a0cfa5de-3a2fccac65.zip/node_modules/@fullcalendar/timegrid/",\ + ["npm:6.1.4", {\ + "packageLocation": "./.yarn/cache/@fullcalendar-timegrid-npm-6.1.4-36b739a426-1329b941f9.zip/node_modules/@fullcalendar/timegrid/",\ "packageDependencies": [\ - ["@fullcalendar/timegrid", "npm:5.11.4"],\ - ["@fullcalendar/common", "npm:5.11.4"],\ - ["@fullcalendar/daygrid", "npm:5.11.4"],\ - ["tslib", "npm:2.4.0"]\ + ["@fullcalendar/timegrid", "npm:6.1.4"]\ + ],\ + "linkType": "SOFT"\ + }],\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4", {\ + "packageLocation": "./.yarn/__virtual__/@fullcalendar-timegrid-virtual-ebfa3e5bc2/0/cache/@fullcalendar-timegrid-npm-6.1.4-36b739a426-1329b941f9.zip/node_modules/@fullcalendar/timegrid/",\ + "packageDependencies": [\ + ["@fullcalendar/timegrid", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@fullcalendar/daygrid", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@types/fullcalendar__core", null]\ + ],\ + "packagePeers": [\ + "@fullcalendar/core",\ + "@types/fullcalendar__core"\ ],\ "linkType": "HARD"\ }]\ ]],\ ["@fullcalendar/vue3", [\ - ["npm:5.11.4", {\ - "packageLocation": "./.yarn/cache/@fullcalendar-vue3-npm-5.11.4-adcf8ba171-3e0fc0423b.zip/node_modules/@fullcalendar/vue3/",\ + ["npm:6.1.4", {\ + "packageLocation": "./.yarn/cache/@fullcalendar-vue3-npm-6.1.4-db9e1f7c10-3e11102fbf.zip/node_modules/@fullcalendar/vue3/",\ "packageDependencies": [\ - ["@fullcalendar/vue3", "npm:5.11.4"]\ + ["@fullcalendar/vue3", "npm:6.1.4"]\ ],\ "linkType": "SOFT"\ }],\ - ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.11.4", {\ - "packageLocation": "./.yarn/__virtual__/@fullcalendar-vue3-virtual-a335aaeca0/0/cache/@fullcalendar-vue3-npm-5.11.4-adcf8ba171-3e0fc0423b.zip/node_modules/@fullcalendar/vue3/",\ + ["virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4", {\ + "packageLocation": "./.yarn/__virtual__/@fullcalendar-vue3-virtual-2510e107ca/0/cache/@fullcalendar-vue3-npm-6.1.4-db9e1f7c10-3e11102fbf.zip/node_modules/@fullcalendar/vue3/",\ "packageDependencies": [\ - ["@fullcalendar/vue3", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.11.4"],\ - ["@fullcalendar/core", "npm:5.11.4"],\ + ["@fullcalendar/vue3", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@types/fullcalendar__core", null],\ ["@types/vue", null],\ - ["tslib", "npm:2.4.0"],\ ["vue", "npm:3.2.47"]\ ],\ "packagePeers": [\ + "@fullcalendar/core",\ + "@types/fullcalendar__core",\ "@types/vue",\ "vue"\ ],\ @@ -7400,14 +7447,14 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "packageDependencies": [\ ["root-workspace-0b6124", "workspace:."],\ ["@faker-js/faker", "npm:7.6.0"],\ - ["@fullcalendar/bootstrap5", "npm:5.11.4"],\ - ["@fullcalendar/core", "npm:5.11.4"],\ - ["@fullcalendar/daygrid", "npm:5.11.4"],\ - ["@fullcalendar/interaction", "npm:5.11.4"],\ - ["@fullcalendar/list", "npm:5.11.4"],\ - ["@fullcalendar/luxon2", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.11.4"],\ - ["@fullcalendar/timegrid", "npm:5.11.4"],\ - ["@fullcalendar/vue3", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:5.11.4"],\ + ["@fullcalendar/bootstrap5", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/core", "npm:6.1.4"],\ + ["@fullcalendar/daygrid", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/interaction", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/list", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/luxon2", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/timegrid", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ + ["@fullcalendar/vue3", "virtual:dc3fc578bfa5e06182a4d2be39ede0bc5b74940b1ffe0d70c26892ab140a4699787750fba175dc306292e80b4aa2c8c5f68c2a821e69b2c37e360c0dff36ff66#npm:6.1.4"],\ ["@parcel/optimizer-data-url", "npm:2.8.3"],\ ["@parcel/transformer-inline-string", "npm:2.8.3"],\ ["@parcel/transformer-sass", "npm:2.8.3"],\ diff --git a/.pylintrc b/.pylintrc index c9a33fcec..008f89e45 100644 --- a/.pylintrc +++ b/.pylintrc @@ -405,4 +405,4 @@ analyse-fallback-blocks=no # Exceptions that will emit a warning when being caught. Defaults to # "Exception" -overgeneral-exceptions=Exception +overgeneral-exceptions=builtins.Exception diff --git a/.yarn/cache/@fullcalendar-bootstrap5-npm-5.11.4-c3e252aaf4-26f838f304.zip b/.yarn/cache/@fullcalendar-bootstrap5-npm-5.11.4-c3e252aaf4-26f838f304.zip deleted file mode 100644 index 09650f0cf..000000000 Binary files a/.yarn/cache/@fullcalendar-bootstrap5-npm-5.11.4-c3e252aaf4-26f838f304.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-bootstrap5-npm-6.1.4-0bc121fab1-e4a5dd281d.zip b/.yarn/cache/@fullcalendar-bootstrap5-npm-6.1.4-0bc121fab1-e4a5dd281d.zip new file mode 100644 index 000000000..02c4dafeb Binary files /dev/null and b/.yarn/cache/@fullcalendar-bootstrap5-npm-6.1.4-0bc121fab1-e4a5dd281d.zip differ diff --git a/.yarn/cache/@fullcalendar-common-npm-5.11.4-b6ba4b8756-8fc0e05539.zip b/.yarn/cache/@fullcalendar-common-npm-5.11.4-b6ba4b8756-8fc0e05539.zip deleted file mode 100644 index 1ff37662e..000000000 Binary files a/.yarn/cache/@fullcalendar-common-npm-5.11.4-b6ba4b8756-8fc0e05539.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-core-npm-5.11.4-2234b9e7f4-11652a58dc.zip b/.yarn/cache/@fullcalendar-core-npm-5.11.4-2234b9e7f4-11652a58dc.zip deleted file mode 100644 index b1e6b32ad..000000000 Binary files a/.yarn/cache/@fullcalendar-core-npm-5.11.4-2234b9e7f4-11652a58dc.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-core-npm-6.1.4-a304b2f512-3c659bbaf8.zip b/.yarn/cache/@fullcalendar-core-npm-6.1.4-a304b2f512-3c659bbaf8.zip new file mode 100644 index 000000000..ca26602f1 Binary files /dev/null and b/.yarn/cache/@fullcalendar-core-npm-6.1.4-a304b2f512-3c659bbaf8.zip differ diff --git a/.yarn/cache/@fullcalendar-daygrid-npm-5.11.4-821caf4780-a25d83cfe5.zip b/.yarn/cache/@fullcalendar-daygrid-npm-5.11.4-821caf4780-a25d83cfe5.zip deleted file mode 100644 index 344b8208e..000000000 Binary files a/.yarn/cache/@fullcalendar-daygrid-npm-5.11.4-821caf4780-a25d83cfe5.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-daygrid-npm-6.1.4-51af4b5615-24b3c6e521.zip b/.yarn/cache/@fullcalendar-daygrid-npm-6.1.4-51af4b5615-24b3c6e521.zip new file mode 100644 index 000000000..885be074c Binary files /dev/null and b/.yarn/cache/@fullcalendar-daygrid-npm-6.1.4-51af4b5615-24b3c6e521.zip differ diff --git a/.yarn/cache/@fullcalendar-interaction-npm-5.11.4-ba2c965da3-88231b9254.zip b/.yarn/cache/@fullcalendar-interaction-npm-5.11.4-ba2c965da3-88231b9254.zip deleted file mode 100644 index 8e19461a7..000000000 Binary files a/.yarn/cache/@fullcalendar-interaction-npm-5.11.4-ba2c965da3-88231b9254.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-interaction-npm-6.1.4-a5a798ee1e-5e282ba36b.zip b/.yarn/cache/@fullcalendar-interaction-npm-6.1.4-a5a798ee1e-5e282ba36b.zip new file mode 100644 index 000000000..aab66a24e Binary files /dev/null and b/.yarn/cache/@fullcalendar-interaction-npm-6.1.4-a5a798ee1e-5e282ba36b.zip differ diff --git a/.yarn/cache/@fullcalendar-list-npm-5.11.4-4791653eeb-e2cec5e89c.zip b/.yarn/cache/@fullcalendar-list-npm-5.11.4-4791653eeb-e2cec5e89c.zip deleted file mode 100644 index b84b99ddd..000000000 Binary files a/.yarn/cache/@fullcalendar-list-npm-5.11.4-4791653eeb-e2cec5e89c.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-list-npm-6.1.4-9af3788481-0338a8bb15.zip b/.yarn/cache/@fullcalendar-list-npm-6.1.4-9af3788481-0338a8bb15.zip new file mode 100644 index 000000000..7641c2a1c Binary files /dev/null and b/.yarn/cache/@fullcalendar-list-npm-6.1.4-9af3788481-0338a8bb15.zip differ diff --git a/.yarn/cache/@fullcalendar-luxon2-npm-5.11.4-e4b0003255-503e3e32d2.zip b/.yarn/cache/@fullcalendar-luxon2-npm-5.11.4-e4b0003255-503e3e32d2.zip deleted file mode 100644 index be21e9f25..000000000 Binary files a/.yarn/cache/@fullcalendar-luxon2-npm-5.11.4-e4b0003255-503e3e32d2.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-luxon2-npm-6.1.4-09e1c45826-577283ad7c.zip b/.yarn/cache/@fullcalendar-luxon2-npm-6.1.4-09e1c45826-577283ad7c.zip new file mode 100644 index 000000000..2ae2d9cf1 Binary files /dev/null and b/.yarn/cache/@fullcalendar-luxon2-npm-6.1.4-09e1c45826-577283ad7c.zip differ diff --git a/.yarn/cache/@fullcalendar-timegrid-npm-5.11.4-64a0cfa5de-3a2fccac65.zip b/.yarn/cache/@fullcalendar-timegrid-npm-5.11.4-64a0cfa5de-3a2fccac65.zip deleted file mode 100644 index 03359f22b..000000000 Binary files a/.yarn/cache/@fullcalendar-timegrid-npm-5.11.4-64a0cfa5de-3a2fccac65.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-timegrid-npm-6.1.4-36b739a426-1329b941f9.zip b/.yarn/cache/@fullcalendar-timegrid-npm-6.1.4-36b739a426-1329b941f9.zip new file mode 100644 index 000000000..21c647e22 Binary files /dev/null and b/.yarn/cache/@fullcalendar-timegrid-npm-6.1.4-36b739a426-1329b941f9.zip differ diff --git a/.yarn/cache/@fullcalendar-vue3-npm-5.11.4-adcf8ba171-3e0fc0423b.zip b/.yarn/cache/@fullcalendar-vue3-npm-5.11.4-adcf8ba171-3e0fc0423b.zip deleted file mode 100644 index 55622e1a1..000000000 Binary files a/.yarn/cache/@fullcalendar-vue3-npm-5.11.4-adcf8ba171-3e0fc0423b.zip and /dev/null differ diff --git a/.yarn/cache/@fullcalendar-vue3-npm-6.1.4-db9e1f7c10-3e11102fbf.zip b/.yarn/cache/@fullcalendar-vue3-npm-6.1.4-db9e1f7c10-3e11102fbf.zip new file mode 100644 index 000000000..ad8bd3139 Binary files /dev/null and b/.yarn/cache/@fullcalendar-vue3-npm-6.1.4-db9e1f7c10-3e11102fbf.zip differ diff --git a/client/agenda/AgendaScheduleCalendar.vue b/client/agenda/AgendaScheduleCalendar.vue index 0dbea168a..bc1ee6a0d 100644 --- a/client/agenda/AgendaScheduleCalendar.vue +++ b/client/agenda/AgendaScheduleCalendar.vue @@ -81,7 +81,6 @@ import { NPopover } from 'naive-ui' -import '@fullcalendar/core/vdom' // solves problem with Vite import FullCalendar from '@fullcalendar/vue3' import timeGridPlugin from '@fullcalendar/timegrid' import interactionPlugin from '@fullcalendar/interaction' diff --git a/docker/configs/settings_local.py b/docker/configs/settings_local.py index 759bf6b7f..79442b2e9 100644 --- a/docker/configs/settings_local.py +++ b/docker/configs/settings_local.py @@ -50,8 +50,8 @@ CHARTER_PATH = '/assets/ietf-ftp/charter/' BOFREQ_PATH = '/assets/ietf-ftp/bofreq/' CONFLICT_REVIEW_PATH = '/assets/ietf-ftp/conflict-reviews/' STATUS_CHANGE_PATH = '/assets/ietf-ftp/status-changes/' -INTERNET_DRAFT_ARCHIVE_DIR = '/assets/ietf-ftp/internet-drafts/' -INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/assets/ietf-ftp/internet-drafts/' +INTERNET_DRAFT_ARCHIVE_DIR = '/assets/archive/id' +INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/assets/archive/id' NOMCOM_PUBLIC_KEYS_DIR = 'data/nomcom_keys/public_keys/' SLIDE_STAGING_PATH = 'test/staging/' diff --git a/ietf/api/tests.py b/ietf/api/tests.py index 1be220f0f..124f3edd5 100644 --- a/ietf/api/tests.py +++ b/ietf/api/tests.py @@ -23,6 +23,8 @@ import debug # pyflakes:ignore import ietf from ietf.doc.utils import get_unicode_document_content +from ietf.doc.models import RelatedDocument, State +from ietf.doc.factories import IndividualDraftFactory, WgDraftFactory from ietf.group.factories import RoleFactory from ietf.meeting.factories import MeetingFactory, SessionFactory from ietf.meeting.test_data import make_meeting_test_data @@ -33,7 +35,7 @@ from ietf.person.models import PersonalApiKey from ietf.stats.models import MeetingRegistration from ietf.utils.mail import outbox, get_payload_text from ietf.utils.models import DumpInfo -from ietf.utils.test_utils import TestCase, login_testing_unauthorized +from ietf.utils.test_utils import TestCase, login_testing_unauthorized, reload_db_objects OMITTED_APPS = ( 'ietf.secr.meetings', @@ -589,3 +591,207 @@ class TastypieApiTestCase(ResourceTestCaseMixin, TestCase): #print("There doesn't seem to be any resource for model %s.models.%s"%(app.__name__,model.__name__,)) self.assertIn(model._meta.model_name, list(app_resources.keys()), "There doesn't seem to be any API resource for model %s.models.%s"%(app.__name__,model.__name__,)) + + +class RfcdiffSupportTests(TestCase): + + def setUp(self): + super().setUp() + self.target_view = 'ietf.api.views.rfcdiff_latest_json' + self._last_rfc_num = 8000 + + def getJson(self, view_args): + url = urlreverse(self.target_view, kwargs=view_args) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + return r.json() + + def next_rfc_number(self): + self._last_rfc_num += 1 + return self._last_rfc_num + + def do_draft_test(self, name): + draft = IndividualDraftFactory(name=name, rev='00', create_revisions=range(0,13)) + draft = reload_db_objects(draft) + prev_draft_rev = f'{(int(draft.rev)-1):02d}' + + received = self.getJson(dict(name=draft.name)) + self.assertEqual( + received, + dict( + name=draft.name, + rev=draft.rev, + content_url=draft.get_href(), + previous=f'{draft.name}-{prev_draft_rev}', + previous_url= draft.history_set.get(rev=prev_draft_rev).get_href(), + ), + 'Incorrect JSON when draft revision not specified', + ) + + received = self.getJson(dict(name=draft.name, rev=draft.rev)) + self.assertEqual( + received, + dict( + name=draft.name, + rev=draft.rev, + content_url=draft.get_href(), + previous=f'{draft.name}-{prev_draft_rev}', + previous_url= draft.history_set.get(rev=prev_draft_rev).get_href(), + ), + 'Incorrect JSON when latest revision specified', + ) + + received = self.getJson(dict(name=draft.name, rev='10')) + prev_draft_rev = '09' + self.assertEqual( + received, + dict( + name=draft.name, + rev='10', + content_url=draft.history_set.get(rev='10').get_href(), + previous=f'{draft.name}-{prev_draft_rev}', + previous_url= draft.history_set.get(rev=prev_draft_rev).get_href(), + ), + 'Incorrect JSON when historical revision specified', + ) + + received = self.getJson(dict(name=draft.name, rev='00')) + self.assertNotIn('previous', received, 'Rev 00 has no previous name when not replacing a draft') + + replaced = IndividualDraftFactory() + RelatedDocument.objects.create(relationship_id='replaces',source=draft,target=replaced.docalias.first()) + received = self.getJson(dict(name=draft.name, rev='00')) + self.assertEqual(received['previous'], f'{replaced.name}-{replaced.rev}', + 'Rev 00 has a previous name when replacing a draft') + + def test_draft(self): + # test with typical, straightforward names + self.do_draft_test(name='draft-somebody-did-a-thing') + # try with different potentially problematic names + self.do_draft_test(name='draft-someone-did-something-01-02') + self.do_draft_test(name='draft-someone-did-something-else-02') + self.do_draft_test(name='draft-someone-did-something-02-weird-01') + + def do_draft_with_broken_history_test(self, name): + draft = IndividualDraftFactory(name=name, rev='10') + received = self.getJson(dict(name=draft.name,rev='09')) + self.assertEqual(received['rev'],'09') + self.assertEqual(received['previous'], f'{draft.name}-08') + self.assertTrue('warning' in received) + + def test_draft_with_broken_history(self): + # test with typical, straightforward names + self.do_draft_with_broken_history_test(name='draft-somebody-did-something') + # try with different potentially problematic names + self.do_draft_with_broken_history_test(name='draft-someone-did-something-01-02') + self.do_draft_with_broken_history_test(name='draft-someone-did-something-else-02') + self.do_draft_with_broken_history_test(name='draft-someone-did-something-02-weird-03') + + def do_rfc_test(self, draft_name): + draft = WgDraftFactory(name=draft_name, create_revisions=range(0,2)) + draft.docalias.create(name=f'rfc{self.next_rfc_number():04}') + draft.set_state(State.objects.get(type_id='draft',slug='rfc')) + draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) + draft = reload_db_objects(draft) + rfc = draft + + number = rfc.rfc_number() + received = self.getJson(dict(name=number)) + self.assertEqual( + received, + dict( + content_url=rfc.get_href(), + name=rfc.canonical_name(), + previous=f'{draft.name}-{draft.rev}', + previous_url= draft.history_set.get(rev=draft.rev).get_href(), + ), + 'Can look up an RFC by number', + ) + + num_received = received + received = self.getJson(dict(name=rfc.canonical_name())) + self.assertEqual(num_received, received, 'RFC by canonical name gives same result as by number') + + received = self.getJson(dict(name=f'RfC {number}')) + self.assertEqual(num_received, received, 'RFC with unusual spacing/caps gives same result as by number') + + received = self.getJson(dict(name=draft.name)) + self.assertEqual(num_received, received, 'RFC by draft name and no rev gives same result as by number') + + received = self.getJson(dict(name=draft.name, rev='01')) + prev_draft_rev = '00' + self.assertEqual( + received, + dict( + content_url=draft.history_set.get(rev='01').get_href(), + name=draft.name, + rev='01', + previous=f'{draft.name}-{prev_draft_rev}', + previous_url= draft.history_set.get(rev=prev_draft_rev).get_href(), + ), + 'RFC by draft name with rev should give draft name, not canonical name' + ) + + def test_rfc(self): + # simple draft name + self.do_rfc_test(draft_name='draft-test-ar-ef-see') + # tricky draft names + self.do_rfc_test(draft_name='draft-whatever-02') + self.do_rfc_test(draft_name='draft-test-me-03-04') + + def test_rfc_with_tombstone(self): + draft = WgDraftFactory(create_revisions=range(0,2)) + draft.docalias.create(name='rfc3261') # See views_doc.HAS_TOMBSTONE + draft.set_state(State.objects.get(type_id='draft',slug='rfc')) + draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) + draft = reload_db_objects(draft) + rfc = draft + + # Some old rfcs had tombstones that shouldn't be used for comparisons + received = self.getJson(dict(name=rfc.canonical_name())) + self.assertTrue(received['previous'].endswith('00')) + + def do_rfc_with_broken_history_test(self, draft_name): + draft = WgDraftFactory(rev='10', name=draft_name) + draft.docalias.create(name=f'rfc{self.next_rfc_number():04}') + draft.set_state(State.objects.get(type_id='draft',slug='rfc')) + draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) + draft = reload_db_objects(draft) + rfc = draft + + received = self.getJson(dict(name=draft.name)) + self.assertEqual( + received, + dict( + content_url=rfc.get_href(), + name=rfc.canonical_name(), + previous=f'{draft.name}-10', + previous_url= f'{settings.IETF_ID_ARCHIVE_URL}{draft.name}-10.txt', + ), + 'RFC by draft name without rev should return canonical RFC name and no rev', + ) + + received = self.getJson(dict(name=draft.name, rev='10')) + self.assertEqual(received['name'], draft.name, 'RFC by draft name with rev should return draft name') + self.assertEqual(received['rev'], '10', 'Requested rev should be returned') + self.assertEqual(received['previous'], f'{draft.name}-09', 'Previous rev is one less than requested') + self.assertIn(f'{draft.name}-10', received['content_url'], 'Returned URL should include requested rev') + self.assertNotIn('warning', received, 'No warning when we have the rev requested') + + received = self.getJson(dict(name=f'{draft.name}-09')) + self.assertEqual(received['name'], draft.name, 'RFC by draft name with rev should return draft name') + self.assertEqual(received['rev'], '09', 'Requested rev should be returned') + self.assertEqual(received['previous'], f'{draft.name}-08', 'Previous rev is one less than requested') + self.assertIn(f'{draft.name}-09', received['content_url'], 'Returned URL should include requested rev') + self.assertEqual( + received['warning'], + 'History for this version not found - these results are speculation', + 'Warning should be issued when requested rev is not found' + ) + + def test_rfc_with_broken_history(self): + # simple draft name + self.do_rfc_with_broken_history_test(draft_name='draft-some-draft') + # tricky draft names + self.do_rfc_with_broken_history_test(draft_name='draft-gizmo-01') + self.do_rfc_with_broken_history_test(draft_name='draft-oh-boy-what-a-draft-02-03') diff --git a/ietf/api/urls.py b/ietf/api/urls.py index 714be8a6a..aff73d607 100644 --- a/ietf/api/urls.py +++ b/ietf/api/urls.py @@ -1,5 +1,6 @@ # Copyright The IETF Trust 2017, All Rights Reserved +from django.conf import settings from django.conf.urls import include from django.views.generic import TemplateView @@ -56,6 +57,9 @@ urlpatterns = [ url(r'^version/?$', api_views.version), # Application authentication API key url(r'^appauth/[authortools|bibxml]', api_views.app_auth), + # latest versions + url(r'^rfcdiff-latest-json/%(name)s(?:-%(rev)s)?(\.txt|\.html)?/?$' % settings.URL_REGEXPS, api_views.rfcdiff_latest_json), + url(r'^rfcdiff-latest-json/(?P[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', api_views.rfcdiff_latest_json), ] # Additional (standard) Tastypie endpoints diff --git a/ietf/api/views.py b/ietf/api/views.py index ea7af3caf..18cfa38b7 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -4,6 +4,7 @@ import json import pytz +import re from jwcrypto.jwk import JWK @@ -12,7 +13,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.core.validators import validate_email -from django.http import HttpResponse +from django.http import HttpResponse, Http404 from django.shortcuts import render, get_object_or_404 from django.urls import reverse from django.utils.decorators import method_decorator @@ -31,10 +32,12 @@ import ietf from ietf.person.models import Person, Email from ietf.api import _api_list from ietf.api.serializer import JsonExportMixin +from ietf.doc.utils import fuzzy_find_documents from ietf.ietfauth.views import send_account_creation_email from ietf.ietfauth.utils import role_required from ietf.meeting.models import Meeting from ietf.stats.models import MeetingRegistration +from ietf.utils import log from ietf.utils.decorators import require_api_key from ietf.utils.models import DumpInfo @@ -225,3 +228,163 @@ def app_auth(request): return HttpResponse( json.dumps({'success': True}), content_type='application/json') + + + +def find_doc_for_rfcdiff(name, rev): + """rfcdiff lookup heuristics + + Returns a tuple with: + [0] - condition string + [1] - document found (or None) + [2] - historic version + [3] - revision actually found (may differ from :rev: input) + """ + found = fuzzy_find_documents(name, rev) + condition = 'no such document' + if found.documents.count() != 1: + return (condition, None, None, rev) + doc = found.documents.get() + if found.matched_rev is None or doc.rev == found.matched_rev: + condition = 'current version' + return (condition, doc, None, found.matched_rev) + else: + candidate = doc.history_set.filter(rev=found.matched_rev).order_by("-time").first() + if candidate: + condition = 'historic version' + return (condition, doc, candidate, found.matched_rev) + else: + condition = 'version dochistory not found' + return (condition, doc, None, found.matched_rev) + +# This is a proof of concept of a service that would redirect to the current revision +# def rfcdiff_latest(request, name, rev=None): +# condition, doc, history = find_doc_for_rfcdiff(name, rev) +# if not doc: +# raise Http404 +# if history: +# return redirect(history.get_href()) +# else: +# return redirect(doc.get_href()) + +HAS_TOMBSTONE = [ + 2821, 2822, 2873, 2919, 2961, 3023, 3029, 3031, 3032, 3033, 3034, 3035, 3036, + 3037, 3038, 3042, 3044, 3050, 3052, 3054, 3055, 3056, 3057, 3059, 3060, 3061, + 3062, 3063, 3064, 3067, 3068, 3069, 3070, 3071, 3072, 3073, 3074, 3075, 3076, + 3077, 3078, 3080, 3081, 3082, 3084, 3085, 3086, 3087, 3088, 3089, 3090, 3094, + 3095, 3096, 3097, 3098, 3101, 3102, 3103, 3104, 3105, 3106, 3107, 3108, 3109, + 3110, 3111, 3112, 3113, 3114, 3115, 3116, 3117, 3118, 3119, 3120, 3121, 3123, + 3124, 3126, 3127, 3128, 3130, 3131, 3132, 3133, 3134, 3135, 3136, 3137, 3138, + 3139, 3140, 3141, 3142, 3143, 3144, 3145, 3147, 3149, 3150, 3151, 3152, 3153, + 3154, 3155, 3156, 3157, 3158, 3159, 3160, 3161, 3162, 3163, 3164, 3165, 3166, + 3167, 3168, 3169, 3170, 3171, 3172, 3173, 3174, 3176, 3179, 3180, 3181, 3182, + 3183, 3184, 3185, 3186, 3187, 3188, 3189, 3190, 3191, 3192, 3193, 3194, 3197, + 3198, 3201, 3202, 3203, 3204, 3205, 3206, 3207, 3208, 3209, 3210, 3211, 3212, + 3213, 3214, 3215, 3216, 3217, 3218, 3220, 3221, 3222, 3224, 3225, 3226, 3227, + 3228, 3229, 3230, 3231, 3232, 3233, 3234, 3235, 3236, 3237, 3238, 3240, 3241, + 3242, 3243, 3244, 3245, 3246, 3247, 3248, 3249, 3250, 3253, 3254, 3255, 3256, + 3257, 3258, 3259, 3260, 3261, 3262, 3263, 3264, 3265, 3266, 3267, 3268, 3269, + 3270, 3271, 3272, 3273, 3274, 3275, 3276, 3278, 3279, 3280, 3281, 3282, 3283, + 3284, 3285, 3286, 3287, 3288, 3289, 3290, 3291, 3292, 3293, 3294, 3295, 3296, + 3297, 3298, 3301, 3302, 3303, 3304, 3305, 3307, 3308, 3309, 3310, 3311, 3312, + 3313, 3315, 3317, 3318, 3319, 3320, 3321, 3322, 3323, 3324, 3325, 3326, 3327, + 3329, 3330, 3331, 3332, 3334, 3335, 3336, 3338, 3340, 3341, 3342, 3343, 3346, + 3348, 3349, 3351, 3352, 3353, 3354, 3355, 3356, 3360, 3361, 3362, 3363, 3364, + 3366, 3367, 3368, 3369, 3370, 3371, 3372, 3374, 3375, 3377, 3378, 3379, 3383, + 3384, 3385, 3386, 3387, 3388, 3389, 3390, 3391, 3394, 3395, 3396, 3397, 3398, + 3401, 3402, 3403, 3404, 3405, 3406, 3407, 3408, 3409, 3410, 3411, 3412, 3413, + 3414, 3415, 3416, 3417, 3418, 3419, 3420, 3421, 3422, 3423, 3424, 3425, 3426, + 3427, 3428, 3429, 3430, 3431, 3433, 3434, 3435, 3436, 3437, 3438, 3439, 3440, + 3441, 3443, 3444, 3445, 3446, 3447, 3448, 3449, 3450, 3451, 3452, 3453, 3454, + 3455, 3458, 3459, 3460, 3461, 3462, 3463, 3464, 3465, 3466, 3467, 3468, 3469, + 3470, 3471, 3472, 3473, 3474, 3475, 3476, 3477, 3480, 3481, 3483, 3485, 3488, + 3494, 3495, 3496, 3497, 3498, 3501, 3502, 3503, 3504, 3505, 3506, 3507, 3508, + 3509, 3511, 3512, 3515, 3516, 3517, 3518, 3520, 3521, 3522, 3523, 3524, 3525, + 3527, 3529, 3530, 3532, 3533, 3534, 3536, 3537, 3538, 3539, 3541, 3543, 3544, + 3545, 3546, 3547, 3548, 3549, 3550, 3551, 3552, 3555, 3556, 3557, 3558, 3559, + 3560, 3562, 3563, 3564, 3565, 3568, 3569, 3570, 3571, 3572, 3573, 3574, 3575, + 3576, 3577, 3578, 3579, 3580, 3581, 3582, 3583, 3584, 3588, 3589, 3590, 3591, + 3592, 3593, 3594, 3595, 3597, 3598, 3601, 3607, 3609, 3610, 3612, 3614, 3615, + 3616, 3625, 3627, 3630, 3635, 3636, 3637, 3638 +] + + +def get_previous_url(name, rev=None): + '''Return previous url''' + condition, document, history, found_rev = find_doc_for_rfcdiff(name, rev) + previous_url = '' + if condition in ('historic version', 'current version'): + doc = history if history else document + if found_rev: + doc.is_rfc = lambda: False + previous_url = doc.get_href() + elif condition == 'version dochistory not found': + document.rev = found_rev + document.is_rfc = lambda: False + previous_url = document.get_href() + return previous_url + + +def rfcdiff_latest_json(request, name, rev=None): + response = dict() + condition, document, history, found_rev = find_doc_for_rfcdiff(name, rev) + + if condition == 'no such document': + raise Http404 + elif condition in ('historic version', 'current version'): + doc = history if history else document + if not found_rev and doc.is_rfc(): + response['content_url'] = doc.get_href() + response['name']=doc.canonical_name() + if doc.name != doc.canonical_name(): + prev_rev = doc.rev + # not sure what to do if non-numeric values come back, so at least log it + log.assertion('doc.rfc_number().isdigit()') # .rfc_number() is expensive... + log.assertion('doc.rev.isdigit()') + if int(doc.rfc_number()) in HAS_TOMBSTONE and prev_rev != '00': + prev_rev = f'{(int(doc.rev)-1):02d}' + response['previous'] = f'{doc.name}-{prev_rev}' + response['previous_url'] = get_previous_url(doc.name, prev_rev) + else: + doc.is_rfc = lambda: False + response['content_url'] = doc.get_href() + response['rev'] = doc.rev + response['name'] = doc.name + if doc.rev == '00': + replaces_docs = (history.doc if condition=='historic version' else doc).related_that_doc('replaces') + if replaces_docs: + replaces = replaces_docs[0].document + response['previous'] = f'{replaces.name}-{replaces.rev}' + response['previous_url'] = get_previous_url(replaces.name, replaces.rev) + else: + match = re.search("-(rfc)?([0-9][0-9][0-9]+)bis(-.*)?$", name) + if match and match.group(2): + response['previous'] = f'rfc{match.group(2)}' + response['previous_url'] = get_previous_url(f'rfc{match.group(2)}') + else: + # not sure what to do if non-numeric values come back, so at least log it + log.assertion('doc.rev.isdigit()') + prev_rev = f'{(int(doc.rev)-1):02d}' + response['previous'] = f'{doc.name}-{prev_rev}' + response['previous_url'] = get_previous_url(doc.name, prev_rev) + elif condition == 'version dochistory not found': + response['warning'] = 'History for this version not found - these results are speculation' + response['name'] = document.name + response['rev'] = found_rev + document.rev = found_rev + document.is_rfc = lambda: False + response['content_url'] = document.get_href() + # not sure what to do if non-numeric values come back, so at least log it + log.assertion('found_rev.isdigit()') + if int(found_rev) > 0: + prev_rev = f'{(int(found_rev)-1):02d}' + response['previous'] = f'{document.name}-{prev_rev}' + response['previous_url'] = get_previous_url(document.name, prev_rev) + else: + match = re.search("-(rfc)?([0-9][0-9][0-9]+)bis(-.*)?$", name) + if match and match.group(2): + response['previous'] = f'rfc{match.group(2)}' + response['previous_url'] = get_previous_url(f'rfc{match.group(2)}') + if not response: + raise Http404 + return HttpResponse(json.dumps(response), content_type='application/json') diff --git a/ietf/doc/mails.py b/ietf/doc/mails.py index b04caa5b7..ddb2843cc 100644 --- a/ietf/doc/mails.py +++ b/ietf/doc/mails.py @@ -175,7 +175,7 @@ def generate_ballot_writeup(request, doc): e.doc = doc e.rev = doc.rev e.desc = "Ballot writeup was generated" - e.text = force_text(render_to_string("doc/mail/ballot_writeup.txt", {'iana': iana})) + e.text = force_text(render_to_string("doc/mail/ballot_writeup.txt", {'iana': iana, 'doc': doc })) # caller is responsible for saving, if necessary return e diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index a65be156c..7761337e3 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -56,7 +56,7 @@ from ietf.name.models import SessionStatusName, BallotPositionName, DocTypeName from ietf.person.models import Person from ietf.person.factories import PersonFactory, EmailFactory from ietf.utils.mail import outbox, empty_outbox -from ietf.utils.test_utils import login_testing_unauthorized, unicontent, reload_db_objects +from ietf.utils.test_utils import login_testing_unauthorized, unicontent from ietf.utils.test_utils import TestCase from ietf.utils.text import normalize_text from ietf.utils.timezone import date_today, datetime_today, DEADLINE_TZINFO, RPC_TZINFO @@ -2693,200 +2693,6 @@ class Idnits2SupportTests(TestCase): self.assertEqual(r.status_code, 200) self.assertContains(r,'Proposed') -class RfcdiffSupportTests(TestCase): - - def setUp(self): - super().setUp() - self.target_view = 'ietf.doc.views_doc.rfcdiff_latest_json' - self._last_rfc_num = 8000 - - def getJson(self, view_args): - url = urlreverse(self.target_view, kwargs=view_args) - r = self.client.get(url) - self.assertEqual(r.status_code, 200) - return r.json() - - def next_rfc_number(self): - self._last_rfc_num += 1 - return self._last_rfc_num - - def do_draft_test(self, name): - draft = IndividualDraftFactory(name=name, rev='00', create_revisions=range(0,13)) - draft = reload_db_objects(draft) - - received = self.getJson(dict(name=draft.name)) - self.assertEqual( - received, - dict( - name=draft.name, - rev=draft.rev, - content_url=draft.get_href(), - previous=f'{draft.name}-{(int(draft.rev)-1):02d}' - ), - 'Incorrect JSON when draft revision not specified', - ) - - received = self.getJson(dict(name=draft.name, rev=draft.rev)) - self.assertEqual( - received, - dict( - name=draft.name, - rev=draft.rev, - content_url=draft.get_href(), - previous=f'{draft.name}-{(int(draft.rev)-1):02d}' - ), - 'Incorrect JSON when latest revision specified', - ) - - received = self.getJson(dict(name=draft.name, rev='10')) - self.assertEqual( - received, - dict( - name=draft.name, - rev='10', - content_url=draft.history_set.get(rev='10').get_href(), - previous=f'{draft.name}-09' - ), - 'Incorrect JSON when historical revision specified', - ) - - received = self.getJson(dict(name=draft.name, rev='00')) - self.assertNotIn('previous', received, 'Rev 00 has no previous name when not replacing a draft') - - replaced = IndividualDraftFactory() - RelatedDocument.objects.create(relationship_id='replaces',source=draft,target=replaced.docalias.first()) - received = self.getJson(dict(name=draft.name, rev='00')) - self.assertEqual(received['previous'], f'{replaced.name}-{replaced.rev}', - 'Rev 00 has a previous name when replacing a draft') - - def test_draft(self): - # test with typical, straightforward names - self.do_draft_test(name='draft-somebody-did-a-thing') - # try with different potentially problematic names - self.do_draft_test(name='draft-someone-did-something-01-02') - self.do_draft_test(name='draft-someone-did-something-else-02') - self.do_draft_test(name='draft-someone-did-something-02-weird-01') - - def do_draft_with_broken_history_test(self, name): - draft = IndividualDraftFactory(name=name, rev='10') - received = self.getJson(dict(name=draft.name,rev='09')) - self.assertEqual(received['rev'],'09') - self.assertEqual(received['previous'], f'{draft.name}-08') - self.assertTrue('warning' in received) - - def test_draft_with_broken_history(self): - # test with typical, straightforward names - self.do_draft_with_broken_history_test(name='draft-somebody-did-something') - # try with different potentially problematic names - self.do_draft_with_broken_history_test(name='draft-someone-did-something-01-02') - self.do_draft_with_broken_history_test(name='draft-someone-did-something-else-02') - self.do_draft_with_broken_history_test(name='draft-someone-did-something-02-weird-03') - - def do_rfc_test(self, draft_name): - draft = WgDraftFactory(name=draft_name, create_revisions=range(0,2)) - draft.docalias.create(name=f'rfc{self.next_rfc_number():04}') - draft.set_state(State.objects.get(type_id='draft',slug='rfc')) - draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) - draft = reload_db_objects(draft) - rfc = draft - - number = rfc.rfc_number() - received = self.getJson(dict(name=number)) - self.assertEqual( - received, - dict( - content_url=rfc.get_href(), - name=rfc.canonical_name(), - previous=f'{draft.name}-{draft.rev}', - ), - 'Can look up an RFC by number', - ) - - num_received = received - received = self.getJson(dict(name=rfc.canonical_name())) - self.assertEqual(num_received, received, 'RFC by canonical name gives same result as by number') - - received = self.getJson(dict(name=f'RfC {number}')) - self.assertEqual(num_received, received, 'RFC with unusual spacing/caps gives same result as by number') - - received = self.getJson(dict(name=draft.name)) - self.assertEqual(num_received, received, 'RFC by draft name and no rev gives same result as by number') - - received = self.getJson(dict(name=draft.name, rev='01')) - self.assertEqual( - received, - dict( - content_url=draft.history_set.get(rev='01').get_href(), - name=draft.name, - rev='01', - previous=f'{draft.name}-00', - ), - 'RFC by draft name with rev should give draft name, not canonical name' - ) - - def test_rfc(self): - # simple draft name - self.do_rfc_test(draft_name='draft-test-ar-ef-see') - # tricky draft names - self.do_rfc_test(draft_name='draft-whatever-02') - self.do_rfc_test(draft_name='draft-test-me-03-04') - - def test_rfc_with_tombstone(self): - draft = WgDraftFactory(create_revisions=range(0,2)) - draft.docalias.create(name='rfc3261') # See views_doc.HAS_TOMBSTONE - draft.set_state(State.objects.get(type_id='draft',slug='rfc')) - draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) - draft = reload_db_objects(draft) - rfc = draft - - # Some old rfcs had tombstones that shouldn't be used for comparisons - received = self.getJson(dict(name=rfc.canonical_name())) - self.assertTrue(received['previous'].endswith('00')) - - def do_rfc_with_broken_history_test(self, draft_name): - draft = WgDraftFactory(rev='10', name=draft_name) - draft.docalias.create(name=f'rfc{self.next_rfc_number():04}') - draft.set_state(State.objects.get(type_id='draft',slug='rfc')) - draft.set_state(State.objects.get(type_id='draft-iesg', slug='pub')) - draft = reload_db_objects(draft) - rfc = draft - - received = self.getJson(dict(name=draft.name)) - self.assertEqual( - received, - dict( - content_url=rfc.get_href(), - name=rfc.canonical_name(), - previous=f'{draft.name}-10', - ), - 'RFC by draft name without rev should return canonical RFC name and no rev', - ) - - received = self.getJson(dict(name=draft.name, rev='10')) - self.assertEqual(received['name'], draft.name, 'RFC by draft name with rev should return draft name') - self.assertEqual(received['rev'], '10', 'Requested rev should be returned') - self.assertEqual(received['previous'], f'{draft.name}-09', 'Previous rev is one less than requested') - self.assertIn(f'{draft.name}-10', received['content_url'], 'Returned URL should include requested rev') - self.assertNotIn('warning', received, 'No warning when we have the rev requested') - - received = self.getJson(dict(name=f'{draft.name}-09')) - self.assertEqual(received['name'], draft.name, 'RFC by draft name with rev should return draft name') - self.assertEqual(received['rev'], '09', 'Requested rev should be returned') - self.assertEqual(received['previous'], f'{draft.name}-08', 'Previous rev is one less than requested') - self.assertIn(f'{draft.name}-09', received['content_url'], 'Returned URL should include requested rev') - self.assertEqual( - received['warning'], - 'History for this version not found - these results are speculation', - 'Warning should be issued when requested rev is not found' - ) - - def test_rfc_with_broken_history(self): - # simple draft name - self.do_rfc_with_broken_history_test(draft_name='draft-some-draft') - # tricky draft names - self.do_rfc_with_broken_history_test(draft_name='draft-gizmo-01') - self.do_rfc_with_broken_history_test(draft_name='draft-oh-boy-what-a-draft-02-03') - class RawIdTests(TestCase): diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py index dd43dc4bb..edfb89f38 100644 --- a/ietf/doc/urls.py +++ b/ietf/doc/urls.py @@ -85,11 +85,6 @@ urlpatterns = [ url(r'^html/(?P[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', views_doc.document_html), url(r'^idnits2-rfcs-obsoleted/?$', views_doc.idnits2_rfcs_obsoleted), url(r'^idnits2-rfc-status/?$', views_doc.idnits2_rfc_status), -# These two are proof-of-concept of a service that would redirect to the latest version -# url(r'^rfcdiff-latest/%(name)s(?:-%(rev)s)?(\.txt|\.html)?/?$' % settings.URL_REGEXPS, views_doc.rfcdiff_latest), -# url(r'^rfcdiff-latest/(?P[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', views_doc.rfcdiff_latest), - url(r'^rfcdiff-latest-json/%(name)s(?:-%(rev)s)?(\.txt|\.html)?/?$' % settings.URL_REGEXPS, views_doc.rfcdiff_latest_json), - url(r'^rfcdiff-latest-json/(?P[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', views_doc.rfcdiff_latest_json), url(r'^all/?$', views_search.index_all_drafts), url(r'^active/?$', views_search.index_active_drafts), @@ -180,4 +175,7 @@ urlpatterns = [ url(r'^%(name)s/session/' % settings.URL_REGEXPS, include('ietf.doc.urls_material')), url(r'^(?P[A-Za-z0-9._+-]+)/session/', include(session_patterns)), url(r'^(?P[A-Za-z0-9\._\+\-]+)$', views_search.search_for_name), + # latest versions - keep old URLs alive during migration period + url(r'^rfcdiff-latest-json/%(name)s(?:-%(rev)s)?(\.txt|\.html)?/?$' % settings.URL_REGEXPS, RedirectView.as_view(pattern_name='ietf.api.views.rfcdiff_latest_json', permanent=True)), + url(r'^rfcdiff-latest-json/(?P[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', RedirectView.as_view(pattern_name='ietf.api.views.rfcdiff_latest_json', permanent=True)), ] diff --git a/ietf/doc/utils.py b/ietf/doc/utils.py index d45d40158..14dfb9513 100644 --- a/ietf/doc/utils.py +++ b/ietf/doc/utils.py @@ -763,9 +763,11 @@ def rebuild_reference_relations(doc, filenames): errors = [] unfound = set() for ( ref, refType ) in refs.items(): - # As of Dec 2021, DocAlias has a unique constraint on the name field, so count > 1 should not occur - refdoc = DocAlias.objects.filter( name=ref ) + refdoc = DocAlias.objects.filter(name=ref) + if not refdoc and re.match(r"^draft-.*-\d{2}$", ref): + refdoc = DocAlias.objects.filter(name=ref[:-3]) count = refdoc.count() + # As of Dec 2021, DocAlias has a unique constraint on the name field, so count > 1 should not occur if count == 0: unfound.add( "%s" % ref ) continue @@ -1205,5 +1207,5 @@ def bibxml_for_draft(doc, rev=None): if name.startswith('rfc'): # bibxml3 does not speak of RFCs raise Http404() - return render_to_string('doc/bibxml.xml', {'name':name, 'doc':doc, 'doc_bibtype':'I-D'}) + return render_to_string('doc/bibxml.xml', {'name':name, 'doc':doc, 'doc_bibtype':'I-D', 'settings':settings}) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index d805532b8..8f136ac21 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -1943,136 +1943,3 @@ def idnits2_state(request, name, rev=None): else: doc.deststatus="Unknown" return render(request, 'doc/idnits2-state.txt', context={'doc':doc}, content_type='text/plain;charset=utf-8') - -def find_doc_for_rfcdiff(name, rev): - """rfcdiff lookup heuristics - - Returns a tuple with: - [0] - condition string - [1] - document found (or None) - [2] - historic version - [3] - revision actually found (may differ from :rev: input) - """ - found = fuzzy_find_documents(name, rev) - condition = 'no such document' - if found.documents.count() != 1: - return (condition, None, None, rev) - doc = found.documents.get() - if found.matched_rev is None or doc.rev == found.matched_rev: - condition = 'current version' - return (condition, doc, None, found.matched_rev) - else: - candidate = doc.history_set.filter(rev=found.matched_rev).order_by("-time").first() - if candidate: - condition = 'historic version' - return (condition, doc, candidate, found.matched_rev) - else: - condition = 'version dochistory not found' - return (condition, doc, None, found.matched_rev) - -# This is a proof of concept of a service that would redirect to the current revision -# def rfcdiff_latest(request, name, rev=None): -# condition, doc, history = find_doc_for_rfcdiff(name, rev) -# if not doc: -# raise Http404 -# if history: -# return redirect(history.get_href()) -# else: -# return redirect(doc.get_href()) - -HAS_TOMBSTONE = [ - 2821, 2822, 2873, 2919, 2961, 3023, 3029, 3031, 3032, 3033, 3034, 3035, 3036, - 3037, 3038, 3042, 3044, 3050, 3052, 3054, 3055, 3056, 3057, 3059, 3060, 3061, - 3062, 3063, 3064, 3067, 3068, 3069, 3070, 3071, 3072, 3073, 3074, 3075, 3076, - 3077, 3078, 3080, 3081, 3082, 3084, 3085, 3086, 3087, 3088, 3089, 3090, 3094, - 3095, 3096, 3097, 3098, 3101, 3102, 3103, 3104, 3105, 3106, 3107, 3108, 3109, - 3110, 3111, 3112, 3113, 3114, 3115, 3116, 3117, 3118, 3119, 3120, 3121, 3123, - 3124, 3126, 3127, 3128, 3130, 3131, 3132, 3133, 3134, 3135, 3136, 3137, 3138, - 3139, 3140, 3141, 3142, 3143, 3144, 3145, 3147, 3149, 3150, 3151, 3152, 3153, - 3154, 3155, 3156, 3157, 3158, 3159, 3160, 3161, 3162, 3163, 3164, 3165, 3166, - 3167, 3168, 3169, 3170, 3171, 3172, 3173, 3174, 3176, 3179, 3180, 3181, 3182, - 3183, 3184, 3185, 3186, 3187, 3188, 3189, 3190, 3191, 3192, 3193, 3194, 3197, - 3198, 3201, 3202, 3203, 3204, 3205, 3206, 3207, 3208, 3209, 3210, 3211, 3212, - 3213, 3214, 3215, 3216, 3217, 3218, 3220, 3221, 3222, 3224, 3225, 3226, 3227, - 3228, 3229, 3230, 3231, 3232, 3233, 3234, 3235, 3236, 3237, 3238, 3240, 3241, - 3242, 3243, 3244, 3245, 3246, 3247, 3248, 3249, 3250, 3253, 3254, 3255, 3256, - 3257, 3258, 3259, 3260, 3261, 3262, 3263, 3264, 3265, 3266, 3267, 3268, 3269, - 3270, 3271, 3272, 3273, 3274, 3275, 3276, 3278, 3279, 3280, 3281, 3282, 3283, - 3284, 3285, 3286, 3287, 3288, 3289, 3290, 3291, 3292, 3293, 3294, 3295, 3296, - 3297, 3298, 3301, 3302, 3303, 3304, 3305, 3307, 3308, 3309, 3310, 3311, 3312, - 3313, 3315, 3317, 3318, 3319, 3320, 3321, 3322, 3323, 3324, 3325, 3326, 3327, - 3329, 3330, 3331, 3332, 3334, 3335, 3336, 3338, 3340, 3341, 3342, 3343, 3346, - 3348, 3349, 3351, 3352, 3353, 3354, 3355, 3356, 3360, 3361, 3362, 3363, 3364, - 3366, 3367, 3368, 3369, 3370, 3371, 3372, 3374, 3375, 3377, 3378, 3379, 3383, - 3384, 3385, 3386, 3387, 3388, 3389, 3390, 3391, 3394, 3395, 3396, 3397, 3398, - 3401, 3402, 3403, 3404, 3405, 3406, 3407, 3408, 3409, 3410, 3411, 3412, 3413, - 3414, 3415, 3416, 3417, 3418, 3419, 3420, 3421, 3422, 3423, 3424, 3425, 3426, - 3427, 3428, 3429, 3430, 3431, 3433, 3434, 3435, 3436, 3437, 3438, 3439, 3440, - 3441, 3443, 3444, 3445, 3446, 3447, 3448, 3449, 3450, 3451, 3452, 3453, 3454, - 3455, 3458, 3459, 3460, 3461, 3462, 3463, 3464, 3465, 3466, 3467, 3468, 3469, - 3470, 3471, 3472, 3473, 3474, 3475, 3476, 3477, 3480, 3481, 3483, 3485, 3488, - 3494, 3495, 3496, 3497, 3498, 3501, 3502, 3503, 3504, 3505, 3506, 3507, 3508, - 3509, 3511, 3512, 3515, 3516, 3517, 3518, 3520, 3521, 3522, 3523, 3524, 3525, - 3527, 3529, 3530, 3532, 3533, 3534, 3536, 3537, 3538, 3539, 3541, 3543, 3544, - 3545, 3546, 3547, 3548, 3549, 3550, 3551, 3552, 3555, 3556, 3557, 3558, 3559, - 3560, 3562, 3563, 3564, 3565, 3568, 3569, 3570, 3571, 3572, 3573, 3574, 3575, - 3576, 3577, 3578, 3579, 3580, 3581, 3582, 3583, 3584, 3588, 3589, 3590, 3591, - 3592, 3593, 3594, 3595, 3597, 3598, 3601, 3607, 3609, 3610, 3612, 3614, 3615, - 3616, 3625, 3627, 3630, 3635, 3636, 3637, 3638 -] - -def rfcdiff_latest_json(request, name, rev=None): - response = dict() - condition, document, history, found_rev = find_doc_for_rfcdiff(name, rev) - - if condition == 'no such document': - raise Http404 - elif condition in ('historic version', 'current version'): - doc = history if history else document - if not found_rev and doc.is_rfc(): - response['content_url'] = doc.get_href() - response['name']=doc.canonical_name() - if doc.name != doc.canonical_name(): - prev_rev = doc.rev - # not sure what to do if non-numeric values come back, so at least log it - log.assertion('doc.rfc_number().isdigit()') # .rfc_number() is expensive... - log.assertion('doc.rev.isdigit()') - if int(doc.rfc_number()) in HAS_TOMBSTONE and prev_rev != '00': - prev_rev = f'{(int(doc.rev)-1):02d}' - response['previous'] = f'{doc.name}-{prev_rev}' - else: - doc.is_rfc = lambda: False - response['content_url'] = doc.get_href() - response['rev'] = doc.rev - response['name'] = doc.name - if doc.rev == '00': - replaces_docs = (history.doc if condition=='historic version' else doc).related_that_doc('replaces') - if replaces_docs: - replaces = replaces_docs[0].document - response['previous'] = f'{replaces.name}-{replaces.rev}' - else: - match = re.search("-(rfc)?([0-9][0-9][0-9]+)bis(-.*)?$", name) - if match and match.group(2): - response['previous'] = f'rfc{match.group(2)}' - else: - # not sure what to do if non-numeric values come back, so at least log it - log.assertion('doc.rev.isdigit()') - response['previous'] = f'{doc.name}-{(int(doc.rev)-1):02d}' - elif condition == 'version dochistory not found': - response['warning'] = 'History for this version not found - these results are speculation' - response['name'] = document.name - response['rev'] = found_rev - document.rev = found_rev - document.is_rfc = lambda: False - response['content_url'] = document.get_href() - # not sure what to do if non-numeric values come back, so at least log it - log.assertion('found_rev.isdigit()') - if int(found_rev) > 0: - response['previous'] = f'{document.name}-{(int(found_rev)-1):02d}' - else: - match = re.search("-(rfc)?([0-9][0-9][0-9]+)bis(-.*)?$", name) - if match and match.group(2): - response['previous'] = f'rfc{match.group(2)}' - if not response: - raise Http404 - return HttpResponse(json.dumps(response), content_type='application/json') diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index 92dcfe4aa..f78b3fff3 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -679,7 +679,27 @@ def docs_for_ad(request, name): 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] - # + + # filter out some results + results = [ + r + for r in results + if not ( + r.type_id == "charter" + and ( + r.group.state_id == "abandon" + or r.get_state_slug("charter") == "replaced" + ) + ) + and not ( + r.type_id == "draft" + and ( + r.get_state_slug("draft-iesg") == "dead" + or r.get_state_slug("draft") == "repl" + ) + ) + ] + for d in results: d.search_heading = ad_dashboard_group(d) # diff --git a/ietf/group/tests.py b/ietf/group/tests.py index 532f599a3..233cde55e 100644 --- a/ietf/group/tests.py +++ b/ietf/group/tests.py @@ -17,8 +17,8 @@ from django.utils import timezone import debug # pyflakes:ignore -from ietf.doc.factories import DocumentFactory, WgDraftFactory -from ietf.doc.models import DocEvent, RelatedDocument +from ietf.doc.factories import DocumentFactory, WgDraftFactory, EditorialDraftFactory +from ietf.doc.models import DocEvent, RelatedDocument, Document from ietf.group.models import Role, Group from ietf.group.utils import get_group_role_emails, get_child_group_role_emails, get_group_ad_emails from ietf.group.factories import GroupFactory, RoleFactory @@ -41,6 +41,11 @@ class StreamTests(TestCase): self.assertEqual(r.status_code, 200) self.assertContains(r, draft.name) + EditorialDraftFactory() # Quick way to ensure RSWG exists. + r = self.client.get(urlreverse("ietf.group.views.stream_documents", kwargs=dict(acronym="editorial"))) + self.assertRedirects(r, expected_url=urlreverse('ietf.group.views.group_documents',kwargs={"acronym":"rswg"})) + + def test_stream_edit(self): EmailFactory(address="ad2@ietf.org") @@ -58,6 +63,32 @@ class StreamTests(TestCase): self.assertTrue(Role.objects.filter(name="delegate", group__acronym=stream_acronym, email__address="ad2@ietf.org")) +class GroupStatsTests(TestCase): + def setUp(self): + super().setUp() + a = WgDraftFactory() + b = WgDraftFactory() + RelatedDocument.objects.create( + source=a, target=b.docalias.first(), relationship_id="refnorm" + ) + + def test_group_stats(self): + client = Client(Accept="application/json") + url = urlreverse("ietf.group.views.group_stats_data") + r = client.get(url) + self.assertTrue(r.status_code == 200, "Failed to receive group stats") + self.assertGreater(len(r.content), 0, "Group stats have no content") + + try: + data = json.loads(r.content) + except Exception as e: + self.fail("JSON load failed: %s" % e) + + ids = [d["id"] for d in data] + for doc in Document.objects.all(): + self.assertIn(doc.name, ids) + + class GroupDocDependencyTests(TestCase): def setUp(self): super().setUp() diff --git a/ietf/group/urls.py b/ietf/group/urls.py index 46bde4ede..713a0b7ee 100644 --- a/ietf/group/urls.py +++ b/ietf/group/urls.py @@ -53,6 +53,7 @@ info_detail_urls = [ group_urls = [ url(r'^$', views.active_groups), + url(r'^groupstats.json', views.group_stats_data, None, 'ietf.group.views.group_stats_data'), url(r'^groupmenu.json', views.group_menu_data, None, 'ietf.group.views.group_menu_data'), url(r'^chartering/$', views.chartering_groups), url(r'^chartering/create/(?P(wg|rg))/$', views.edit, {'action': "charter"}), diff --git a/ietf/group/views.py b/ietf/group/views.py index 16e7bb55e..d34645abb 100644 --- a/ietf/group/views.py +++ b/ietf/group/views.py @@ -820,6 +820,9 @@ def meetings(request, acronym=None, group_type=None): ).filter( current_status__in=['sched','schedw','appr','canceled'], ) + sessions = list(sessions) + for s in sessions: + s.order_number = s.order_in_meeting() future, in_progress, recent, past = group_sessions(sessions) @@ -1283,6 +1286,8 @@ def streams(request): return render(request, 'group/index.html', {'streams':streams}) def stream_documents(request, acronym): + if acronym == "editorial": + return HttpResponseRedirect(urlreverse(group_documents, kwargs=dict(acronym="rswg"))) streams = [ s.slug for s in StreamName.objects.all().exclude(slug__in=['ietf', 'legacy']) ] if not acronym in streams: raise Http404("No such stream: %s" % acronym) @@ -1354,6 +1359,85 @@ def group_menu_data(request): return JsonResponse(groups_by_parent) +@cache_control(public=True, max_age=30 * 60) +@cache_page(30 * 60) +def group_stats_data(request, years="3", only_active=True): + when = timezone.now() - datetime.timedelta(days=int(years) * 365) + docs = ( + Document.objects.filter(type="draft", stream="ietf") + .filter( + Q(docevent__newrevisiondocevent__time__gte=when) + | Q(docevent__type="published_rfc", docevent__time__gte=when) + ) + .exclude(states__type="draft", states__slug="repl") + .distinct() + ) + + data = [] + for a in Group.objects.filter(type="area"): + if only_active and not a.is_active: + continue + + area_docs = docs.filter(group__parent=a).exclude(group__acronym="none") + if not area_docs: + continue + + area_page_cnt = 0 + area_doc_cnt = 0 + for wg in Group.objects.filter(type="wg", parent=a): + if only_active and not wg.is_active: + continue + + wg_docs = area_docs.filter(group=wg) + if not wg_docs: + continue + + wg_page_cnt = 0 + for doc in wg_docs: + # add doc data + data.append( + { + "id": doc.name, + "active": True, + "parent": wg.acronym, + "grandparent": a.acronym, + "pages": doc.pages, + "docs": 1, + } + ) + wg_page_cnt += doc.pages + + area_doc_cnt += len(wg_docs) + area_docs = area_docs.exclude(group=wg) + + # add WG data + data.append( + { + "id": wg.acronym, + "active": wg.is_active, + "parent": a.acronym, + "grandparent": "ietf", + "pages": wg_page_cnt, + "docs": len(wg_docs), + } + ) + area_page_cnt += wg_page_cnt + + # add area data + data.append( + { + "id": a.acronym, + "active": a.is_active, + "parent": "ietf", + "pages": area_page_cnt, + "docs": area_doc_cnt, + } + ) + + data.append({"id": "ietf", "active": True}) + return JsonResponse(data, safe=False) + + # --- Review views ----------------------------------------------------- def get_open_review_requests_for_team(team, assignment_status=None): diff --git a/ietf/meeting/helpers.py b/ietf/meeting/helpers.py index 256d10fb7..7286f7453 100644 --- a/ietf/meeting/helpers.py +++ b/ietf/meeting/helpers.py @@ -124,7 +124,7 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe # check before blindly assigning to meeting just in case. if a.session.meeting.pk == meeting.pk: a.session.meeting = meeting - a.session.order_number = None + a.session.order_number = a.session.order_in_meeting() if a.session.group else None if a.session.group and a.session.group not in groups: groups.append(a.session.group) @@ -134,12 +134,6 @@ def preprocess_assignments_for_agenda(assignments_queryset, meeting, extra_prefe if a.session and a.session.group: sessions_for_groups[(a.session.group, a.session.type_id)].append(a) - for a in assignments: - if a.session and a.session.group: - - l = sessions_for_groups.get((a.session.group, a.session.type_id), []) - a.session.order_number = l.index(a) + 1 if a in l else 0 - timeslot_by_session_pk = {a.session_id: a.timeslot for a in assignments} for a in assignments: diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py index b557df253..7173e3008 100644 --- a/ietf/meeting/tests_views.py +++ b/ietf/meeting/tests_views.py @@ -35,7 +35,7 @@ from django.utils.text import slugify import debug # pyflakes:ignore -from ietf.doc.models import Document +from ietf.doc.models import Document, NewRevisionDocEvent from ietf.group.models import Group, Role, GroupFeatures from ietf.group.utils import can_manage_group from ietf.person.models import Person @@ -213,7 +213,11 @@ class MeetingTests(BaseMeetingTestCase): def test_meeting_agenda(self): meeting = make_meeting_test_data() session = Session.objects.filter(meeting=meeting, group__acronym="mars").first() + session.remote_instructions='https://remote.example.com' + session.save() slot = TimeSlot.objects.get(sessionassignments__session=session,sessionassignments__schedule=meeting.schedule) + slot.location.urlresource_set.create(name_id='meetecho_onsite', url='https://onsite.example.com') + slot.location.urlresource_set.create(name_id='meetecho', url='https://meetecho.example.com') # self.write_materials_files(meeting, session) # @@ -316,7 +320,10 @@ class MeetingTests(BaseMeetingTestCase): assert_ical_response_is_valid(self, r) self.assertContains(r, session.group.acronym) self.assertContains(r, session.group.name) + self.assertContains(r, session.remote_instructions) self.assertContains(r, slot.location.name) + self.assertContains(r, 'https://onsite.example.com') + self.assertContains(r, 'https://meetecho.example.com') self.assertContains(r, "BEGIN:VTIMEZONE") self.assertContains(r, "END:VTIMEZONE") @@ -600,13 +607,73 @@ class MeetingTests(BaseMeetingTestCase): @override_settings(MEETING_MATERIALS_SERVE_LOCALLY=True) def test_materials_name_endswith_hyphen_number_number(self): - sp = SessionPresentationFactory(document__name='slides-junk-15',document__type_id='slides',document__states=[('reuse_policy','single')]) - sp.document.uploaded_filename = '%s-%s.pdf'%(sp.document.name,sp.document.rev) + # be sure a shadowed filename without the hyphen does not interfere + shadow = SessionPresentationFactory( + document__name="slides-115-junk", + document__type_id="slides", + document__states=[("reuse_policy", "single")], + ) + shadow.document.uploaded_filename = ( + f"{shadow.document.name}-{shadow.document.rev}.pdf" + ) + shadow.document.save() + # create the material we want to find for the test + sp = SessionPresentationFactory( + document__name="slides-115-junk-15", + document__type_id="slides", + document__states=[("reuse_policy", "single")], + ) + sp.document.uploaded_filename = f"{sp.document.name}-{sp.document.rev}.pdf" sp.document.save() - self.write_materials_file(sp.session.meeting, sp.document, 'Fake slide contents') - url = urlreverse("ietf.meeting.views.materials_document", kwargs=dict(document=sp.document.name,num=sp.session.meeting.number)) + self.write_materials_file( + sp.session.meeting, sp.document, "Fake slide contents rev 00" + ) + + # create rev 01 + sp.document.rev = "01" + sp.document.uploaded_filename = f"{sp.document.name}-{sp.document.rev}.pdf" + sp.document.save_with_history( + [ + NewRevisionDocEvent.objects.create( + type="new_revision", + doc=sp.document, + rev=sp.document.rev, + by=Person.objects.get(name="(System)"), + desc=f"New version available: {sp.document.name}-{sp.document.rev}.txt", + ) + ] + ) + self.write_materials_file( + sp.session.meeting, sp.document, "Fake slide contents rev 01" + ) + url = urlreverse( + "ietf.meeting.views.materials_document", + kwargs=dict(document=sp.document.name, num=sp.session.meeting.number), + ) r = self.client.get(url) - self.assertEqual(r.status_code, 200) + self.assertContains( + r, + "Fake slide contents rev 01", + status_code=200, + msg_prefix="Should return latest rev by default", + ) + url = urlreverse( + "ietf.meeting.views.materials_document", + kwargs=dict(document=sp.document.name + "-00", num=sp.session.meeting.number), + ) + r = self.client.get(url) + self.assertContains( + r, + "Fake slide contents rev 00", + status_code=200, + msg_prefix="Should return existing version on request", + ) + url = urlreverse( + "ietf.meeting.views.materials_document", + kwargs=dict(document=sp.document.name + "-02", num=sp.session.meeting.number), + ) + r = self.client.get(url) + self.assertEqual(r.status_code, 404, "Should not find nonexistent version") def test_important_dates(self): meeting=MeetingFactory(type_id='ietf') @@ -2156,24 +2223,30 @@ class EditTimeslotsTests(TestCase): ) self.login() + url = self.edit_timeslot_url(ts) + + # check that sched parameter is preserved + r = self.client.get(url) + self.assertNotContains(r, '?sched=', status_code=200) + r = self.client.get(url + '?sched=1234') + self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail + name_after = 'New Name (tm)' type_after = 'plenary' time_after = (time_utc + datetime.timedelta(days=1, hours=2)).astimezone(meeting.tz()) duration_after = duration_before * 2 show_location_after = False location_after = meeting.room_set.last() - r = self.client.post( - self.edit_timeslot_url(ts), - data=dict( - name=name_after, - type=type_after, - time_0=time_after.strftime('%Y-%m-%d'), # date for SplitDateTimeField - time_1=time_after.strftime('%H:%M'), # time for SplitDateTimeField - duration=str(duration_after), - # show_location=show_location_after, # False values are omitted from form - location=location_after.pk, - ) + post_data = dict( + name=name_after, + type=type_after, + time_0=time_after.strftime('%Y-%m-%d'), # date for SplitDateTimeField + time_1=time_after.strftime('%H:%M'), # time for SplitDateTimeField + duration=str(duration_after), + # show_location=show_location_after, # False values are omitted from form + location=location_after.pk, ) + r = self.client.post(url, data=post_data) self.assertEqual(r.status_code, 302) # expect redirect to timeslot edit url self.assertEqual(r['Location'], self.edit_timeslots_url(meeting), 'Expected to be redirected to meeting timeslots edit page') @@ -2194,6 +2267,12 @@ class EditTimeslotsTests(TestCase): self.assertEqual(ts.show_location, show_location_after) self.assertEqual(ts.location, location_after) + # and check with sched param set + r = self.client.post(url + '?sched=1234', data=post_data) + self.assertEqual(r.status_code, 302) # expect redirect to timeslot edit url + self.assertEqual(r['Location'], self.edit_timeslots_url(meeting) + '?sched=1234', + 'Expected to be redirected to meeting timeslots edit page with sched param set') + def test_invalid_edit_timeslot(self): meeting = self.create_bare_meeting() ts: TimeSlot = TimeSlotFactory(meeting=meeting, name='slot') # n.b., colon indicates type hinting @@ -2316,6 +2395,7 @@ class EditTimeslotsTests(TestCase): meeting = self.create_meeting() timeslots_before = set(ts.pk for ts in meeting.timeslot_set.all()) + url = self.create_timeslots_url(meeting) post_data = dict( name='some name', type='regular', @@ -2326,10 +2406,14 @@ class EditTimeslotsTests(TestCase): locations=str(meeting.room_set.first().pk), ) self.login() - r = self.client.post( - self.create_timeslots_url(meeting), - data=post_data, - ) + + # check that sched parameter is preserved + r = self.client.get(url) + self.assertNotContains(r, '?sched=', status_code=200) + r = self.client.get(url + '?sched=1234') + self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail + + r = self.client.post(url, data=post_data) self.assertEqual(r.status_code, 302) self.assertEqual(r['Location'], self.edit_timeslots_url(meeting), 'Expected to be redirected to meeting timeslots edit page') @@ -2344,6 +2428,12 @@ class EditTimeslotsTests(TestCase): self.assertEqual(ts.show_location, post_data['show_location']) self.assertEqual(str(ts.location.pk), post_data['locations']) + # check again with sched parameter + r = self.client.post(url + '?sched=1234', data=post_data) + self.assertEqual(r.status_code, 302) + self.assertEqual(r['Location'], self.edit_timeslots_url(meeting) + '?sched=1234', + 'Expected to be redirected to meeting timeslots edit page with sched parameter set') + def test_create_single_timeslot_outside_meeting_days(self): """Creating a single timeslot outside the official meeting days should work""" meeting = self.create_meeting() @@ -2627,6 +2717,17 @@ class EditTimeslotsTests(TestCase): day_locs.discard((ts.time.date(), ts.location)) self.assertEqual(day_locs, set(), 'Not all day/location combinations created') + def test_sched_param_preserved(self): + meeting = MeetingFactory(type_id='ietf') + url = urlreverse('ietf.meeting.views.edit_timeslots', kwargs={'num': meeting.number}) + self.client.login(username='secretary', password='secretary+password') + r = self.client.get(url) + self.assertNotContains(r, '?sched=', status_code=200) + self.assertNotContains(r, "Back to agenda") + r = self.client.get(url + '?sched=1234') + self.assertContains(r, '?sched=1234', status_code=200) # could check in more detail + self.assertContains(r, "Back to agenda") + def test_ajax_delete_timeslot(self): """AJAX call to delete timeslot should work""" meeting = self.create_bare_meeting() @@ -3202,10 +3303,11 @@ class EditTests(TestCase): e = q("#session{}".format(s.pk)) # should be link to edit/cancel session + edit_session_url = urlreverse( + 'ietf.meeting.views.edit_session', kwargs={'session_id': s.pk} + ) + f'?sched={meeting.schedule.pk}' self.assertTrue( - e.find('a[href="{}"]'.format( - urlreverse('ietf.meeting.views.edit_session', kwargs={'session_id': s.pk}), - )) + e.find(f'a[href="{edit_session_url}"]') ) self.assertTrue( e.find('a[href="{}?sched={}"]'.format( @@ -3773,11 +3875,15 @@ class EditTests(TestCase): def test_edit_session(self): session = SessionFactory(meeting__type_id='ietf', group__type_id='team') # type determines allowed session purposes + edit_meeting_url = urlreverse('ietf.meeting.views.edit_meeting_schedule', kwargs={'num': session.meeting.number}) self.client.login(username='secretary', password='secretary+password') url = urlreverse('ietf.meeting.views.edit_session', kwargs={'session_id': session.pk}) r = self.client.get(url) self.assertContains(r, 'Edit session', status_code=200) - r = self.client.post(url, { + pq = PyQuery(r.content) + back_button = pq(f'a[href="{edit_meeting_url}"]') + self.assertEqual(len(back_button), 1) + post_data = { 'name': 'this is a name', 'short': 'tian', 'purpose': 'coding', @@ -3787,10 +3893,10 @@ class EditTests(TestCase): 'remote_instructions': 'Do this do that', 'attendees': '103', 'comments': 'So much to say', - }) + } + r = self.client.post(url, post_data) self.assertNoFormPostErrors(r) - self.assertRedirects(r, urlreverse('ietf.meeting.views.edit_meeting_schedule', - kwargs={'num': session.meeting.number})) + self.assertRedirects(r, edit_meeting_url) session = Session.objects.get(pk=session.pk) # refresh objects from DB self.assertEqual(session.name, 'this is a name') self.assertEqual(session.short, 'tian') @@ -3802,6 +3908,23 @@ class EditTests(TestCase): self.assertEqual(session.attendees, 103) self.assertEqual(session.comments, 'So much to say') + # Verify return to correct schedule when sched query parameter is present + other_schedule = ScheduleFactory(meeting=session.meeting) + r = self.client.get(url + f'?sched={other_schedule.pk}') + edit_meeting_url = urlreverse( + 'ietf.meeting.views.edit_meeting_schedule', + kwargs={ + 'num': session.meeting.number, + 'owner': other_schedule.owner.email(), + 'name': other_schedule.name, + }, + ) + pq = PyQuery(r.content) + back_button = pq(f'a[href="{edit_meeting_url}"]') + self.assertEqual(len(back_button), 1) + r = self.client.post(url + f'?sched={other_schedule.pk}', post_data) + self.assertRedirects(r, edit_meeting_url) + def test_cancel_session(self): # session for testing with official schedule session = SessionFactory(meeting__type_id='ietf') @@ -4531,18 +4654,34 @@ class InterimTests(TestCase): meeting = make_meeting_test_data(create_interims=True) populate_important_dates(meeting) url = urlreverse("ietf.meeting.views.upcoming_ical") - - r = self.client.get(url) - self.assertEqual(r.status_code, 200) # Expect events 3 sessions - one for each WG and one for the IETF meeting + expected_event_summaries = [ + 'ames - Asteroid Mining Equipment Standardization Group', + 'mars - Martian Special Interest Group', + 'IETF 72', + ] + + Session.objects.filter( + meeting__type_id='interim', + group__acronym="mars", + ).update( + remote_instructions='https://someurl.example.com', + ) + r = self.client.get(url) + self.assertEqual(r.status_code, 200) assert_ical_response_is_valid(self, r, - expected_event_summaries=[ - 'ames - Asteroid Mining Equipment Standardization Group', - 'mars - Martian Special Interest Group', - 'IETF 72', - ], - expected_event_count=3) + expected_event_summaries=expected_event_summaries, + expected_event_count=len(expected_event_summaries)) + self.assertContains(r, 'Remote instructions: https://someurl.example.com') + + Session.objects.filter(meeting__type_id='interim').update(remote_instructions='') + r = self.client.get(url) + self.assertEqual(r.status_code, 200) + assert_ical_response_is_valid(self, r, + expected_event_summaries=expected_event_summaries, + expected_event_count=len(expected_event_summaries)) + self.assertNotContains(r, 'Remote instructions:') def test_upcoming_ical_filter(self): # Just a quick check of functionality - details tested by test_js.InterimTests @@ -5592,6 +5731,7 @@ class InterimTests(TestCase): make_interim_test_data() meeting = Meeting.objects.filter(type='interim', session__group__acronym='mars').first() s1 = Session.objects.filter(meeting=meeting, group__acronym="mars").first() + self.assertGreater(len(s1.remote_instructions), 0, 'Expected remote_instructions to be set') a1 = s1.official_timeslotassignment() t1 = a1.timeslot # Create an extra session @@ -5610,6 +5750,7 @@ class InterimTests(TestCase): self.assertEqual(r.content.count(b'UID'), 2) self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group') self.assertContains(r, t1.local_start_time().strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, s1.remote_instructions) self.assertContains(r, t2.local_start_time().strftime('%Y%m%dT%H%M%S')) self.assertContains(r, 'END:VEVENT') # @@ -5620,6 +5761,7 @@ class InterimTests(TestCase): self.assertEqual(r.content.count(b'UID'), 1) self.assertContains(r, 'SUMMARY:mars - Martian Special Interest Group') self.assertContains(r, t1.time.strftime('%Y%m%dT%H%M%S')) + self.assertContains(r, s1.remote_instructions) self.assertNotContains(r, t2.time.strftime('%Y%m%dT%H%M%S')) self.assertContains(r, 'END:VEVENT') diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 49d6c0af5..44193ff9a 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -17,7 +17,7 @@ import tempfile from calendar import timegm from collections import OrderedDict, Counter, deque, defaultdict, namedtuple -from urllib.parse import unquote +from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit from tempfile import mkstemp from wsgiref.handlers import format_date_time @@ -202,32 +202,37 @@ def current_materials(request): else: raise Http404('No such meeting') + +def _get_materials_doc(meeting, name): + """Get meeting materials document named by name + + Raises Document.DoesNotExist if a match cannot be found. + """ + # try an exact match first + doc = Document.objects.filter(name=name).first() + if doc is not None and doc.get_related_meeting() == meeting: + return doc, None + # try parsing a rev number + if "-" in name: + docname, rev = name.rsplit("-", 1) + if len(rev) == 2 and rev.isdigit(): + doc = Document.objects.get(name=docname) # may raise Document.DoesNotExist + if doc.get_related_meeting() == meeting and rev in doc.revisions(): + return doc, rev + # give up + raise Document.DoesNotExist + + @cache_page(1 * 60) def materials_document(request, document, num=None, ext=None): meeting=get_meeting(num,type_in=['ietf','interim']) num = meeting.number - if (re.search(r'^\w+-\d+-.+-\d\d$', document) or - re.search(r'^\w+-interim-\d+-.+-\d\d-\d\d$', document) or - re.search(r'^\w+-interim-\d+-.+-sess[a-z]-\d\d$', document) or - re.search(r'^(minutes|slides|chatlog|polls)-interim-\d+-.+-\d\d$', document)): - name, rev = document.rsplit('-', 1) - else: - name, rev = document, None # This view does not allow the use of DocAliases. Right now we are probably only creating one (identity) alias, but that may not hold in the future. - doc = Document.objects.filter(name=name).first() - # Handle edge case where the above name, rev splitter misidentifies the end of a document name as a revision number - if not doc: - if rev: - name = name + '-' + rev - rev = None - doc = get_object_or_404(Document, name=name) - else: - raise Http404("No such document") - - if not doc.meeting_related(): - raise Http404("Not a meeting related document") - if doc.get_related_meeting() != meeting: + try: + doc, rev = _get_materials_doc(meeting=meeting, name=document) + except Document.DoesNotExist: raise Http404("No such document for meeting %s" % num) + if not rev: filename = doc.get_file_name() else: @@ -281,8 +286,13 @@ def materials_editable_groups(request, num=None): @role_required('Secretariat') def edit_timeslots(request, num=None): - meeting = get_meeting(num) + if 'sched' in request.GET: + schedule = Schedule.objects.filter(pk=request.GET.get('sched', None)).first() + schedule_edit_url = _schedule_edit_url(meeting, schedule) + else: + schedule_edit_url = None + with timezone.override(meeting.tz()): if request.method == 'POST': # handle AJAX requests @@ -333,6 +343,7 @@ def edit_timeslots(request, num=None): "slot_slices": slots, "date_slices":date_slices, "meeting":meeting, + "schedule_edit_url": schedule_edit_url, "ts_list":ts_list, "ts_with_official_assignments": ts_with_official_assignments, "ts_with_any_assignments": ts_with_any_assignments, @@ -659,8 +670,8 @@ def edit_meeting_schedule(request, num=None, owner=None, name=None): sorted_rooms = sorted( rooms_with_timeslots, key=lambda room: ( - # Sort higher capacity rooms first. - -room.capacity if room.capacity is not None else 1, # sort rooms with capacity = None at end + # Sort lower capacity rooms first. + room.capacity if room.capacity is not None else math.inf, # sort rooms with capacity = None at end # Sort regular session rooms ahead of others - these will usually # have more timeslots than other room types. 0 if room_data[room.pk]['timeslot_count'] == max_timeslots else 1, @@ -3425,7 +3436,6 @@ def interim_request_edit(request, number): "form": form, "formset": formset}) -@cache_page(60*60) def past(request): '''List of past meetings''' today = timezone.now() @@ -3793,7 +3803,6 @@ def proceedings_overview(request, num=None): 'template': template, }) -@cache_page( 60 * 60 ) def proceedings_progress_report(request, num=None): '''Display Progress Report (stats since last meeting)''' if not (num and num.isdigit()): @@ -4123,7 +4132,15 @@ def edit_timeslot(request, num, slot_id): form = TimeSlotEditForm(instance=timeslot, data=request.POST) if form.is_valid(): form.save() - return HttpResponseRedirect(reverse('ietf.meeting.views.edit_timeslots', kwargs={'num': num})) + redirect_to = reverse('ietf.meeting.views.edit_timeslots', kwargs={'num': num}) + if 'sched' in request.GET: + # Preserve 'sched' as a query parameter + urlparts = list(urlsplit(redirect_to)) + query = parse_qs(urlparts[3]) + query['sched'] = request.GET['sched'] + urlparts[3] = urlencode(query) + redirect_to = urlunsplit(urlparts) + return HttpResponseRedirect(redirect_to) else: form = TimeSlotEditForm(instance=timeslot) @@ -4156,7 +4173,15 @@ def create_timeslot(request, num): show_location=form.cleaned_data['show_location'], ) ) - return HttpResponseRedirect(reverse('ietf.meeting.views.edit_timeslots',kwargs={'num':num})) + redirect_to = reverse('ietf.meeting.views.edit_timeslots',kwargs={'num':num}) + if 'sched' in request.GET: + # Preserve 'sched' as a query parameter + urlparts = list(urlsplit(redirect_to)) + query = parse_qs(urlparts[3]) + query['sched'] = request.GET['sched'] + urlparts[3] = urlencode(query) + redirect_to = urlunsplit(urlparts) + return HttpResponseRedirect(redirect_to) else: form = TimeSlotCreateForm(meeting) @@ -4171,19 +4196,19 @@ def create_timeslot(request, num): @role_required('Secretariat') def edit_session(request, session_id): session = get_object_or_404(Session, pk=session_id) + schedule = Schedule.objects.filter(pk=request.GET.get('sched', None)).first() + editor_url = _schedule_edit_url(session.meeting, schedule) if request.method == 'POST': form = SessionEditForm(instance=session, data=request.POST) if form.is_valid(): form.save() - return HttpResponseRedirect( - reverse('ietf.meeting.views.edit_meeting_schedule', - kwargs={'num': form.instance.meeting.number})) + return HttpResponseRedirect(editor_url) else: form = SessionEditForm(instance=session) return render( request, 'meeting/edit_session.html', - {'session': session, 'form': form}, + {'session': session, 'form': form, 'editor_url': editor_url}, ) def _schedule_edit_url(meeting, schedule): diff --git a/ietf/nomcom/tests.py b/ietf/nomcom/tests.py index d7b162fbc..22c67be43 100644 --- a/ietf/nomcom/tests.py +++ b/ietf/nomcom/tests.py @@ -1973,7 +1973,7 @@ class NoPublicKeyTests(TestCase): response = self.client.get(url) self.assertEqual(response.status_code,200) q=PyQuery(response.content) - text_bits = [x.xpath('./text()') for x in q('.alert-warning')] + text_bits = [x.xpath('.//text()') for x in q('.alert-warning')] flat_text_bits = [item for sublist in text_bits for item in sublist] self.assertTrue(any(['not yet' in y for y in flat_text_bits])) self.assertEqual(bool(q('form:not(.navbar-form)')),expected_form) diff --git a/ietf/person/models.py b/ietf/person/models.py index 9ca641045..a09656b81 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -188,12 +188,31 @@ class Person(models.Model): def active_drafts(self): from ietf.doc.models import Document - return Document.objects.filter(documentauthor__person=self, type='draft', states__slug='active').distinct().order_by('-time') + + return ( + Document.objects.filter( + documentauthor__person=self, + type="draft", + states__type="draft", + states__slug="active", + ) + .distinct() + .order_by("-time") + ) def expired_drafts(self): from ietf.doc.models import Document - return Document.objects.filter(documentauthor__person=self, type='draft', states__slug__in=['repl', 'expired', 'auth-rm', 'ietf-rm']).distinct().order_by('-time') + return ( + Document.objects.filter( + documentauthor__person=self, + type="draft", + states__type="draft", + states__slug__in=["repl", "expired", "auth-rm", "ietf-rm"], + ) + .distinct() + .order_by("-time") + ) def save(self, *args, **kwargs): created = not self.pk diff --git a/ietf/secr/sreq/forms.py b/ietf/secr/sreq/forms.py index 840b23549..0c112a063 100644 --- a/ietf/secr/sreq/forms.py +++ b/ietf/secr/sreq/forms.py @@ -3,6 +3,7 @@ from django import forms +from django.template.defaultfilters import pluralize import debug # pyflakes:ignore @@ -235,6 +236,25 @@ class SessionForm(forms.Form): def clean_comments(self): return clean_text_field(self.cleaned_data['comments']) + def clean_bethere(self): + bethere = self.cleaned_data["bethere"] + if bethere: + extra = set( + Person.objects.filter( + role__group=self.group, role__name__in=["chair", "ad"] + ) + & bethere + ) + if extra: + extras = ", ".join(e.name for e in extra) + raise forms.ValidationError( + ( + f"Please remove the following person{pluralize(len(extra))}, the system " + f"tracks their availability due to their role{pluralize(len(extra))}: {extras}." + ) + ) + return bethere + def clean_send_notifications(self): return True if not self.notifications_optional else self.cleaned_data['send_notifications'] diff --git a/ietf/secr/templates/includes/session_info.txt b/ietf/secr/templates/includes/session_info.txt index eea4a5f17..bffc13e3e 100644 --- a/ietf/secr/templates/includes/session_info.txt +++ b/ietf/secr/templates/includes/session_info.txt @@ -14,7 +14,7 @@ Conflicts to Avoid: {% if session.adjacent_with_wg %} Adjacent with WG: {{ session.adjacent_with_wg }}{% endif %} {% if session.timeranges_display %} Can't meet: {{ session.timeranges_display|join:", " }}{% endif %} -People who must be present: +Participants who must be present: {% for person in session.bethere %} {{ person.ascii_name }} {% endfor %} Resources Requested: diff --git a/ietf/secr/templates/includes/sessions_request_form.html b/ietf/secr/templates/includes/sessions_request_form.html index f05bee3db..7a8c90855 100755 --- a/ietf/secr/templates/includes/sessions_request_form.html +++ b/ietf/secr/templates/includes/sessions_request_form.html @@ -28,12 +28,12 @@ {% endif %} Number of Attendees:{% if not is_virtual %}*{% endif %}{{ form.attendees.errors }}{{ form.attendees }} - People who must be present: + Participants who must be present: {{ form.bethere.errors }} {{ form.bethere }}

- You should not include the Area Directors; they will be added automatically. + Do not include Area Directors and WG Chairs; the system already tracks their availability.

diff --git a/ietf/secr/templates/includes/sessions_request_view.html b/ietf/secr/templates/includes/sessions_request_view.html index 596338c47..bc6aef061 100644 --- a/ietf/secr/templates/includes/sessions_request_view.html +++ b/ietf/secr/templates/includes/sessions_request_view.html @@ -35,7 +35,7 @@ {% endif %} - People who must be present: + Participants who must be present: {% if session.bethere %}
    {% for person in session.bethere %}
  • {{ person }}
  • {% endfor %}
{% else %}None{% endif %} @@ -70,4 +70,4 @@ {% endif %} - + \ No newline at end of file diff --git a/ietf/static/css/document_html.scss b/ietf/static/css/document_html.scss index 8b3969cf5..ff693259f 100644 --- a/ietf/static/css/document_html.scss +++ b/ietf/static/css/document_html.scss @@ -108,28 +108,15 @@ $tooltip-margin: inherit !default; } @media screen { - @include media-breakpoint-only(xs) { - font-size: min(7pt, var(--doc-ptsize-max)); - } - @include media-breakpoint-up(sm) { - font-size: min(9.5pt, var(--doc-ptsize-max)); + // the viewport-width ("vw") constants are magic; they seem to work for + // many monospace fonts, but may need tweaking + @include media-breakpoint-up(xs) { + font-size: min(2.2vw, var(--doc-ptsize-max)); } @include media-breakpoint-up(md) { - font-size: min(9.5pt, var(--doc-ptsize-max)); - } - - @include media-breakpoint-up(lg) { - font-size: min(11pt, var(--doc-ptsize-max)); - } - - @include media-breakpoint-up(xl) { - font-size: min(13pt, var(--doc-ptsize-max)); - } - - @include media-breakpoint-up(xxl) { - font-size: min(16pt, var(--doc-ptsize-max)); + font-size: min(1.6vw, var(--doc-ptsize-max)); } .grey, @@ -175,29 +162,15 @@ $tooltip-margin: inherit !default; } @media screen { - @include media-breakpoint-only(xs) { - font-size: min(6.75pt, var(--doc-ptsize-max)); - } - @include media-breakpoint-up(sm) { - font-size: min(8.75pt, var(--doc-ptsize-max)); + // the viewport-width ("vw") constants are magic; they seem to work for + // many monospace fonts, but may need tweaking + @include media-breakpoint-up(xs) { + font-size: min(2vw, var(--doc-ptsize-max)); } @include media-breakpoint-up(md) { - font-size: min(8.75pt, var(--doc-ptsize-max)); - } - - // Rest of font sizes is the same as above. - @include media-breakpoint-up(lg) { - font-size: min(11pt, var(--doc-ptsize-max)); - } - - @include media-breakpoint-up(xl) { - font-size: min(13pt, var(--doc-ptsize-max)); - } - - @include media-breakpoint-up(xxl) { - font-size: min(16pt, var(--doc-ptsize-max)); + font-size: min(1.5vw, var(--doc-ptsize-max)); } } diff --git a/ietf/static/css/ietf.scss b/ietf/static/css/ietf.scss index 3dab7880c..afc53ef49 100644 --- a/ietf/static/css/ietf.scss +++ b/ietf/static/css/ietf.scss @@ -97,6 +97,11 @@ pre { width: 60px; } +// Style preformatted alert messages better. +.preformatted { + white-space: pre-line; +} + .leftmenu { width: 13em; diff --git a/ietf/static/js/fullcalendar.js b/ietf/static/js/fullcalendar.js index 0d58a24e7..276a31caa 100644 --- a/ietf/static/js/fullcalendar.js +++ b/ietf/static/js/fullcalendar.js @@ -1,5 +1,7 @@ import { Calendar } from '@fullcalendar/core'; import dayGridPlugin from '@fullcalendar/daygrid'; +import bootstrap5Plugin from '@fullcalendar/bootstrap5' global.FullCalendar = Calendar; -global.dayGridPlugin = dayGridPlugin; \ No newline at end of file +global.dayGridPlugin = dayGridPlugin; +global.bootstrap5Plugin = bootstrap5Plugin; diff --git a/ietf/static/js/highcharts.js b/ietf/static/js/highcharts.js index f9b7aa615..0b99f87a5 100644 --- a/ietf/static/js/highcharts.js +++ b/ietf/static/js/highcharts.js @@ -3,11 +3,119 @@ import Highcharts from "highcharts"; import Highcharts_Exporting from "highcharts/modules/exporting"; import Highcharts_Offline_Exporting from "highcharts/modules/offline-exporting"; import Highcharts_Export_Data from "highcharts/modules/export-data"; -import Highcharts_Accessibility from"highcharts/modules/accessibility"; +import Highcharts_Accessibility from "highcharts/modules/accessibility"; +import Highcharts_Sunburst from "highcharts/modules/sunburst"; Highcharts_Exporting(Highcharts); Highcharts_Offline_Exporting(Highcharts); Highcharts_Export_Data(Highcharts); Highcharts_Accessibility(Highcharts); +Highcharts_Sunburst(Highcharts); + +Highcharts.setOptions({ + // use colors from https://colorbrewer2.org/#type=qualitative&scheme=Paired&n=12 + colors: ['#a6cee3', '#1f78b4', '#b2df8a', '#33a02c', '#fb9a99', + '#e31a1c', '#fdbf6f', '#ff7f00', '#cab2d6', '#6a3d9a', + '#ffff99', '#b15928' + ], + chart: { + height: "100%", + style: { + fontFamily: getComputedStyle(document.body) + .getPropertyValue('--bs-body-font-family') + } + }, + credits: { + enabled: false + }, +}); window.Highcharts = Highcharts; + +window.group_stats = function (url, chart_selector) { + $.getJSON(url, function (data) { + $(chart_selector) + .each(function (i, e) { + const dataset = e.dataset.dataset; + if (!dataset) { + console.log("dataset data attribute not set"); + return; + } + const area = e.dataset.area; + if (!area) { + console.log("area data attribute not set"); + return; + } + + const chart = Highcharts.chart(e, { + title: { + text: `${dataset == "docs" ? "Documents" : "Pages"} in ${area.toUpperCase()}` + }, + series: [{ + type: "sunburst", + data: [], + tooltip: { + pointFormatter: function () { + return `There ${this.value == 1 ? "is" : "are"} ${this.value} ${dataset == "docs" ? "documents" : "pages"} in ${this.name}.`; + } + }, + dataLabels: { + formatter() { + return this.point.active ? this.point.name : `(${this.point.name})`; + } + }, + allowDrillToNode: true, + cursor: 'pointer', + levels: [{ + level: 1, + color: "transparent", + levelSize: { + value: .5 + } + }, { + level: 2, + colorByPoint: true + }, { + level: 3, + colorVariation: { + key: "brightness", + to: 0.5 + } + }] + }], + }); + + // limit data to area if set and (for now) drop docs + const slice = data.filter(d => (area == "ietf" && d.grandparent == area) || d.parent == area || d.id == area) + .map((d) => { + return { + value: d[dataset], + id: d.id, + parent: d.parent, + grandparent: d.grandparent, + active: d.active, + }; + }) + .sort((a, b) => { + if (a.parent != b.parent) { + if (a.parent < b.parent) { + return -1; + } + if (a.parent > b.parent) { + return 1; + } + } else if (a.parent == area) { + if (a.id < b.id) { + return 1; + } + if (a.id > b.id) { + return -1; + } + return 0; + } + return b.value - a.value; + }); + chart.series[0].setData(slice); + }); + }); +} diff --git a/ietf/static/js/session_details_form.js b/ietf/static/js/session_details_form.js index 84dbc5173..04b11875a 100644 --- a/ietf/static/js/session_details_form.js +++ b/ietf/static/js/session_details_form.js @@ -55,9 +55,19 @@ function update_name_field_visibility(name_elts, purpose) { if (!purpose || purpose === 'regular') { - name_elts.forEach(e => e.closest('.form-group').classList.add('hidden')); + name_elts.forEach(e => { + const formGroup = e.closest('.form-group'); + if (formGroup) { + formGroup.classList.add('hidden'); + } + }); } else { - name_elts.forEach(e => e.closest('.form-group').classList.remove('hidden')); + name_elts.forEach(e => { + const formGroup = e.closest('.form-group'); + if (formGroup) { + formGroup.classList.remove('hidden'); + } + }); } } @@ -113,4 +123,4 @@ } } window.addEventListener('load', on_load, false); -})(); \ No newline at end of file +})(); diff --git a/ietf/static/js/upcoming.js b/ietf/static/js/upcoming.js index 8711ce0d3..0785d02cc 100644 --- a/ietf/static/js/upcoming.js +++ b/ietf/static/js/upcoming.js @@ -84,8 +84,9 @@ function update_calendar(tz, filter_params) { */ var calendarEl = document.getElementById('calendar'); event_calendar = new FullCalendar(calendarEl, { - plugins: [dayGridPlugin], + plugins: [dayGridPlugin, bootstrap5Plugin], initialView: 'dayGridMonth', + themeSystem: 'bootstrap5', displayEventTime: false, events: function (fInfo, success) { success(display_events); }, eventContent: function(info) { diff --git a/ietf/sync/discrepancies.py b/ietf/sync/discrepancies.py index 8268b0967..55c6c02cc 100644 --- a/ietf/sync/discrepancies.py +++ b/ietf/sync/discrepancies.py @@ -6,25 +6,25 @@ def find_discrepancies(): title = "Internet-Drafts that have been sent to the RFC Editor but do not have an RFC Editor state" - docs = Document.objects.filter(states__in=list(State.objects.filter(used=True, type="draft-iesg", slug__in=("ann", "rfcqueue")))).exclude(states__in=list(State.objects.filter(used=True, type="draft-rfceditor"))) + docs = Document.objects.filter(states__in=list(State.objects.filter(used=True, type="draft-iesg", slug__in=("ann", "rfcqueue")))).exclude(states__in=list(State.objects.filter(used=True, type="draft-rfceditor"))).distinct() res.append((title, docs)) title = "Internet-Drafts that have the IANA Action state \"In Progress\" but do not have a \"IANA\" RFC-Editor state/tag" - docs = Document.objects.filter(states__in=list(State.objects.filter(used=True, type="draft-iana-action", slug__in=("inprog",)))).exclude(tags="iana").exclude(states__in=list(State.objects.filter(used=True, type="draft-rfceditor", slug="iana"))) + docs = Document.objects.filter(states__in=list(State.objects.filter(used=True, type="draft-iana-action", slug__in=("inprog",)))).exclude(tags="iana").exclude(states__in=list(State.objects.filter(used=True, type="draft-rfceditor", slug="iana"))).distinct() res.append((title, docs)) title = "Internet-Drafts that have the IANA Action state \"Waiting on RFC Editor\" or \"RFC-Ed-Ack\" but are in the RFC Editor state \"IANA\"/tagged with \"IANA\"" - docs = Document.objects.filter(states__in=list(State.objects.filter(used=True, type="draft-iana-action", slug__in=("waitrfc", "rfcedack")))).filter(models.Q(tags="iana") | models.Q(states__in=list(State.objects.filter(used=True, type="draft-rfceditor", slug="iana")))) + docs = Document.objects.filter(states__in=list(State.objects.filter(used=True, type="draft-iana-action", slug__in=("waitrfc", "rfcedack")))).filter(models.Q(tags="iana") | models.Q(states__in=list(State.objects.filter(used=True, type="draft-rfceditor", slug="iana")))).distinct() res.append((title, docs)) title = "Internet-Drafts that have a state other than \"RFC Ed Queue\", \"RFC Published\" or \"Sent to the RFC Editor\" and have an RFC Editor or IANA Action state" - docs = Document.objects.exclude(states__in=list(State.objects.filter(used=True, type="draft-iesg", slug__in=("rfcqueue", "pub"))) + list(State.objects.filter(used=True, type__in=("draft-stream-iab", "draft-stream-ise", "draft-stream-irtf"), slug="rfc-edit"))).filter(states__in=list(State.objects.filter(used=True, type__in=("draft-iana-action", "draft-rfceditor")))) + docs = Document.objects.exclude(states__in=list(State.objects.filter(used=True, type="draft-iesg", slug__in=("rfcqueue", "pub"))) + list(State.objects.filter(used=True, type__in=("draft-stream-iab", "draft-stream-ise", "draft-stream-irtf"), slug="rfc-edit"))).filter(states__in=list(State.objects.filter(used=True, type__in=("draft-iana-action", "draft-rfceditor")))).distinct() res.append((title, docs)) diff --git a/ietf/templates/base.html b/ietf/templates/base.html index 1571ff500..9cd386832 100644 --- a/ietf/templates/base.html +++ b/ietf/templates/base.html @@ -105,12 +105,12 @@ {% for message in messages %} -
+
- {{ message.message }} + {{ message.message }}
{% endfor %} {% block content %}{{ content|safe }}{% endblock %} diff --git a/ietf/templates/doc/bibxml.xml b/ietf/templates/doc/bibxml.xml index a7513df47..750789da6 100644 --- a/ietf/templates/doc/bibxml.xml +++ b/ietf/templates/doc/bibxml.xml @@ -1,5 +1,5 @@ - + {{doc.title}}{% for author in doc.documentauthor_set.all %} diff --git a/ietf/templates/doc/document_html.html b/ietf/templates/doc/document_html.html index b5122484b..9823a0d37 100644 --- a/ietf/templates/doc/document_html.html +++ b/ietf/templates/doc/document_html.html @@ -232,7 +232,7 @@ - Report a bug + Report a datatracker bug diff --git a/ietf/templates/doc/document_info.html b/ietf/templates/doc/document_info.html index c9a73eafb..392c5f124 100644 --- a/ietf/templates/doc/document_info.html +++ b/ietf/templates/doc/document_info.html @@ -26,9 +26,17 @@ {% endif %} {% if document_html %}
{% endif %} {% if has_verified_errata or has_errata %} - - Errata + {% if document_html %}View errata{% else %}Errata{% endif %} + + {% endif %} + {% if document_html and doc.get_state_slug == "rfc" and not snapshot %} + + Report errata {% endif %} {% if doc.related_ipr %} diff --git a/ietf/templates/doc/mail/ballot_writeup.txt b/ietf/templates/doc/mail/ballot_writeup.txt index c808e5a4a..7c7209cf6 100644 --- a/ietf/templates/doc/mail/ballot_writeup.txt +++ b/ietf/templates/doc/mail/ballot_writeup.txt @@ -1,11 +1,12 @@ -{% autoescape off %} -Technical Summary - +{% load ietf_filters %}{% autoescape off %}Technical Summary +{% if doc.abstract %} +{{ doc.abstract.rstrip }} +{% else %} Relevant content can frequently be found in the abstract and/or introduction of the document. If not, this may be an indication that there are deficiencies in the abstract or introduction. - +{% endif %} Working Group Summary Was there anything in the WG process that is worth noting? @@ -26,23 +27,21 @@ Document Quality Review, on what date was the request posted? Personnel - +{% if doc.shepherd and doc.ad %}{% filter wordwrap:"76" %} + The Document Shepherd for this document is {{ doc.shepherd.person.name }}. The Responsible Area Director is {{ doc.ad.name }}.{% endfilter %} +{% else %} Who is the Document Shepherd for this document? Who is the - Responsible Area Director? If the document requires IANA - experts(s), insert 'The IANA Expert(s) for the registries - in this document are .' - -IRTF Note + Responsible Area Director? +{% endif %} +{% if doc.stream.slug == "irtf" %}IRTF Note (Insert IRTF Note here or remove section) - -IESG Note +{% elif doc.stream.slug == "ietf" %}IESG Note (Insert IESG Note here or remove section) - +{% endif %} IANA Note {% if iana %} - {% load ietf_filters %}{% filter wordwrap:"76"|indent:2 %}{{ iana }}{% endfilter %} + {% filter wordwrap:"76"|indent:2 %}{{ iana }}{% endfilter %} {% endif %} - (Insert IANA Note here or remove section) -{% endautoescape%} + (Insert IANA Note here or remove section){% endautoescape%} diff --git a/ietf/templates/group/active_areas.html b/ietf/templates/group/active_areas.html index d3fe45f45..0f4704932 100644 --- a/ietf/templates/group/active_areas.html +++ b/ietf/templates/group/active_areas.html @@ -1,6 +1,6 @@ {% extends "base.html" %} {# Copyright The IETF Trust 2015, All Rights Reserved #} -{% load origin textfilters ietf_filters%} +{% load origin textfilters ietf_filters static %} {% block title %}Active areas{% endblock %} {% block content %} {% origin %} @@ -43,8 +43,24 @@ {% endfor %} +

+ The following diagrams show the sizes of the different areas and working groups, + based on the number of documents - and pages - a group has worked on in the last three years. +

+
+
+
+ Loading... +
+
+
+
+ Loading... +
+
+
{% for area in areas %} -

+

{{ area.name }} ({{ area.acronym|upper }})

@@ -53,5 +69,14 @@ {{ area.description|urlize_ietf_docs|linkify|safe }}

{% endif %} + {% include "group/group_stats_modal.html" with group=area only %} {% endfor %} +{% endblock %} +{% block js %} + + {% endblock %} \ No newline at end of file diff --git a/ietf/templates/group/group_about.html b/ietf/templates/group/group_about.html index 261ffe31f..6b2a36e90 100644 --- a/ietf/templates/group/group_about.html +++ b/ietf/templates/group/group_about.html @@ -211,6 +211,18 @@ height: 100vh; {% endif %} {% endwith %} + {% if group.type.slug == "area" %} + + + + Group statistics + + + + {% include "group/group_stats_modal.html" with group=group only %} + + + {% endif %} {% if group.personnel %} @@ -393,4 +405,10 @@ height: 100vh; {% block js %} + + {% endblock %} \ No newline at end of file diff --git a/ietf/templates/group/group_stats_modal.html b/ietf/templates/group/group_stats_modal.html new file mode 100644 index 000000000..6431b675b --- /dev/null +++ b/ietf/templates/group/group_stats_modal.html @@ -0,0 +1,45 @@ + + \ No newline at end of file diff --git a/ietf/templates/liaisons/detail.html b/ietf/templates/liaisons/detail.html index 12081122a..1f3ae3fc0 100644 --- a/ietf/templates/liaisons/detail.html +++ b/ietf/templates/liaisons/detail.html @@ -13,6 +13,7 @@
{% include 'liaisons/liaison_title.html' %} + {% include "liaisons/liaison_desc.html" only %} {% include "liaisons/detail_tabs.html" %} diff --git a/ietf/templates/liaisons/liaison_base.html b/ietf/templates/liaisons/liaison_base.html index aacab6dea..218181758 100644 --- a/ietf/templates/liaisons/liaison_base.html +++ b/ietf/templates/liaisons/liaison_base.html @@ -11,6 +11,7 @@ {% block content %} {% origin %}

Liaison Statements

+ {% include "liaisons/liaison_desc.html" only %} {% if with_search %}
{% include "liaisons/search_form.html" %}
{% endif %} diff --git a/ietf/templates/liaisons/liaison_desc.html b/ietf/templates/liaisons/liaison_desc.html new file mode 100644 index 000000000..1286198e2 --- /dev/null +++ b/ietf/templates/liaisons/liaison_desc.html @@ -0,0 +1,4 @@ +
+ Additional information about IETF liaison relationships is available on the + Internet Architecture Board liaison webpage. +
\ No newline at end of file diff --git a/ietf/templates/meeting/agenda.ics b/ietf/templates/meeting/agenda.ics index bcb9827c9..08b544917 100644 --- a/ietf/templates/meeting/agenda.ics +++ b/ietf/templates/meeting/agenda.ics @@ -13,11 +13,15 @@ DTEND{% ics_date_time item.timeslot.local_end_time schedule.meeting.time_zone %} DTSTAMP{% ics_date_time item.timeslot.modified|utc 'utc' %}{% if item.session.agenda %} URL:{{item.session.agenda.get_versionless_href}}{% endif %} DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %} - Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% if item.timeslot.location.webex_url %} + Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% if item.timeslot.location.onsite_tool_url %} \n - Webex: {{ item.timeslot.location.webex_url }}\n{% endif %}{% if item.timeslot.location.video_stream_url %} + Onsite tool: {{ item.timeslot.location.onsite_tool_url|format:item.session }}\n{% endif %}{% if item.timeslot.location.video_stream_url %} \n - Meetecho: {{ item.timeslot.location.video_stream_url|format:item.session }}\n{% endif %}{% if item.session.agenda %}{% with agenda=item.session.agenda %} + Meetecho: {{ item.timeslot.location.video_stream_url|format:item.session }}\n{% endif %}{% if item.timeslot.location.webex_url %} + \n + Webex: {{ item.timeslot.location.webex_url }}\n{% endif %}{% if item.session.remote_instructions %} + \n + Remote instructions: {{ item.session.remote_instructions }}\n{% endif %}{% if item.session.agenda %}{% with agenda=item.session.agenda %} \n {{agenda.type}} {{agenda.get_versionless_href}}\n{% endwith %}{% endif %} \n diff --git a/ietf/templates/meeting/create_timeslot.html b/ietf/templates/meeting/create_timeslot.html index 8a47b9c96..62847c1a0 100755 --- a/ietf/templates/meeting/create_timeslot.html +++ b/ietf/templates/meeting/create_timeslot.html @@ -12,7 +12,7 @@ {% bootstrap_form form %} Back + href="{% url 'ietf.meeting.views.edit_timeslots' num=meeting.number %}{% if 'sched' in request.GET %}?sched={{ request.GET.sched }}{% endif %}">Back {% endblock %} {% block js %} diff --git a/ietf/templates/meeting/edit_meeting_schedule.html b/ietf/templates/meeting/edit_meeting_schedule.html index 7005ad502..91c426022 100644 --- a/ietf/templates/meeting/edit_meeting_schedule.html +++ b/ietf/templates/meeting/edit_meeting_schedule.html @@ -40,22 +40,22 @@ @@ -64,7 +64,7 @@ You can't edit this schedule. {% if schedule.is_official_record %}This is the official schedule for a meeting in the past.{% endif %} Make a - + new agenda from this. {% endif %} @@ -74,7 +74,8 @@ No timeslots exist for this meeting yet.

- + Edit timeslots

diff --git a/ietf/templates/meeting/edit_meeting_schedule_session.html b/ietf/templates/meeting/edit_meeting_schedule_session.html index da37705cd..4a2bbfdde 100644 --- a/ietf/templates/meeting/edit_meeting_schedule_session.html +++ b/ietf/templates/meeting/edit_meeting_schedule_session.html @@ -104,7 +104,7 @@ {% endfor %} {% if secretariat %} + href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}?sched={{ schedule.pk }}"> Edit session Save - - Back - + Back {% endblock %} {% block js %}{{ form.media.js }}{% endblock %} \ No newline at end of file diff --git a/ietf/templates/meeting/edit_timeslot.html b/ietf/templates/meeting/edit_timeslot.html index c969f171d..a840adbc8 100644 --- a/ietf/templates/meeting/edit_timeslot.html +++ b/ietf/templates/meeting/edit_timeslot.html @@ -25,7 +25,7 @@ {% bootstrap_form form %} + href="{% url 'ietf.meeting.views.edit_timeslots' num=timeslot.meeting.number %}{% if "sched" in request.GET %}?sched={{ request.GET.sched }}{% endif %}"> Back diff --git a/ietf/templates/meeting/past.html b/ietf/templates/meeting/past.html index d6ca1466f..6de44b80b 100644 --- a/ietf/templates/meeting/past.html +++ b/ietf/templates/meeting/past.html @@ -2,11 +2,13 @@ {# Copyright The IETF Trust 2015, All Rights Reserved #} {% load origin %} {% load ietf_filters static %} +{% load cache %} {% block pagehead %} {% endblock %} {% block title %}Past Meetings{% endblock %} {% block content %} + {% cache 3600 pastmeetings %} {% origin %}

Past Meetings

{% if meetings %} @@ -55,6 +57,7 @@ {% else %}

No past meetings

{% endif %} + {% endcache %} {% endblock %} {% block js %} diff --git a/ietf/templates/meeting/proceedings_progress_report.html b/ietf/templates/meeting/proceedings_progress_report.html index a51fb01e6..27757f5fa 100644 --- a/ietf/templates/meeting/proceedings_progress_report.html +++ b/ietf/templates/meeting/proceedings_progress_report.html @@ -1,7 +1,8 @@ {% extends "base.html" %} -{% load ams_filters ietf_filters %} +{% load ams_filters ietf_filters cache %} {% block title %}IETF {{ meeting.number }} Proceedings - Progress Report{% endblock %} {% block content %} + {% cache 3600 proceedings_progress_report meeting.number %}

@@ -67,4 +68,5 @@

{% endif %}
+ {% endcache %} {% endblock %} \ No newline at end of file diff --git a/ietf/templates/meeting/session_details.html b/ietf/templates/meeting/session_details.html index 03e6fa195..7f1a825c4 100644 --- a/ietf/templates/meeting/session_details.html +++ b/ietf/templates/meeting/session_details.html @@ -3,8 +3,8 @@ {% load origin ietf_filters static %} {% block title %}{{ meeting }} : {{ acronym }}{% endblock %} {% block morecss %} - .draggable { - cursor: pointer; + .drag-handle { + cursor: move; } {% endblock %} {% block content %} @@ -64,6 +64,7 @@ var options = { group: "slides", animation: 150, + handle: ".drag-handle", onAdd: function(event) {onAdd(event)}, onRemove: function(event) {onRemove(event)}, onEnd: function(event) {onEnd(event)} diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html index 986dfc9b2..c5662bfc2 100644 --- a/ietf/templates/meeting/session_details_panel.html +++ b/ietf/templates/meeting/session_details_panel.html @@ -137,16 +137,11 @@ data-remove-from-session="{% url 'ietf.meeting.views.ajax_remove_slides_from_session' session_id=session.pk num=session.meeting.number %}" data-reorder-in-session="{% url 'ietf.meeting.views.ajax_reorder_slides_in_session' session_id=session.pk num=session.meeting.number %}"> {% for pres in session.filtered_slides %} - + {% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %} {% if can_manage_materials %} - + {% endif %} diff --git a/ietf/templates/meeting/timeslot_edit.html b/ietf/templates/meeting/timeslot_edit.html index 5c31c8195..8f161a3bb 100644 --- a/ietf/templates/meeting/timeslot_edit.html +++ b/ietf/templates/meeting/timeslot_edit.html @@ -35,7 +35,7 @@ a.new-timeslot-link { color: lightgray; font-size: large;}
+ href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}{% if "sched" in request.GET %}?sched={{ request.GET.sched }}{% endif %}"> New timeslot {% if meeting.schedule %} @@ -51,6 +51,9 @@ a.new-timeslot-link { color: lightgray; font-size: large;} href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}"> Agenda list + {% if schedule_edit_url %} + Back to agenda + {% endif %}
{% if rooms|length == 0 %} @@ -126,7 +129,9 @@ a.new-timeslot-link { color: lightgray; font-size: large;}

No timeslots exist for this meeting yet.

- Create a timeslot. + + Create a timeslot. + {% else %} {% for day in time_slices %} @@ -137,11 +142,11 @@ a.new-timeslot-link { color: lightgray; font-size: large;} {% if cell_ts %} {% for ts in cell_ts %} - {% include 'meeting/timeslot_edit_timeslot.html' with ts=ts in_use=ts_with_any_assignments in_official_use=ts_with_official_assignments only %} + {% include 'meeting/timeslot_edit_timeslot.html' with ts=ts in_use=ts_with_any_assignments in_official_use=ts_with_official_assignments request=request only %} {% endfor %} {% endif %} + href="{% url 'ietf.meeting.views.create_timeslot' num=meeting.number %}?day={{ day.toordinal }}&date={{ day|date:'Y-m-d' }}&location={{ room.pk }}&time={{ slot.time|date:'H:i' }}&duration={{ slot.duration }}{% if "sched" in request.GET %}&sched={{ request.GET.sched }}{% endif %}"> diff --git a/ietf/templates/meeting/timeslot_edit_timeslot.html b/ietf/templates/meeting/timeslot_edit_timeslot.html index 15c6fe9d9..e3c51fb75 100644 --- a/ietf/templates/meeting/timeslot_edit_timeslot.html +++ b/ietf/templates/meeting/timeslot_edit_timeslot.html @@ -16,7 +16,7 @@ data-official-use="{% if ts in in_official_use %}true{% else %}false{% endif %}"
- + diff --git a/ietf/templates/meeting/upcoming.html b/ietf/templates/meeting/upcoming.html index 09dff6de4..815698dbf 100644 --- a/ietf/templates/meeting/upcoming.html +++ b/ietf/templates/meeting/upcoming.html @@ -5,7 +5,6 @@ {% load ietf_filters static classname tz %} {% block pagehead %} - {% endblock %} {% block title %}Upcoming Meetings{% endblock %} {% block content %} diff --git a/ietf/templates/meeting/upcoming.ics b/ietf/templates/meeting/upcoming.ics index 74105a054..fb5b37d77 100644 --- a/ietf/templates/meeting/upcoming.ics +++ b/ietf/templates/meeting/upcoming.ics @@ -1,4 +1,4 @@ -{% load humanize tz %}{% autoescape off %}{% load ietf_filters %}BEGIN:VCALENDAR +{% load humanize tz %}{% autoescape off %}{% load ietf_filters textfilters %}BEGIN:VCALENDAR VERSION:2.0 METHOD:PUBLISH PRODID:-//IETF//datatracker.ietf.org ical upcoming//EN @@ -10,14 +10,15 @@ SUMMARY:{% if item.session.name %}{{item.session.group.acronym|lower}} - {{item. CLASS:PUBLIC DTSTART{% ics_date_time item.timeslot.local_start_time item.schedule.meeting.time_zone %} DTEND{% ics_date_time item.timeslot.local_end_time item.schedule.meeting.time_zone %} -DTSTAMP{% ics_date_time item.timeslot.modified|utc 'utc' %} -{% if item.session.agenda %}URL:{{item.session.agenda.get_href}} -DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %} +DTSTAMP{% ics_date_time item.timeslot.modified|utc 'utc' %}{% if item.session.agenda %} +URL:{{item.session.agenda.get_href}}{% endif %} +DESCRIPTION:{% if item.timeslot.name %}{{item.timeslot.name|ics_esc}}\n{% endif %}{% if item.session.agenda_note %} Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% for material in item.session.materials.all %} \n{{material.type}}{% if material.type.name != "Agenda" %} ({{material.title|ics_esc}}){% endif %}: - {{material.get_href}}\n{% endfor %} -{% endif %}END:VEVENT + {{material.get_href}}\n{% endfor %}{% if item.session.remote_instructions %} + Remote instructions: {{ item.session.remote_instructions }}\n{% endif %} +END:VEVENT {% endfor %}{% for meeting in ietfs %}BEGIN:VEVENT UID:ietf-{{ meeting.number }} SUMMARY:IETF {{ meeting.number }}{% if meeting.city %} diff --git a/ietf/utils/fields.py b/ietf/utils/fields.py index 39f97535a..95d8a2aa7 100644 --- a/ietf/utils/fields.py +++ b/ietf/utils/fields.py @@ -200,7 +200,7 @@ class SearchableField(forms.MultipleChoiceField): # model = None # must be filled in by subclass model = None # type:Optional[Type[models.Model]] # max_entries = None # may be overridden in __init__ - max_entries = None # type: Optional[int] + max_entries = None # type: Optional[int] min_search_length = None # type: Optional[int] default_hint_text = 'Type a value to search' diff --git a/ietf/utils/test_draft_with_references_v3.xml b/ietf/utils/test_draft_with_references_v3.xml index a04880d1d..ad9b2b280 100644 --- a/ietf/utils/test_draft_with_references_v3.xml +++ b/ietf/utils/test_draft_with_references_v3.xml @@ -1,6 +1,6 @@ - + Test Draft with References @@ -37,9 +37,25 @@ + + + Cloud Software + + + + + + + + Informative References + + + + + Status of network hosts @@ -51,6 +67,34 @@ + + + Key Consistency and Discovery + + Brave Software + + + The Tor Project + + + Mozilla + + + Cloudflare + + + + This document describes the key consistency and correctness + requirements of protocols such as Privacy Pass, Oblivious DoH, and + Oblivious HTTP for user privacy. It discusses several mechanisms and + proposals for enabling user privacy in varying threat models. In + concludes with discussion of open problems in this area. + + + + + + @@ -191,4 +235,4 @@ - \ No newline at end of file + diff --git a/ietf/utils/tests.py b/ietf/utils/tests.py index 2095b5fa4..69c16be6d 100644 --- a/ietf/utils/tests.py +++ b/ietf/utils/tests.py @@ -374,10 +374,17 @@ class XMLDraftTests(TestCase): draft.get_refs(), { 'rfc1': XMLDraft.REF_TYPE_NORMATIVE, + 'rfc2': XMLDraft.REF_TYPE_NORMATIVE, + 'draft-wood-key-consistency-03': XMLDraft.REF_TYPE_INFORMATIVE, 'rfc255': XMLDraft.REF_TYPE_INFORMATIVE, 'bcp6': XMLDraft.REF_TYPE_INFORMATIVE, + 'bcp14': XMLDraft.REF_TYPE_INFORMATIVE, 'rfc1207': XMLDraft.REF_TYPE_UNKNOWN, 'rfc4086': XMLDraft.REF_TYPE_NORMATIVE, + 'draft-ietf-teas-pcecc-use-cases-00': XMLDraft.REF_TYPE_INFORMATIVE, + 'draft-ietf-teas-pcecc-use-cases': XMLDraft.REF_TYPE_INFORMATIVE, + 'draft-ietf-sipcore-multiple-reasons-00': XMLDraft.REF_TYPE_INFORMATIVE, + 'draft-ietf-sipcore-multiple-reasons': XMLDraft.REF_TYPE_INFORMATIVE, } ) diff --git a/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py index 15bf745cc..7e8674ea7 100644 --- a/ietf/utils/xmldraft.py +++ b/ietf/utils/xmldraft.py @@ -60,17 +60,47 @@ class XMLDraft(Draft): tree.tree = v2v3.convert2to3() return tree, xml_version - def _document_name(self, anchor): - """Guess document name from reference anchor + def _document_name(self, ref): + """Get document name from reference.""" + series = ["rfc", "bcp", "fyi", "std"] + # handle xinclude first + # FIXME: this assumes the xinclude is a bibxml href; if it isn't, there can + # still be false negatives. it would be better to expand the xinclude and parse + # its seriesInfo. + if ref.tag.endswith("}include"): + name = re.search( + rf"reference\.({'|'.join(series).upper()})\.(\d{{4}})\.xml", + ref.attrib["href"], + ) + if name: + return f"{name.group(1)}{int(name.group(2))}".lower() + name = re.search( + r"reference\.I-D\.(?:draft-)?(.*)\.xml", ref.attrib["href"] + ) + if name: + return f"draft-{name.group(1)}" + # can't extract the name, give up + return "" - Looks for series numbers and removes leading 0s from the number. - """ - anchor = anchor.lower() # always give back lowercase - label = anchor.rstrip('0123456789') # remove trailing digits - if label in ['rfc', 'bcp', 'fyi', 'std']: - number = int(anchor[len(label):]) - return f'{label}{number}' - return anchor + # check the anchor next + anchor = ref.get("anchor").lower() # always give back lowercase + label = anchor.rstrip("0123456789") # remove trailing digits + if label in series: + number = int(anchor[len(label) :]) + return f"{label}{number}" + + # if we couldn't find a match so far, try the seriesInfo + series_query = " or ".join(f"@name='{x.upper()}'" for x in series) + for info in ref.xpath( + f"./seriesInfo[{series_query} or @name='Internet-Draft']" + ): + if not info.attrib["value"]: + continue + if info.attrib["name"] == "Internet-Draft": + return info.attrib["value"] + else: + return f'{info.attrib["name"].lower()}{info.attrib["value"]}' + return "" def _reference_section_type(self, section_name): """Determine reference type from name of references section""" @@ -154,10 +184,20 @@ class XMLDraft(Draft): """Extract references from the draft""" refs = {} # accept nested sections - for section in self.xmlroot.findall('back//references'): - ref_type = self._reference_section_type(self._reference_section_name(section)) - for ref in (section.findall('./reference') + section.findall('./referencegroup')): - refs[self._document_name(ref.get('anchor'))] = ref_type + for section in self.xmlroot.findall("back//references"): + ref_type = self._reference_section_type( + self._reference_section_name(section) + ) + for ref in ( + section.findall("./reference") + + section.findall("./referencegroup") + + section.findall( + "./xi:include", {"xi": "http://www.w3.org/2001/XInclude"} + ) + ): + name = self._document_name(ref) + if name: + refs[name] = ref_type return refs diff --git a/package.json b/package.json index 252cde4ad..1373fa2ce 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,14 @@ "legacy:build": "parcel build" }, "dependencies": { - "@fullcalendar/bootstrap5": "5.11.4", - "@fullcalendar/core": "5.11.4", - "@fullcalendar/daygrid": "5.11.4", - "@fullcalendar/interaction": "5.11.4", - "@fullcalendar/list": "5.11.4", - "@fullcalendar/luxon2": "5.11.4", - "@fullcalendar/timegrid": "5.11.4", - "@fullcalendar/vue3": "5.11.4", + "@fullcalendar/bootstrap5": "6.1.4", + "@fullcalendar/core": "6.1.4", + "@fullcalendar/daygrid": "6.1.4", + "@fullcalendar/interaction": "6.1.4", + "@fullcalendar/list": "6.1.4", + "@fullcalendar/luxon2": "6.1.4", + "@fullcalendar/timegrid": "6.1.4", + "@fullcalendar/vue3": "6.1.4", "@popperjs/core": "2.11.6", "@twuni/emojify": "1.0.2", "bootstrap": "5.2.3", diff --git a/yarn.lock b/yarn.lock index 42d992c42..e34805f13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,98 +121,79 @@ __metadata: languageName: node linkType: hard -"@fullcalendar/bootstrap5@npm:5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/bootstrap5@npm:5.11.4" - dependencies: - "@fullcalendar/common": ~5.11.4 - tslib: ^2.1.0 - checksum: 26f838f30425ddd629026d7d6e8c4bf1b016b87f7b9ee1bfafa6d4ee536f140aee64b8cdc159ec338a8789d3353baa5e7e725b509389cfe4a47d20bc87a93009 +"@fullcalendar/bootstrap5@npm:6.1.4": + version: 6.1.4 + resolution: "@fullcalendar/bootstrap5@npm:6.1.4" + peerDependencies: + "@fullcalendar/core": ~6.1.4 + checksum: e4a5dd281d95d0cd24ee8ecb6b0454b1a081c5236cb9ac8d2b0ddac777160d0bd31a7f489368c7d0c33a1ac026cec9e8d62cc4e7f1980a11d84165a271860371 languageName: node linkType: hard -"@fullcalendar/common@npm:~5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/common@npm:5.11.4" +"@fullcalendar/core@npm:6.1.4": + version: 6.1.4 + resolution: "@fullcalendar/core@npm:6.1.4" dependencies: - tslib: ^2.1.0 - checksum: 8fc0e05539ba83d310eb5a7163dcd10582d83465393cccb525022b20c954e29e6361b289a2d2eec1ae5b5d950700333739b81cb5e81bc8cb72f682484ca697af - languageName: node - linkType: hard - -"@fullcalendar/core@npm:5.11.4, @fullcalendar/core@npm:~5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/core@npm:5.11.4" - dependencies: - "@fullcalendar/common": ~5.11.4 preact: ^10.0.5 - tslib: ^2.1.0 - checksum: 11652a58dc4a7af2b9c552ca71e4215c56d574f7d639deab0a6604afa0a67c7bfef445d5a6e364b6bc6b0ffb333ba9e7730e184d480e0113dce90c470d988d17 + checksum: 3c659bbaf814a1f084ddd3e2eeaf68bb48f268a36560abb399c4aa29a40d6017c5bb1cd87c43664f333a1f6682c32b6a85ffb661538ea0faa78a1c119760a381 languageName: node linkType: hard -"@fullcalendar/daygrid@npm:5.11.4, @fullcalendar/daygrid@npm:~5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/daygrid@npm:5.11.4" - dependencies: - "@fullcalendar/common": ~5.11.4 - tslib: ^2.1.0 - checksum: a25d83cfe5b3ac3feeb49af47701c54e858d30b0b53871df2a83aa7edbcc7d49f435d59fdbf3d6e18b5699caced8133e9c4b1c919caca2c8717bd92f57c08ab4 - languageName: node - linkType: hard - -"@fullcalendar/interaction@npm:5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/interaction@npm:5.11.4" - dependencies: - "@fullcalendar/common": ~5.11.4 - tslib: ^2.1.0 - checksum: 88231b925498b947f5af98fcabf564f7d72ecde6660696e5aec7aa5c4aca7988deab74c9486a30e0e461cdd31913c560bb016f54a61641d13359488e845e053f - languageName: node - linkType: hard - -"@fullcalendar/list@npm:5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/list@npm:5.11.4" - dependencies: - "@fullcalendar/common": ~5.11.4 - tslib: ^2.1.0 - checksum: e2cec5e89c57836a9ca4db57fbe67e16f78d91191df39ac0b5d0f18c1fdac9763f04647ae42e5f2d31ffa3cd245423d3aec449ed6c84fafc0195e62079a4eedc - languageName: node - linkType: hard - -"@fullcalendar/luxon2@npm:5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/luxon2@npm:5.11.4" - dependencies: - "@fullcalendar/common": ~5.11.4 - tslib: ^2.1.0 +"@fullcalendar/daygrid@npm:6.1.4, @fullcalendar/daygrid@npm:~6.1.4": + version: 6.1.4 + resolution: "@fullcalendar/daygrid@npm:6.1.4" peerDependencies: + "@fullcalendar/core": ~6.1.4 + checksum: 24b3c6e521c5e5288cb05d34e080daace1138b4b421a5dd059938a8c9b7730374b7f07908984b124d48e810276a3c24f1e1c7b6080d375ca80e4d7ef387d7774 + languageName: node + linkType: hard + +"@fullcalendar/interaction@npm:6.1.4": + version: 6.1.4 + resolution: "@fullcalendar/interaction@npm:6.1.4" + peerDependencies: + "@fullcalendar/core": ~6.1.4 + checksum: 5e282ba36bfbc306e8e36ea88b9df0c5a11b7bcdf4c8952377396da24a3ca058b436a39e9002eba9fc1ca8ce70ca23ef2fd74ef08f48d4b8b45576f7e30a6bea + languageName: node + linkType: hard + +"@fullcalendar/list@npm:6.1.4": + version: 6.1.4 + resolution: "@fullcalendar/list@npm:6.1.4" + peerDependencies: + "@fullcalendar/core": ~6.1.4 + checksum: 0338a8bb1546e103921d1d6a5baf54b9d43759d9aaf905357c2272e01cbd42e120812128b2f2661d0ef82abc0b8447b16e6f1618968487ffa4c6f0a570f907d5 + languageName: node + linkType: hard + +"@fullcalendar/luxon2@npm:6.1.4": + version: 6.1.4 + resolution: "@fullcalendar/luxon2@npm:6.1.4" + peerDependencies: + "@fullcalendar/core": ~6.1.4 luxon: ^2.0.0 - checksum: 503e3e32d27c1fbd95d0e7c9cbf89e751eaaf57920017672d3f716fa0ba919f618ccefb6d278c60ee72b9ac8756e1f57ad9148084ffa356bf51950f9e7ee8426 + checksum: 577283ad7cd555c25143f64c865554aebaa957b0bc51bcece7114bb7282abb448a8450b23863fb6287c6e91f7f89cc261beb51c8e0542c5b0df6c853fe3bd9c3 languageName: node linkType: hard -"@fullcalendar/timegrid@npm:5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/timegrid@npm:5.11.4" +"@fullcalendar/timegrid@npm:6.1.4": + version: 6.1.4 + resolution: "@fullcalendar/timegrid@npm:6.1.4" dependencies: - "@fullcalendar/common": ~5.11.4 - "@fullcalendar/daygrid": ~5.11.4 - tslib: ^2.1.0 - checksum: 3a2fccac65198c5fffa53286fcbb2556b6e3885cfc6f7978b7ee21c57cbc2a58617e6c54eff977f5234163cef3281e50548327a06cf253acc43c6881bed00f2c - languageName: node - linkType: hard - -"@fullcalendar/vue3@npm:5.11.4": - version: 5.11.4 - resolution: "@fullcalendar/vue3@npm:5.11.4" - dependencies: - "@fullcalendar/core": ~5.11.4 - tslib: ^2.1.0 + "@fullcalendar/daygrid": ~6.1.4 peerDependencies: + "@fullcalendar/core": ~6.1.4 + checksum: 1329b941f94e2c17a866e612e7d92adacbca0364ba7e76908caa2a936f526c0cde43fa050f571634dd2db2e779f1cbd3115b64cbc0b524d34f0c4d61243659b8 + languageName: node + linkType: hard + +"@fullcalendar/vue3@npm:6.1.4": + version: 6.1.4 + resolution: "@fullcalendar/vue3@npm:6.1.4" + peerDependencies: + "@fullcalendar/core": ~6.1.4 vue: ^3.0.11 - checksum: 3e0fc0423b396813ef1d6409951903bd1e411c0ab7e64aa7d28ba81e3066e50c7b24d224c14d873a0ffa0aacc94d4bc64097d4fb6083870f748a9502fefebc29 + checksum: 3e11102fbf09e2e62d52c03f7fc42435c816faeefd57c1e551271af6bfdeb52be7380b6d9b199e652a742626c17a21227092b83039f30a48f76d9d26ffae9beb languageName: node linkType: hard @@ -6159,14 +6140,14 @@ browserlist@latest: resolution: "root-workspace-0b6124@workspace:." dependencies: "@faker-js/faker": 7.6.0 - "@fullcalendar/bootstrap5": 5.11.4 - "@fullcalendar/core": 5.11.4 - "@fullcalendar/daygrid": 5.11.4 - "@fullcalendar/interaction": 5.11.4 - "@fullcalendar/list": 5.11.4 - "@fullcalendar/luxon2": 5.11.4 - "@fullcalendar/timegrid": 5.11.4 - "@fullcalendar/vue3": 5.11.4 + "@fullcalendar/bootstrap5": 6.1.4 + "@fullcalendar/core": 6.1.4 + "@fullcalendar/daygrid": 6.1.4 + "@fullcalendar/interaction": 6.1.4 + "@fullcalendar/list": 6.1.4 + "@fullcalendar/luxon2": 6.1.4 + "@fullcalendar/timegrid": 6.1.4 + "@fullcalendar/vue3": 6.1.4 "@parcel/optimizer-data-url": 2.8.3 "@parcel/transformer-inline-string": 2.8.3 "@parcel/transformer-sass": 2.8.3 @@ -6815,7 +6796,7 @@ browserlist@latest: languageName: node linkType: hard -"tslib@npm:^2.1.0, tslib@npm:^2.4.0": +"tslib@npm:^2.4.0": version: 2.4.0 resolution: "tslib@npm:2.4.0" checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113