Merge remote-tracking branch 'ietf-tools/main' into feat/postgres

This commit is contained in:
Robert Sparks 2023-02-27 12:55:47 -06:00
commit f010766654
No known key found for this signature in database
GPG key ID: 6E2A6A5775F91318
85 changed files with 1447 additions and 735 deletions

View file

@ -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.

View file

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

View file

@ -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**

View file

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

View file

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

193
.pnp.cjs generated
View file

@ -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"],\

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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'

View file

@ -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/'

View file

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

View file

@ -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<name>[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', api_views.rfcdiff_latest_json),
]
# Additional (standard) Tastypie endpoints

View file

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

View file

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

View file

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

View file

@ -85,11 +85,6 @@ urlpatterns = [
url(r'^html/(?P<name>[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<name>[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<name>[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<name>[A-Za-z0-9._+-]+)/session/', include(session_patterns)),
url(r'^(?P<name>[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<name>[Rr][Ff][Cc] [0-9]+?)(\.txt|\.html)?/?$', RedirectView.as_view(pattern_name='ietf.api.views.rfcdiff_latest_json', permanent=True)),
]

View file

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

View file

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

View file

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

View file

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

View file

@ -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<group_type>(wg|rg))/$', views.edit, {'action': "charter"}),

View file

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

View file

@ -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:

View file

@ -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: <b>{sp.document.name}-{sp.document.rev}.txt</b>",
)
]
)
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')

View file

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

View file

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

View file

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

View file

@ -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']

View file

@ -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:

View file

@ -28,12 +28,12 @@
{% endif %}
<tr class="bg1"><td>Number of Attendees:{% if not is_virtual %}<span class="required">*</span>{% endif %}</td><td>{{ form.attendees.errors }}{{ form.attendees }}</td></tr>
<tr class="bg2">
<td>People who must be present:</td>
<td>Participants who must be present:</td>
<td>
{{ form.bethere.errors }}
{{ form.bethere }}
<p class="fw-bold text-danger">
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.
</p>
</td>
</tr>

View file

@ -35,7 +35,7 @@
</tr>
{% endif %}
<tr class="row1">
<td>People who must be present:</td>
<td>Participants who must be present:</td>
<td>{% if session.bethere %}<ul>{% for person in session.bethere %}<li>{{ person }}</li>{% endfor %}</ul>{% else %}<i>None</i>{% endif %}</td>
</tr>
<tr class="row2">
@ -70,4 +70,4 @@
</tr>
{% endif %}
</tbody>
</table>
</table>

View file

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

View file

@ -97,6 +97,11 @@ pre {
width: 60px;
}
// Style preformatted alert messages better.
.preformatted {
white-space: pre-line;
}
.leftmenu {
width: 13em;

View file

@ -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;
global.dayGridPlugin = dayGridPlugin;
global.bootstrap5Plugin = bootstrap5Plugin;

View file

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

View file

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

View file

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

View file

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

View file

@ -105,12 +105,12 @@
</div>
</noscript>
{% for message in messages %}
<div class="alert {% if message.level_tag %} alert-{% if message.level_tag == 'error' %}danger{% else %}{{ message.level_tag }}{% endif %} {% endif %} {% if message.extra_tags %} {{ message.extra_tags }}{% endif %} alert-dismissable fade show">
<div class="alert {% if message.level_tag %} alert-{% if message.level_tag == 'error' %}danger{% else %}{{ message.level_tag }}{% endif %} {% endif %} alert-dismissable fade show">
<button type="button"
class="btn-close float-end"
data-bs-dismiss="alert"
aria-label="Close"></button>
{{ message.message }}
<div{% if message.extra_tags %} class="{{ message.extra_tags }}"{% endif %}>{{ message.message }}</div>
</div>
{% endfor %}
{% block content %}{{ content|safe }}{% endblock %}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<reference anchor="{{doc_bibtype}}.{{name|slice:"6:"}}">
<reference anchor="{{doc_bibtype}}.{{name|slice:"6:"}}" target="{{ settings.IDTRACKER_BASE_URL }}{% url "ietf.doc.views_doc.document_html" name=doc.name rev=doc.rev %}">
<front>
<title>{{doc.title}}</title>{% for author in doc.documentauthor_set.all %}
<author initials="{{ author.person.initials }}" surname="{{ author.person.last_name }}" fullname="{{ author.person.name }}">

View file

@ -232,7 +232,7 @@
<a class="btn btn-sm btn-warning mb-3"
target="_blank"
href="https://github.com/ietf-tools/datatracker/issues/new/choose">
Report a bug
Report a datatracker bug
<i class="bi bi-bug"></i>
</a>
</div>

View file

@ -26,9 +26,17 @@
{% endif %}
{% if document_html %}<br>{% endif %}
{% if has_verified_errata or has_errata %}
<a class="{% if document_html %}btn btn-danger btn-sm my-1{% else %}badge rounded-pill bg-danger text-decoration-none text-light{% endif %}"
<a class="{% if document_html %}btn btn-primary btn-sm my-1{% else %}badge rounded-pill bg-danger text-decoration-none text-light{% endif %}"
href="https://www.rfc-editor.org/errata_search.php?rfc={{ doc.rfc_number }}" title="Click to view errata." rel="nofollow">
Errata
{% if document_html %}View errata{% else %}Errata{% endif %}
</a>
{% endif %}
{% if document_html and doc.get_state_slug == "rfc" and not snapshot %}
<a class="btn btn-sm btn-warning"
title="Click to report an error in the document."
href="https://www.rfc-editor.org/errata.php#reportnew"
target="_blank">
Report errata
</a>
{% endif %}
{% if doc.related_ipr %}

View file

@ -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 <TO BE ADDED BY THE AD>.'
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%}

