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.
{% 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.
+
{% 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 @@
+
+
+
+
+
+
{{ group.acronym|upper }} statistics
+
+
+
+
+
+
+ Loading...
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
+
\ 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" %}
@@ -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.