Merge remote-tracking branch 'ietf-tools/main' into feat/postgres
This commit is contained in:
commit
f010766654
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -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.
|
||||
|
|
8
.github/ISSUE_TEMPLATE/new-feature.yml
vendored
8
.github/ISSUE_TEMPLATE/new-feature.yml
vendored
|
@ -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
|
||||
|
|
10
.github/ISSUE_TEMPLATE/report-a-bug.yml
vendored
10
.github/ISSUE_TEMPLATE/report-a-bug.yml
vendored
|
@ -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**
|
||||
|
|
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
|
@ -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 }}
|
||||
|
||||
|
|
2
.github/workflows/ci-run-tests.yml
vendored
2
.github/workflows/ci-run-tests.yml
vendored
|
@ -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
193
.pnp.cjs
generated
|
@ -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"],\
|
||||
|
|
|
@ -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.
BIN
.yarn/cache/@fullcalendar-bootstrap5-npm-6.1.4-0bc121fab1-e4a5dd281d.zip
vendored
Normal file
BIN
.yarn/cache/@fullcalendar-bootstrap5-npm-6.1.4-0bc121fab1-e4a5dd281d.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@fullcalendar-core-npm-6.1.4-a304b2f512-3c659bbaf8.zip
vendored
Normal file
BIN
.yarn/cache/@fullcalendar-core-npm-6.1.4-a304b2f512-3c659bbaf8.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@fullcalendar-daygrid-npm-6.1.4-51af4b5615-24b3c6e521.zip
vendored
Normal file
BIN
.yarn/cache/@fullcalendar-daygrid-npm-6.1.4-51af4b5615-24b3c6e521.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@fullcalendar-interaction-npm-6.1.4-a5a798ee1e-5e282ba36b.zip
vendored
Normal file
BIN
.yarn/cache/@fullcalendar-interaction-npm-6.1.4-a5a798ee1e-5e282ba36b.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@fullcalendar-list-npm-6.1.4-9af3788481-0338a8bb15.zip
vendored
Normal file
BIN
.yarn/cache/@fullcalendar-list-npm-6.1.4-9af3788481-0338a8bb15.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@fullcalendar-luxon2-npm-6.1.4-09e1c45826-577283ad7c.zip
vendored
Normal file
BIN
.yarn/cache/@fullcalendar-luxon2-npm-6.1.4-09e1c45826-577283ad7c.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@fullcalendar-timegrid-npm-6.1.4-36b739a426-1329b941f9.zip
vendored
Normal file
BIN
.yarn/cache/@fullcalendar-timegrid-npm-6.1.4-36b739a426-1329b941f9.zip
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
.yarn/cache/@fullcalendar-vue3-npm-6.1.4-db9e1f7c10-3e11102fbf.zip
vendored
Normal file
BIN
.yarn/cache/@fullcalendar-vue3-npm-6.1.4-db9e1f7c10-3e11102fbf.zip
vendored
Normal file
Binary file not shown.
|
@ -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'
|
||||
|
|
|
@ -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/'
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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)),
|
||||
]
|
||||
|
|
|
@ -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})
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
#
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"}),
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -97,6 +97,11 @@ pre {
|
|||
width: 60px;
|
||||
}
|
||||
|
||||
// Style preformatted alert messages better.
|
||||
.preformatted {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.leftmenu {
|
||||
width: 13em;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
})();
|
||||
})();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 }}">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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%}
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
45
ietf/templates/group/group_stats_modal.html
Normal file
45
ietf/templates/group/group_stats_modal.html
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
4
ietf/templates/liaisons/liaison_desc.html
Normal file
4
ietf/templates/liaisons/liaison_desc.html
Normal 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>
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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)}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}&date={{ day|date:'Y-m-d' }}&location={{ room.pk }}&time={{ slot.time|date:'H:i' }}&duration={{ slot.duration }}">
|
||||
href="{% url 'ietf.meeting.views.create_timeslot' num=meeting.number %}?day={{ day.toordinal }}&date={{ day|date:'Y-m-d' }}&location={{ room.pk }}&time={{ slot.time|date:'H:i' }}&duration={{ slot.duration }}{% if "sched" in request.GET %}&sched={{ request.GET.sched }}{% endif %}">
|
||||
<i class="bi bi-plus-square"></i>
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
16
package.json
16
package.json
|
@ -7,14 +7,14 @@
|
|||
"legacy:build": "parcel build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullcalendar/bootstrap5": "5.11.4",
|
||||
"@fullcalendar/core": "5.11.4",
|
||||
"@fullcalendar/daygrid": "5.11.4",
|
||||
"@fullcalendar/interaction": "5.11.4",
|
||||
"@fullcalendar/list": "5.11.4",
|
||||
"@fullcalendar/luxon2": "5.11.4",
|
||||
"@fullcalendar/timegrid": "5.11.4",
|
||||
"@fullcalendar/vue3": "5.11.4",
|
||||
"@fullcalendar/bootstrap5": "6.1.4",
|
||||
"@fullcalendar/core": "6.1.4",
|
||||
"@fullcalendar/daygrid": "6.1.4",
|
||||
"@fullcalendar/interaction": "6.1.4",
|
||||
"@fullcalendar/list": "6.1.4",
|
||||
"@fullcalendar/luxon2": "6.1.4",
|
||||
"@fullcalendar/timegrid": "6.1.4",
|
||||
"@fullcalendar/vue3": "6.1.4",
|
||||
"@popperjs/core": "2.11.6",
|
||||
"@twuni/emojify": "1.0.2",
|
||||
"bootstrap": "5.2.3",
|
||||
|
|
151
yarn.lock
151
yarn.lock
|
@ -121,98 +121,79 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/bootstrap5@npm:5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/bootstrap5@npm:5.11.4"
|
||||
dependencies:
|
||||
"@fullcalendar/common": ~5.11.4
|
||||
tslib: ^2.1.0
|
||||
checksum: 26f838f30425ddd629026d7d6e8c4bf1b016b87f7b9ee1bfafa6d4ee536f140aee64b8cdc159ec338a8789d3353baa5e7e725b509389cfe4a47d20bc87a93009
|
||||
"@fullcalendar/bootstrap5@npm:6.1.4":
|
||||
version: 6.1.4
|
||||
resolution: "@fullcalendar/bootstrap5@npm:6.1.4"
|
||||
peerDependencies:
|
||||
"@fullcalendar/core": ~6.1.4
|
||||
checksum: e4a5dd281d95d0cd24ee8ecb6b0454b1a081c5236cb9ac8d2b0ddac777160d0bd31a7f489368c7d0c33a1ac026cec9e8d62cc4e7f1980a11d84165a271860371
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/common@npm:~5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/common@npm:5.11.4"
|
||||
"@fullcalendar/core@npm:6.1.4":
|
||||
version: 6.1.4
|
||||
resolution: "@fullcalendar/core@npm:6.1.4"
|
||||
dependencies:
|
||||
tslib: ^2.1.0
|
||||
checksum: 8fc0e05539ba83d310eb5a7163dcd10582d83465393cccb525022b20c954e29e6361b289a2d2eec1ae5b5d950700333739b81cb5e81bc8cb72f682484ca697af
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/core@npm:5.11.4, @fullcalendar/core@npm:~5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/core@npm:5.11.4"
|
||||
dependencies:
|
||||
"@fullcalendar/common": ~5.11.4
|
||||
preact: ^10.0.5
|
||||
tslib: ^2.1.0
|
||||
checksum: 11652a58dc4a7af2b9c552ca71e4215c56d574f7d639deab0a6604afa0a67c7bfef445d5a6e364b6bc6b0ffb333ba9e7730e184d480e0113dce90c470d988d17
|
||||
checksum: 3c659bbaf814a1f084ddd3e2eeaf68bb48f268a36560abb399c4aa29a40d6017c5bb1cd87c43664f333a1f6682c32b6a85ffb661538ea0faa78a1c119760a381
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/daygrid@npm:5.11.4, @fullcalendar/daygrid@npm:~5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/daygrid@npm:5.11.4"
|
||||
dependencies:
|
||||
"@fullcalendar/common": ~5.11.4
|
||||
tslib: ^2.1.0
|
||||
checksum: a25d83cfe5b3ac3feeb49af47701c54e858d30b0b53871df2a83aa7edbcc7d49f435d59fdbf3d6e18b5699caced8133e9c4b1c919caca2c8717bd92f57c08ab4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/interaction@npm:5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/interaction@npm:5.11.4"
|
||||
dependencies:
|
||||
"@fullcalendar/common": ~5.11.4
|
||||
tslib: ^2.1.0
|
||||
checksum: 88231b925498b947f5af98fcabf564f7d72ecde6660696e5aec7aa5c4aca7988deab74c9486a30e0e461cdd31913c560bb016f54a61641d13359488e845e053f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/list@npm:5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/list@npm:5.11.4"
|
||||
dependencies:
|
||||
"@fullcalendar/common": ~5.11.4
|
||||
tslib: ^2.1.0
|
||||
checksum: e2cec5e89c57836a9ca4db57fbe67e16f78d91191df39ac0b5d0f18c1fdac9763f04647ae42e5f2d31ffa3cd245423d3aec449ed6c84fafc0195e62079a4eedc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/luxon2@npm:5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/luxon2@npm:5.11.4"
|
||||
dependencies:
|
||||
"@fullcalendar/common": ~5.11.4
|
||||
tslib: ^2.1.0
|
||||
"@fullcalendar/daygrid@npm:6.1.4, @fullcalendar/daygrid@npm:~6.1.4":
|
||||
version: 6.1.4
|
||||
resolution: "@fullcalendar/daygrid@npm:6.1.4"
|
||||
peerDependencies:
|
||||
"@fullcalendar/core": ~6.1.4
|
||||
checksum: 24b3c6e521c5e5288cb05d34e080daace1138b4b421a5dd059938a8c9b7730374b7f07908984b124d48e810276a3c24f1e1c7b6080d375ca80e4d7ef387d7774
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/interaction@npm:6.1.4":
|
||||
version: 6.1.4
|
||||
resolution: "@fullcalendar/interaction@npm:6.1.4"
|
||||
peerDependencies:
|
||||
"@fullcalendar/core": ~6.1.4
|
||||
checksum: 5e282ba36bfbc306e8e36ea88b9df0c5a11b7bcdf4c8952377396da24a3ca058b436a39e9002eba9fc1ca8ce70ca23ef2fd74ef08f48d4b8b45576f7e30a6bea
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/list@npm:6.1.4":
|
||||
version: 6.1.4
|
||||
resolution: "@fullcalendar/list@npm:6.1.4"
|
||||
peerDependencies:
|
||||
"@fullcalendar/core": ~6.1.4
|
||||
checksum: 0338a8bb1546e103921d1d6a5baf54b9d43759d9aaf905357c2272e01cbd42e120812128b2f2661d0ef82abc0b8447b16e6f1618968487ffa4c6f0a570f907d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/luxon2@npm:6.1.4":
|
||||
version: 6.1.4
|
||||
resolution: "@fullcalendar/luxon2@npm:6.1.4"
|
||||
peerDependencies:
|
||||
"@fullcalendar/core": ~6.1.4
|
||||
luxon: ^2.0.0
|
||||
checksum: 503e3e32d27c1fbd95d0e7c9cbf89e751eaaf57920017672d3f716fa0ba919f618ccefb6d278c60ee72b9ac8756e1f57ad9148084ffa356bf51950f9e7ee8426
|
||||
checksum: 577283ad7cd555c25143f64c865554aebaa957b0bc51bcece7114bb7282abb448a8450b23863fb6287c6e91f7f89cc261beb51c8e0542c5b0df6c853fe3bd9c3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/timegrid@npm:5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/timegrid@npm:5.11.4"
|
||||
"@fullcalendar/timegrid@npm:6.1.4":
|
||||
version: 6.1.4
|
||||
resolution: "@fullcalendar/timegrid@npm:6.1.4"
|
||||
dependencies:
|
||||
"@fullcalendar/common": ~5.11.4
|
||||
"@fullcalendar/daygrid": ~5.11.4
|
||||
tslib: ^2.1.0
|
||||
checksum: 3a2fccac65198c5fffa53286fcbb2556b6e3885cfc6f7978b7ee21c57cbc2a58617e6c54eff977f5234163cef3281e50548327a06cf253acc43c6881bed00f2c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/vue3@npm:5.11.4":
|
||||
version: 5.11.4
|
||||
resolution: "@fullcalendar/vue3@npm:5.11.4"
|
||||
dependencies:
|
||||
"@fullcalendar/core": ~5.11.4
|
||||
tslib: ^2.1.0
|
||||
"@fullcalendar/daygrid": ~6.1.4
|
||||
peerDependencies:
|
||||
"@fullcalendar/core": ~6.1.4
|
||||
checksum: 1329b941f94e2c17a866e612e7d92adacbca0364ba7e76908caa2a936f526c0cde43fa050f571634dd2db2e779f1cbd3115b64cbc0b524d34f0c4d61243659b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@fullcalendar/vue3@npm:6.1.4":
|
||||
version: 6.1.4
|
||||
resolution: "@fullcalendar/vue3@npm:6.1.4"
|
||||
peerDependencies:
|
||||
"@fullcalendar/core": ~6.1.4
|
||||
vue: ^3.0.11
|
||||
checksum: 3e0fc0423b396813ef1d6409951903bd1e411c0ab7e64aa7d28ba81e3066e50c7b24d224c14d873a0ffa0aacc94d4bc64097d4fb6083870f748a9502fefebc29
|
||||
checksum: 3e11102fbf09e2e62d52c03f7fc42435c816faeefd57c1e551271af6bfdeb52be7380b6d9b199e652a742626c17a21227092b83039f30a48f76d9d26ffae9beb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -6159,14 +6140,14 @@ browserlist@latest:
|
|||
resolution: "root-workspace-0b6124@workspace:."
|
||||
dependencies:
|
||||
"@faker-js/faker": 7.6.0
|
||||
"@fullcalendar/bootstrap5": 5.11.4
|
||||
"@fullcalendar/core": 5.11.4
|
||||
"@fullcalendar/daygrid": 5.11.4
|
||||
"@fullcalendar/interaction": 5.11.4
|
||||
"@fullcalendar/list": 5.11.4
|
||||
"@fullcalendar/luxon2": 5.11.4
|
||||
"@fullcalendar/timegrid": 5.11.4
|
||||
"@fullcalendar/vue3": 5.11.4
|
||||
"@fullcalendar/bootstrap5": 6.1.4
|
||||
"@fullcalendar/core": 6.1.4
|
||||
"@fullcalendar/daygrid": 6.1.4
|
||||
"@fullcalendar/interaction": 6.1.4
|
||||
"@fullcalendar/list": 6.1.4
|
||||
"@fullcalendar/luxon2": 6.1.4
|
||||
"@fullcalendar/timegrid": 6.1.4
|
||||
"@fullcalendar/vue3": 6.1.4
|
||||
"@parcel/optimizer-data-url": 2.8.3
|
||||
"@parcel/transformer-inline-string": 2.8.3
|
||||
"@parcel/transformer-sass": 2.8.3
|
||||
|
@ -6815,7 +6796,7 @@ browserlist@latest:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tslib@npm:^2.1.0, tslib@npm:^2.4.0":
|
||||
"tslib@npm:^2.4.0":
|
||||
version: 2.4.0
|
||||
resolution: "tslib@npm:2.4.0"
|
||||
checksum: 8c4aa6a3c5a754bf76aefc38026134180c053b7bd2f81338cb5e5ebf96fefa0f417bff221592bf801077f5bf990562f6264fecbc42cd3309b33872cb6fc3b113
|
||||
|
|
Loading…
Reference in a new issue