View file

@ -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 @@
</li>
{% endfor %}
</ul>
<p>
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.
</p>
<div class="row mt-3">
<div class="col-sm chart text-center" data-area="ietf" data-dataset="docs">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="col-sm chart text-center" data-area="ietf" data-dataset="pages">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
{% for area in areas %}
<h2 class="mt-3" id="id-{{ area.acronym|slugify }}">
<h2 class="mt-5" id="id-{{ area.acronym|slugify }}">
{{ area.name }}
<a href="{% url 'ietf.group.views.active_groups' group_type='wg' %}#{{ area.acronym }}">({{ area.acronym|upper }})</a>
</h2>
@ -53,5 +69,14 @@
{{ area.description|urlize_ietf_docs|linkify|safe }}
</p>
{% endif %}
{% include "group/group_stats_modal.html" with group=area only %}
{% endfor %}
{% endblock %}
{% block js %}
<script src="{% static 'ietf/js/highcharts.js' %}"></script>
<script>
$(function () {
group_stats("{% url 'ietf.group.views.group_stats_data' %}", ".chart");
});
</script>
{% endblock %}

View file

@ -211,6 +211,18 @@ height: 100vh;
</tr>
{% endif %}
{% endwith %}
{% if group.type.slug == "area" %}
<tr>
<td></td>
<th scope="row">
Group statistics
</th>
<td class="edit"></td>
<td>
{% include "group/group_stats_modal.html" with group=group only %}
</td>
</tr>
{% endif %}
</tbody>
{% if group.personnel %}
<tbody class="meta border-top">
@ -393,4 +405,10 @@ height: 100vh;
{% block js %}
<script src="{% static 'ietf/js/d3.js' %}"></script>
<script src="{% static 'ietf/js/document_relations.js' %}"></script>
<script src="{% static 'ietf/js/highcharts.js' %}"></script>
<script>
$(function () {
group_stats("{% url 'ietf.group.views.group_stats_data' %}", ".chart");
});
</script>
{% endblock %}

View file

@ -0,0 +1,45 @@
<button type="button"
class="btn btn-sm btn-primary"
data-bs-toggle="modal"
data-bs-target="#stats-modal-{{ group.acronym|slugify }}">
<i class="bi bi-pie-chart-fill"></i> Show {{ group.acronym|upper }} statistics
</button>
<div class="modal fade"
id="stats-modal-{{ group.acronym|slugify }}"
tabindex="-1"
aria-labelledby="stats-modal-label-{{ group.acronym|slugify }}"
aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable modal-xl">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5"
id="stats-modal-label-{{ group.acronym|slugify }}">{{ group.acronym|upper }} statistics</h1>
<button type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-lg chart text-center"
data-area="{{ group.acronym|lower }}"
data-dataset="docs">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="col-lg chart text-center"
data-area="{{ group.acronym|lower }}"
data-dataset="pages">
<div class="spinner-border" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

View file

@ -13,6 +13,7 @@
<br>
<small class="text-muted">{% include 'liaisons/liaison_title.html' %}</small>
</h1>
{% include "liaisons/liaison_desc.html" only %}
{% include "liaisons/detail_tabs.html" %}
<table class="table table-sm table-striped">
<tbody>

View file

@ -11,6 +11,7 @@
{% block content %}
{% origin %}
<h1>Liaison Statements</h1>
{% include "liaisons/liaison_desc.html" only %}
{% if with_search %}
<div class="ietf-box search-form-box">{% include "liaisons/search_form.html" %}</div>
{% endif %}

View file

@ -0,0 +1,4 @@
<div class="alert alert-info">
Additional information about IETF liaison relationships is available on the
<a href="https://www.iab.org/liaisons/" target="_blank">Internet Architecture Board liaison webpage</a>.
</div>

View file

@ -13,11 +13,15 @@ DTEND{% ics_date_time item.timeslot.local_end_time schedule.meeting.time_zone %}
DTSTAMP{% ics_date_time item.timeslot.modified|utc 'utc' %}{% if item.session.agenda %}
URL:{{item.session.agenda.get_versionless_href}}{% endif %}
DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %}
Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% if item.timeslot.location.webex_url %}
Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% if item.timeslot.location.onsite_tool_url %}
\n
Webex: {{ item.timeslot.location.webex_url }}\n{% endif %}{% if item.timeslot.location.video_stream_url %}
Onsite tool: {{ item.timeslot.location.onsite_tool_url|format:item.session }}\n{% endif %}{% if item.timeslot.location.video_stream_url %}
\n
Meetecho: {{ item.timeslot.location.video_stream_url|format:item.session }}\n{% endif %}{% if item.session.agenda %}{% with agenda=item.session.agenda %}
Meetecho: {{ item.timeslot.location.video_stream_url|format:item.session }}\n{% endif %}{% if item.timeslot.location.webex_url %}
\n
Webex: {{ item.timeslot.location.webex_url }}\n{% endif %}{% if item.session.remote_instructions %}
\n
Remote instructions: {{ item.session.remote_instructions }}\n{% endif %}{% if item.session.agenda %}{% with agenda=item.session.agenda %}
\n
{{agenda.type}} {{agenda.get_versionless_href}}\n{% endwith %}{% endif %}
\n

View file

@ -12,7 +12,7 @@
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-secondary float-end"
href="{% url 'ietf.meeting.views.edit_timeslots' num=meeting.number %}">Back</a>
href="{% url 'ietf.meeting.views.edit_timeslots' num=meeting.number %}{% if 'sched' in request.GET %}?sched={{ request.GET.sched }}{% endif %}">Back</a>
</form>
{% endblock %}
{% block js %}

View file

@ -40,22 +40,22 @@
<div class="my-3">
{% if can_edit_properties %}
<a class="btn btn-primary"
href="{% url "ietf.meeting.views.edit_schedule_properties" schedule.meeting.number schedule.owner_email schedule.name %}">
href="{% url 'ietf.meeting.views.edit_schedule_properties' schedule.meeting.number schedule.owner_email schedule.name %}">
Edit properties
</a>
{% endif %}
{% if user|has_role:"Secretariat" %}
<a class="btn btn-primary"
href="{% url "ietf.meeting.views.edit_timeslots" num=meeting.number %}">
href="{% url 'ietf.meeting.views.edit_timeslots' num=meeting.number %}?sched={{ schedule.pk }}">
Edit timeslots
</a>
{% endif %}
<a class="btn btn-primary"
href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">
href="{% url 'ietf.meeting.views.new_meeting_schedule' num=meeting.number owner=schedule.owner_email name=schedule.name %}">
Copy agenda
</a>
<a class="btn btn-primary"
href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">
href="{% url 'ietf.meeting.views.list_schedules' num=meeting.number %}">
Other agendas
</a>
</div>
@ -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
<a href="{% url "ietf.meeting.views.new_meeting_schedule" num=meeting.number owner=schedule.owner_email name=schedule.name %}">
<a href="{% url 'ietf.meeting.views.new_meeting_schedule' num=meeting.number owner=schedule.owner_email name=schedule.name %}">
new agenda from this</a>.
</div>
{% endif %}
@ -74,7 +74,8 @@
No timeslots exist for this meeting yet.
</p>
<p>
<a class="btn btn-primary" href="{% url "ietf.meeting.views.edit_timeslots" num=meeting.number %}">
<a class="btn btn-primary"
href="{% url 'ietf.meeting.views.edit_timeslots' num=meeting.number %}?sched={{ schedule.pk }}">
Edit timeslots
</a>
</p>

View file

@ -104,7 +104,7 @@
{% endfor %}
{% if secretariat %}
<a class="btn btn-primary btn-sm mt-2"
href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}">
href="{% url 'ietf.meeting.views.edit_session' session_id=session.pk %}?sched={{ schedule.pk }}">
Edit session
</a>
<a class="btn btn-danger btn-sm mt-2"

View file

@ -15,10 +15,7 @@
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-secondary float-end"
href="{% url 'ietf.meeting.views.edit_meeting_schedule' num=session.meeting.number %}">
Back
</a>
<a class="btn btn-secondary float-end" href="{{ editor_url }}">Back</a>
</form>
{% endblock %}
{% block js %}{{ form.media.js }}{% endblock %}

View file

@ -25,7 +25,7 @@
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-secondary float-end"
href="{% url 'ietf.meeting.views.edit_timeslots' num=timeslot.meeting.number %}">
href="{% url 'ietf.meeting.views.edit_timeslots' num=timeslot.meeting.number %}{% if "sched" in request.GET %}?sched={{ request.GET.sched }}{% endif %}">
Back
</a>
</form>

View file

@ -2,11 +2,13 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin %}
{% load ietf_filters static %}
{% load cache %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
{% endblock %}
{% block title %}Past Meetings{% endblock %}
{% block content %}
{% cache 3600 pastmeetings %}
{% origin %}
<h1>Past Meetings</h1>
{% if meetings %}
@ -55,6 +57,7 @@
{% else %}
<h2>No past meetings</h2>
{% endif %}
{% endcache %}
{% endblock %}
{% block js %}
<script src="{% static "ietf/js/list.js" %}"></script>

View file

@ -1,7 +1,8 @@
{% extends "base.html" %}
{% load ams_filters ietf_filters %}
{% load ams_filters ietf_filters cache %}
{% block title %}IETF {{ meeting.number }} Proceedings - Progress Report{% endblock %}
{% block content %}
{% cache 3600 proceedings_progress_report meeting.number %}
<h1>
<a class="text-decoration-none text-reset"
href="{% url 'ietf.meeting.views.proceedings' num=meeting.number %}">
@ -67,4 +68,5 @@
</tbody>
{% endif %}
</table>
{% endcache %}
{% endblock %}

View file

@ -3,8 +3,8 @@
{% load origin ietf_filters static %}
{% block title %}{{ meeting }} : {{ acronym }}{% endblock %}
{% block morecss %}
.draggable {
cursor: pointer;
.drag-handle {
cursor: move;
}
{% endblock %}
{% block content %}
@ -64,6 +64,7 @@
var options = {
group: "slides",
animation: 150,
handle: ".drag-handle",
onAdd: function(event) {onAdd(event)},
onRemove: function(event) {onRemove(event)},
onEnd: function(event) {onEnd(event)}

View file

@ -137,16 +137,11 @@
data-remove-from-session="{% url 'ietf.meeting.views.ajax_remove_slides_from_session' session_id=session.pk num=session.meeting.number %}"
data-reorder-in-session="{% url 'ietf.meeting.views.ajax_reorder_slides_in_session' session_id=session.pk num=session.meeting.number %}">
{% for pres in session.filtered_slides %}
<tr data-name="{{ pres.document.name }}"
{% if can_manage_materials %}
class="draggable"
title="Drag to reorder."
{% endif %}
>
<tr data-name="{{ pres.document.name }}" {% if can_manage_materials %} class="draggable" {% endif %}>
{% url 'ietf.doc.views_doc.document_main' name=pres.document.name as url %}
{% if can_manage_materials %}
<td>
<i class="bi bi-grip-vertical"></i>
<i class="bi bi-grip-vertical drag-handle" title="Drag to reorder"></i>
</td>
{% endif %}
<td>

View file

@ -35,7 +35,7 @@ a.new-timeslot-link { color: lightgray; font-size: large;}
</h1>
<div class="my-3">
<a class="btn btn-primary"
href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}">
href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}{% if "sched" in request.GET %}?sched={{ request.GET.sched }}{% endif %}">
New timeslot
</a>
{% if meeting.schedule %}
@ -51,6 +51,9 @@ a.new-timeslot-link { color: lightgray; font-size: large;}
href="{% url "ietf.meeting.views.list_schedules" num=meeting.number %}">
Agenda list
</a>
{% if schedule_edit_url %}
<a class="btn btn-secondary" href="{{ schedule_edit_url }}">Back to agenda</a>
{% endif %}
</div>
<div class="timeslot-edit">
{% if rooms|length == 0 %}
@ -126,7 +129,9 @@ a.new-timeslot-link { color: lightgray; font-size: large;}
<p>
No timeslots exist for this meeting yet.
</p>
<a href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}">Create a timeslot.</a>
<a href="{% url "ietf.meeting.views.create_timeslot" num=meeting.number %}{% if "sched" in request.GET %}?sched={{ request.GET.sched }}{% endif %}">
Create a timeslot.
</a>
</td>
{% else %}
{% for day in time_slices %}
@ -137,11 +142,11 @@ a.new-timeslot-link { color: lightgray; font-size: large;}
<td class="tscell {% if cell_ts|length > 1 %}timeslot-collision{% endif %}{% for ts in cell_ts %} tstype_{{ ts.type.slug }}{% endfor %}">
{% if cell_ts %}
{% for ts in cell_ts %}
{% include 'meeting/timeslot_edit_timeslot.html' with ts=ts in_use=ts_with_any_assignments in_official_use=ts_with_official_assignments only %}
{% include 'meeting/timeslot_edit_timeslot.html' with ts=ts in_use=ts_with_any_assignments in_official_use=ts_with_official_assignments request=request only %}
{% endfor %}
{% endif %}
<a class="new-timeslot-link text-reset text-decoration-none {% if cell_ts %}hidden{% endif %}" aria-label="+"
href="{% url 'ietf.meeting.views.create_timeslot' num=meeting.number %}?day={{ day.toordinal }}&amp;date={{ day|date:'Y-m-d' }}&amp;location={{ room.pk }}&amp;time={{ slot.time|date:'H:i' }}&amp;duration={{ slot.duration }}">
href="{% url 'ietf.meeting.views.create_timeslot' num=meeting.number %}?day={{ day.toordinal }}&amp;date={{ day|date:'Y-m-d' }}&amp;location={{ room.pk }}&amp;time={{ slot.time|date:'H:i' }}&amp;duration={{ slot.duration }}{% if "sched" in request.GET %}&amp;sched={{ request.GET.sched }}{% endif %}">
<i class="bi bi-plus-square"></i>
</a>
</td>

View file

@ -16,7 +16,7 @@ data-official-use="{% if ts in in_official_use %}true{% else %}false{% endif %}"
</span>
</div>
<div class="timeslot-buttons">
<a class="text-reset text-decoration-none" aria-label="Edit" href="{% url 'ietf.meeting.views.edit_timeslot' num=ts.meeting.number slot_id=ts.id %}">
<a class="text-reset text-decoration-none" aria-label="Edit" href="{% url 'ietf.meeting.views.edit_timeslot' num=ts.meeting.number slot_id=ts.id %}{% if "sched" in request.GET %}?sched={{ request.GET.sched }}{% endif %}">
<i class="bi bi-pencil"></i>
</a>
<i class="bi bi-trash delete-button" data-delete-scope="timeslot" title="Delete this timeslot"></i>

View file

@ -5,7 +5,6 @@
{% load ietf_filters static classname tz %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
<link rel="stylesheet" href="{% static "ietf/js/fullcalendar.css" %}">
{% endblock %}
{% block title %}Upcoming Meetings{% endblock %}
{% block content %}

View file

@ -1,4 +1,4 @@
{% load humanize tz %}{% autoescape off %}{% load ietf_filters %}BEGIN:VCALENDAR
{% load humanize tz %}{% autoescape off %}{% load ietf_filters textfilters %}BEGIN:VCALENDAR
VERSION:2.0
METHOD:PUBLISH
PRODID:-//IETF//datatracker.ietf.org ical upcoming//EN
@ -10,14 +10,15 @@ SUMMARY:{% if item.session.name %}{{item.session.group.acronym|lower}} - {{item.
CLASS:PUBLIC
DTSTART{% ics_date_time item.timeslot.local_start_time item.schedule.meeting.time_zone %}
DTEND{% ics_date_time item.timeslot.local_end_time item.schedule.meeting.time_zone %}
DTSTAMP{% ics_date_time item.timeslot.modified|utc 'utc' %}
{% if item.session.agenda %}URL:{{item.session.agenda.get_href}}
DESCRIPTION:{{item.timeslot.name|ics_esc}}\n{% if item.session.agenda_note %}
DTSTAMP{% ics_date_time item.timeslot.modified|utc 'utc' %}{% if item.session.agenda %}
URL:{{item.session.agenda.get_href}}{% endif %}
DESCRIPTION:{% if item.timeslot.name %}{{item.timeslot.name|ics_esc}}\n{% endif %}{% if item.session.agenda_note %}
Note: {{item.session.agenda_note|ics_esc}}\n{% endif %}{% for material in item.session.materials.all %}
\n{{material.type}}{% if material.type.name != "Agenda" %}
({{material.title|ics_esc}}){% endif %}:
{{material.get_href}}\n{% endfor %}
{% endif %}END:VEVENT
{{material.get_href}}\n{% endfor %}{% if item.session.remote_instructions %}
Remote instructions: {{ item.session.remote_instructions }}\n{% endif %}
END:VEVENT
{% endfor %}{% for meeting in ietfs %}BEGIN:VEVENT
UID:ietf-{{ meeting.number }}
SUMMARY:IETF {{ meeting.number }}{% if meeting.city %}

View file

@ -200,7 +200,7 @@ class SearchableField(forms.MultipleChoiceField):
# model = None # must be filled in by subclass
model = None # type:Optional[Type[models.Model]]
# max_entries = None # may be overridden in __init__
max_entries = None # type: Optional[int]
max_entries = None # type: Optional[int]
min_search_length = None # type: Optional[int]
default_hint_text = 'Type a value to search'

View file

@ -1,6 +1,6 @@
<?xml version='1.0'?>
<?xml-stylesheet type='text/xsl' href='rfc2629.xslt' ?>
<rfc category="exp" submissionType="independent" ipr="trust200902" docName="draft-test-references-00" version="3">
<rfc xmlns:xi="http://www.w3.org/2001/XInclude" category="exp" submissionType="independent" ipr="trust200902" docName="draft-test-references-00" version="3">
<front>
<title>Test Draft with References</title>
<author fullname="Alfred Person" initials="A." surname="Person" role="editor">
@ -37,9 +37,25 @@
<seriesInfo name="RFC" value="1"/>
<seriesInfo name="DOI" value="10.17487/RFC0001"/>
</reference>
<reference anchor="MNEMONIC" target="https://www.rfc-editor.org/info/rfc2">
<front>
<title>Cloud Software</title>
<author initials="D." surname="Crocker" fullname="D. Crocker">
<organization/>
</author>
<date year="1969" month="April"/>
</front>
<seriesInfo name="RFC" value="2"/>
<seriesInfo name="DOI" value="10.17487/RFC0002"/>
</reference>
</references>
<references>
<name>Informative References</name>
<xi:include href='https://xml.resource.org/public/rfc/bibxml3/reference.I-D.ietf-teas-pcecc-use-cases-00.xml'/>
<xi:include href='https://xml.resource.org/public/rfc/bibxml3/reference.I-D.ietf-teas-pcecc-use-cases.xml'/>
<xi:include href="https://bib.ietf.org/public/rfc/bibxml3/reference.I-D.draft-ietf-sipcore-multiple-reasons-00.xml" />
<xi:include href="https://bib.ietf.org/public/rfc/bibxml3/reference.I-D.draft-ietf-sipcore-multiple-reasons.xml" />
<xi:include href="https://xml2rfc.ietf.org/public/rfc/bibxml9/reference.BCP.0014.xml" />
<reference anchor='RFC0255' target='https://www.rfc-editor.org/info/rfc255'>
<front>
<title>Status of network hosts</title>
@ -51,6 +67,34 @@
<seriesInfo name='RFC' value='255'/>
<seriesInfo name='DOI' value='10.17487/RFC0255'/>
</reference>
<reference anchor="CONSISTENCY">
<front>
<title>Key Consistency and Discovery</title>
<author fullname="Alex Davidson" initials="A." surname="Davidson">
<organization>Brave Software</organization>
</author>
<author fullname="Matthew Finkel" initials="M." surname="Finkel">
<organization>The Tor Project</organization>
</author>
<author fullname="Martin Thomson" initials="M." surname="Thomson">
<organization>Mozilla</organization>
</author>
<author fullname="Christopher A. Wood" initials="C. A." surname="Wood">
<organization>Cloudflare</organization>
</author>
<date day="17" month="August" year="2022"/>
<abstract>
<t> This document describes the key consistency and correctness
requirements of protocols such as Privacy Pass, Oblivious DoH, and
Oblivious HTTP for user privacy. It discusses several mechanisms and
proposals for enabling user privacy in varying threat models. In
concludes with discussion of open problems in this area.
</t>
</abstract>
</front>
<seriesInfo name="Internet-Draft" value="draft-wood-key-consistency-03"/>
</reference>
<referencegroup anchor="bcp6">
<reference anchor='RFC1930' target='https://www.rfc-editor.org/info/rfc1930'>
<front>
@ -191,4 +235,4 @@
</reference>
</references>
</back>
</rfc>
</rfc>

View file

@ -374,10 +374,17 @@ class XMLDraftTests(TestCase):
draft.get_refs(),
{
'rfc1': XMLDraft.REF_TYPE_NORMATIVE,
'rfc2': XMLDraft.REF_TYPE_NORMATIVE,
'draft-wood-key-consistency-03': XMLDraft.REF_TYPE_INFORMATIVE,
'rfc255': XMLDraft.REF_TYPE_INFORMATIVE,
'bcp6': XMLDraft.REF_TYPE_INFORMATIVE,
'bcp14': XMLDraft.REF_TYPE_INFORMATIVE,
'rfc1207': XMLDraft.REF_TYPE_UNKNOWN,
'rfc4086': XMLDraft.REF_TYPE_NORMATIVE,
'draft-ietf-teas-pcecc-use-cases-00': XMLDraft.REF_TYPE_INFORMATIVE,
'draft-ietf-teas-pcecc-use-cases': XMLDraft.REF_TYPE_INFORMATIVE,
'draft-ietf-sipcore-multiple-reasons-00': XMLDraft.REF_TYPE_INFORMATIVE,
'draft-ietf-sipcore-multiple-reasons': XMLDraft.REF_TYPE_INFORMATIVE,
}
)

View file

@ -60,17 +60,47 @@ class XMLDraft(Draft):
tree.tree = v2v3.convert2to3()
return tree, xml_version
def _document_name(self, anchor):
"""Guess document name from reference anchor
def _document_name(self, ref):
"""Get document name from reference."""
series = ["rfc", "bcp", "fyi", "std"]
# handle xinclude first
# FIXME: this assumes the xinclude is a bibxml href; if it isn't, there can
# still be false negatives. it would be better to expand the xinclude and parse
# its seriesInfo.
if ref.tag.endswith("}include"):
name = re.search(
rf"reference\.({'|'.join(series).upper()})\.(\d{{4}})\.xml",
ref.attrib["href"],
)
if name:
return f"{name.group(1)}{int(name.group(2))}".lower()
name = re.search(
r"reference\.I-D\.(?:draft-)?(.*)\.xml", ref.attrib["href"]
)
if name:
return f"draft-{name.group(1)}"
# can't extract the name, give up
return ""
Looks for series numbers and removes leading 0s from the number.
"""
anchor = anchor.lower() # always give back lowercase
label = anchor.rstrip('0123456789') # remove trailing digits
if label in ['rfc', 'bcp', 'fyi', 'std']:
number = int(anchor[len(label):])
return f'{label}{number}'
return anchor
# check the anchor next
anchor = ref.get("anchor").lower() # always give back lowercase
label = anchor.rstrip("0123456789") # remove trailing digits
if label in series:
number = int(anchor[len(label) :])
return f"{label}{number}"
# if we couldn't find a match so far, try the seriesInfo
series_query = " or ".join(f"@name='{x.upper()}'" for x in series)
for info in ref.xpath(
f"./seriesInfo[{series_query} or @name='Internet-Draft']"
):
if not info.attrib["value"]:
continue
if info.attrib["name"] == "Internet-Draft":
return info.attrib["value"]
else:
return f'{info.attrib["name"].lower()}{info.attrib["value"]}'
return ""
def _reference_section_type(self, section_name):
"""Determine reference type from name of references section"""
@ -154,10 +184,20 @@ class XMLDraft(Draft):
"""Extract references from the draft"""
refs = {}
# accept nested <references> sections
for section in self.xmlroot.findall('back//references'):
ref_type = self._reference_section_type(self._reference_section_name(section))
for ref in (section.findall('./reference') + section.findall('./referencegroup')):
refs[self._document_name(ref.get('anchor'))] = ref_type
for section in self.xmlroot.findall("back//references"):
ref_type = self._reference_section_type(
self._reference_section_name(section)
)
for ref in (
section.findall("./reference")
+ section.findall("./referencegroup")
+ section.findall(
"./xi:include", {"xi": "http://www.w3.org/2001/XInclude"}
)
):
name = self._document_name(ref)
if name:
refs[name] = ref_type
return refs

View file

@ -7,14 +7,14 @@
"legacy:build": "parcel build"
},
"dependencies": {
"@fullcalendar/bootstrap5": "5.11.4",
"@fullcalendar/core": "5.11.4",
"@fullcalendar/daygrid": "5.11.4",
"@fullcalendar/interaction": "5.11.4",
"@fullcalendar/list": "5.11.4",
"@fullcalendar/luxon2": "5.11.4",
"@fullcalendar/timegrid": "5.11.4",
"@fullcalendar/vue3": "5.11.4",
"@fullcalendar/bootstrap5": "6.1.4",
"@fullcalendar/core": "6.1.4",
"@fullcalendar/daygrid": "6.1.4",
"@fullcalendar/interaction": "6.1.4",
"@fullcalendar/list": "6.1.4",
"@fullcalendar/luxon2": "6.1.4",
"@fullcalendar/timegrid": "6.1.4",
"@fullcalendar/vue3": "6.1.4",
"@popperjs/core": "2.11.6",
"@twuni/emojify": "1.0.2",
"bootstrap": "5.2.3",

151
yarn.lock
View file

@ -121,98 +121,79 @@ __metadata:
languageName: node
linkType: hard
"@fullcalendar/bootstrap5@npm:5.11.4":
version: 5.11.4
resolution: "@fullcalendar/bootstrap5@npm:5.11.4"
dependencies:
"@fullcalendar/common": ~5.11.4
tslib: ^2.1.0
checksum: 26f838f30425ddd629026d7d6e8c4bf1b016b87f7b9ee1bfafa6d4ee536f140aee64b8cdc159ec338a8789d3353baa5e7e725b509389cfe4a47d20bc87a93009
"@fullcalendar/bootstrap5@npm:6.1.4":
version: 6.1.4
resolution: "@fullcalendar/bootstrap5@npm:6.1.4"
peerDependencies:
"@fullcalendar/core": ~6.1.4
checksum: e4a5dd281d95d0cd24ee8ecb6b0454b1a081c5236cb9ac8d2b0ddac777160d0bd31a7f489368c7d0c33a1ac026cec9e8d62cc4e7f1980a11d84165a271860371
languageName: node
linkType: hard
"@fullcalendar/common@npm:~5.11.4":
version: 5.11.4
resolution: "@fullcalendar/common@npm:5.11.4"
"@fullcalendar/core@npm:6.1.4":
version: 6.1.4
resolution: "@fullcalendar/core@npm:6.1.4"
dependencies:
tslib: ^2.1.0
checksum: 8fc0e05539ba83d310eb5a7163dcd10582d83465393cccb525022b20c954e29e6361b289a2d2eec1ae5b5d950700333739b81cb5e81bc8cb72f682484ca697af
languageName: node
linkType: hard
"@fullcalendar/core@npm:5.11.4, @fullcalendar/core@npm:~5.11.4":
version: 5.11.4
resolution: "@fullcalendar/core@npm:5.11.4"
dependencies:
"@fullcalendar/common": ~5.11.4
preact: ^10.0.5
tslib: ^2.1.0
checksum: 11652a58dc4a7af2b9c552ca71e4215c56d574f7d639deab0a6604afa0a67c7bfef445d5a6e364b6bc6b0ffb333ba9e7730e184d480e0113dce90c470d988d17
checksum: 3c659bbaf814a1f084ddd3e2eeaf68bb48f268a36560abb399c4aa29a40d6017c5bb1cd87c43664f333a1f6682c32b6a85ffb661538ea0faa78a1c119760a381
languageName: node
linkType: hard
"@fullcalendar/daygrid@npm:5.11.4, @fullcalendar/daygrid@npm:~5.11.4":
version: 5.11.4
resolution: "@fullcalendar/daygrid@npm:5.11.4"
dependencies:
"@fullcalendar/common": ~5.11.4
tslib: ^2.1.0
checksum: a25d83cfe5b3ac3feeb49af47701c54e858d30b0b53871df2a83aa7edbcc7d49f435d59fdbf3d6e18b5699caced8133e9c4b1c919caca2c8717bd92f57c08ab4
languageName: node
linkType: hard
"@fullcalendar/interaction@npm:5.11.4":
version: 5.11.4
resolution: "@fullcalendar/interaction@npm:5.11.4"
dependencies:
"@fullcalendar/common": ~5.11.4
tslib: ^2.1.0
checksum: 88231b925498b947f5af98fcabf564f7d72ecde6660696e5aec7aa5c4aca7988deab74c9486a30e0e461cdd31913c560bb016f54a61641d13359488e845e053f
languageName: node
linkType: hard
"@fullcalendar/list@npm:5.11.4":
version: 5.11.4
resolution: "@fullcalendar/list@npm:5.11.4"
dependencies:
"@fullcalendar/common": ~5.11.4
tslib: ^2.1.0
checksum: e2cec5e89c57836a9ca4db57fbe67e16f78d91191df39ac0b5d0f18c1fdac9763f04647ae42e5f2d31ffa3cd245423d3aec449ed6c84fafc0195e62079a4eedc
languageName: node
linkType: hard
"@fullcalendar/luxon2@npm:5.11.4":
version: 5.11.4
resolution: "@fullcalendar/luxon2@npm:5.11.4"
dependencies:
"@fullcalendar/common": ~5.11.4
tslib: ^2.1.0
"@fullcalendar/daygrid@npm:6.1.4, @fullcalendar/daygrid@npm:~6.1.4":
version: 6.1.4
resolution: "@fullcalendar/daygrid@npm:6.1.4"
peerDependencies:
"@fullcalendar/core": ~6.1.4
checksum: 24b3c6e521c5e5288cb05d34e080daace1138b4b421a5dd059938a8c9b7730374b7f07908984b124d48e810276a3c24f1e1c7b6080d375ca80e4d7ef387d7774
languageName: node
linkType: hard
"@fullcalendar/interaction@npm:6.1.4":
version: 6.1.4
resolution: "@fullcalendar/interaction@npm:6.1.4"
peerDependencies:
"@fullcalendar/core": ~6.1.4
checksum: 5e282ba36bfbc306e8e36ea88b9df0c5a11b7bcdf4c8952377396da24a3ca058b436a39e9002eba9fc1ca8ce70ca23ef2fd74ef08f48d4b8b45576f7e30a6bea
languageName: node
linkType: hard
"@fullcalendar/list@npm:6.1.4":
version: 6.1.4
resolution: "@fullcalendar/list@npm:6.1.4"
peerDependencies:
"@fullcalendar/core": ~6.1.4
checksum: 0338a8bb1546e103921d1d6a5baf54b9d43759d9aaf905357c2272e01cbd42e120812128b2f2661d0ef82abc0b8447b16e6f1618968487ffa4c6f0a570f907d5
languageName: node
linkType: hard
"@fullcalendar/luxon2@npm:6.1.4":
version: 6.1.4
resolution: "@fullcalendar/luxon2@npm:6.1.4"
peerDependencies:
"@fullcalendar/core": ~6.1.4
luxon: ^2.0.0
checksum: 503e3e32d27c1fbd95d0e7c9cbf89e751eaaf57920017672d3f716fa0ba919f618ccefb6d278c60ee72b9ac8756e1f57ad9148084ffa356bf51950f9e7ee8426
checksum: 577283ad7cd555c25143f64c865554aebaa957b0bc51bcece7114bb7282abb448a8450b23863fb6287c6e91f7f89cc261beb51c8e0542c5b0df6c853fe3bd9c3
languageName: node
linkType: hard
"@fullcalendar/timegrid@npm:5.11.4":
version: 5.11.4
resolution: "@fullcalendar/timegrid@npm:5.11.4"
"@fullcalendar/timegrid@npm:6.1.4":
version: 6.1.4
resolution: "@fullcalendar/timegrid@npm:6.1.4"
dependencies:
"@fullcalendar/common": ~5.11.4
"@fullcalendar/daygrid": ~5.11.4
tslib: ^2.1.0
checksum: 3a2fccac65198c5fffa53286fcbb2556b6e3885cfc6f7978b7ee21c57cbc2a58617e6c54eff977f5234163cef3281e50548327a06cf253acc43c6881bed00f2c
languageName: node
linkType: hard
"@fullcalendar/vue3@npm:5.11.4":
version: 5.11.4
resolution: "@fullcalendar/vue3@npm:5.11.4"
dependencies:
"@fullcalendar/core": ~5.11.4
tslib: ^2.1.0
"@fullcalendar/daygrid": ~6.1.4
peerDependencies:
"@fullcalendar/core": ~6.1.4
checksum: 1329b941f94e2c17a866e612e7d92adacbca0364ba7e76908caa2a936f526c0cde43fa050f571634dd2db2e779f1cbd3115b64cbc0b524d34f0c4d61243659b8
languageName: node
linkType: hard
"@fullcalendar/vue3@npm:6.1.4":
version: 6.1.4
resolution: "@fullcalendar/vue3@npm:6.1.4"
peerDependencies:
"@fullcalendar/core": ~6.1.4
vue: ^3.0.11
checksum: 3e0fc0423b396813ef1d6409951903bd1e411c0ab7e64aa7d28ba81e3066e50c7b24d224c14d873a0ffa0aacc94d4bc64097d4fb6083870f748a9502fefebc29
checksum: 3e11102fbf09e2e62d52c03f7fc42435c816faeefd57c1e551271af6bfdeb52be7380b6d9b199e652a742626c17a21227092b83039f30a48f76d9d26ffae9beb
languageName: node
linkType: hard
@ -6159,14 +6140,14 @@ browserlist@latest:
resolution: "root-workspace-0b6124@workspace:."
dependencies:
"@faker-js/faker": 7.6.0
"@fullcalendar/bootstrap5": 5.11.4
"@fullcalendar/core": 5.11.4
"@fullcalendar/daygrid": 5.11.4
"@fullcalendar/interaction": 5.11.4
"@fullcalendar/list": 5.11.4
"@fullcalendar/luxon2": 5.11.4
"@fullcalendar/timegrid": 5.11.4
"@fullcalendar/vue3": 5.11.4
"@fullcalendar/bootstrap5": 6.1.4
"@fullcalendar/core": 6.1.4
"@fullcalendar/daygrid": 6.1.4
"@fullcalendar/interaction": 6.1.4
"@fullcalendar/list": 6.1.4
"@fullcalendar/luxon2": 6.1.4
"@fullcalendar/timegrid": 6.1.4
"@fullcalendar/vue3": 6.1.4
"@parcel/optimizer-data-url": 2.8.3
"@parcel/transformer-inline-string": 2.8.3
"@parcel/transformer-sass": 2.8.3
@ -6815,7 +6796,7 @@ browserlist@latest:
languageName: node
linkType: hard
"tslib@npm:^2.1.0, tslib@npm:^2.4.0":
"tslib@npm:^2.4.0":
version: 2.4.0
resolution: "tslib@npm:2.4.0"
checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113