From 0555eef13a105dbfd3cd9e19f93d6ce1fd7a41c7 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Sat, 20 Jul 2024 12:47:43 -0700
Subject: [PATCH 001/707] fix: prevent error when no file selected
---
ietf/meeting/views.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 2948a2e715..253f2852ff 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -2790,7 +2790,8 @@ def clean_content(self):
def clean_file(self):
submission_method = self.cleaned_data.get("submission_method")
if submission_method == "upload":
- return super().clean_file()
+ if self.cleaned_data.get("file", None) is not None:
+ return super().clean_file()
return None
def clean(self):
From c7f6bdef0a9427f479b0db806423e8d2d3782bb5 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Sat, 20 Jul 2024 13:02:32 -0700
Subject: [PATCH 002/707] test: test missing file
---
ietf/meeting/tests_views.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index d783ed9c75..db62fe6204 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -6246,6 +6246,12 @@ def test_upload_minutes_agenda(self):
q = PyQuery(r.content)
self.assertTrue(q('form input[type="checkbox"]'))
+ # test not submitting a file
+ r = self.client.post(url, dict(submission_method="upload"))
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertTrue(q("form .is-invalid"))
+
test_file = BytesIO(b'this is some text for a test')
test_file.name = "not_really.json"
r = self.client.post(url,dict(submission_method="upload",file=test_file))
From a86c62e133008e5fe2a5cce3b99a7329b53462b6 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Sat, 20 Jul 2024 15:17:43 -0700
Subject: [PATCH 003/707] fix: raw_id_fields for EventAdmin classes (#7711)
* fix: raw_id_fields for ReviewRequestDocEventAdmin
* fix: raw_id_fields for ReviewAssignmentDocEventAdmin
* fix: raw_id_fields for AddedMessageEventAdmin
* fix: raw_id_fields for SubmissionDocEventAdmin
* refactor: make inheritance clearer
---
ietf/doc/admin.py | 44 +++++++++++++++++++++++++++-----------------
1 file changed, 27 insertions(+), 17 deletions(-)
diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py
index 3ad4bee2af..301d32d7cc 100644
--- a/ietf/doc/admin.py
+++ b/ietf/doc/admin.py
@@ -142,6 +142,13 @@ class DocumentActionHolderAdmin(admin.ModelAdmin):
# events
+class DeletedEventAdmin(admin.ModelAdmin):
+ list_display = ['id', 'content_type', 'json', 'by', 'time']
+ list_filter = ['time']
+ raw_id_fields = ['content_type', 'by']
+admin.site.register(DeletedEvent, DeletedEventAdmin)
+
+
class DocEventAdmin(admin.ModelAdmin):
def event_type(self, obj):
return str(obj.type)
@@ -159,39 +166,42 @@ def short_desc(self, obj):
admin.site.register(StateDocEvent, DocEventAdmin)
admin.site.register(ConsensusDocEvent, DocEventAdmin)
admin.site.register(BallotDocEvent, DocEventAdmin)
+admin.site.register(IRSGBallotDocEvent, DocEventAdmin)
admin.site.register(WriteupDocEvent, DocEventAdmin)
admin.site.register(LastCallDocEvent, DocEventAdmin)
admin.site.register(TelechatDocEvent, DocEventAdmin)
-admin.site.register(ReviewRequestDocEvent, DocEventAdmin)
-admin.site.register(ReviewAssignmentDocEvent, DocEventAdmin)
admin.site.register(InitialReviewDocEvent, DocEventAdmin)
-admin.site.register(AddedMessageEvent, DocEventAdmin)
-admin.site.register(SubmissionDocEvent, DocEventAdmin)
admin.site.register(EditedAuthorsDocEvent, DocEventAdmin)
admin.site.register(IanaExpertDocEvent, DocEventAdmin)
-class DeletedEventAdmin(admin.ModelAdmin):
- list_display = ['id', 'content_type', 'json', 'by', 'time']
- list_filter = ['time']
- raw_id_fields = ['content_type', 'by']
-admin.site.register(DeletedEvent, DeletedEventAdmin)
-
class BallotPositionDocEventAdmin(DocEventAdmin):
- raw_id_fields = ["doc", "by", "balloter", "ballot"]
+ raw_id_fields = DocEventAdmin.raw_id_fields + ["balloter", "ballot"]
admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin)
-
-class IRSGBallotDocEventAdmin(DocEventAdmin):
- raw_id_fields = ["doc", "by"]
-admin.site.register(IRSGBallotDocEvent, IRSGBallotDocEventAdmin)
class BofreqEditorDocEventAdmin(DocEventAdmin):
- raw_id_fields = ["doc", "by", "editors" ]
+ raw_id_fields = DocEventAdmin.raw_id_fields + ["editors"]
admin.site.register(BofreqEditorDocEvent, BofreqEditorDocEventAdmin)
class BofreqResponsibleDocEventAdmin(DocEventAdmin):
- raw_id_fields = ["doc", "by", "responsible" ]
+ raw_id_fields = DocEventAdmin.raw_id_fields + ["responsible"]
admin.site.register(BofreqResponsibleDocEvent, BofreqResponsibleDocEventAdmin)
+class ReviewRequestDocEventAdmin(DocEventAdmin):
+ raw_id_fields = DocEventAdmin.raw_id_fields + ["review_request"]
+admin.site.register(ReviewRequestDocEvent, ReviewRequestDocEventAdmin)
+
+class ReviewAssignmentDocEventAdmin(DocEventAdmin):
+ raw_id_fields = DocEventAdmin.raw_id_fields + ["review_assignment"]
+admin.site.register(ReviewAssignmentDocEvent, ReviewAssignmentDocEventAdmin)
+
+class AddedMessageEventAdmin(DocEventAdmin):
+ raw_id_fields = DocEventAdmin.raw_id_fields + ["message"]
+admin.site.register(AddedMessageEvent, AddedMessageEventAdmin)
+
+class SubmissionDocEventAdmin(DocEventAdmin):
+ raw_id_fields = DocEventAdmin.raw_id_fields + ["submission"]
+admin.site.register(SubmissionDocEvent, SubmissionDocEventAdmin)
+
class DocumentUrlAdmin(admin.ModelAdmin):
list_display = ['id', 'doc', 'tag', 'url', 'desc', ]
search_fields = ['doc__name', 'url', ]
From 97837569928580fdbe4a1f40c24e2554396f16ea Mon Sep 17 00:00:00 2001
From: Robert Sparks
Date: Sat, 20 Jul 2024 17:23:32 -0500
Subject: [PATCH 004/707] Changed milestones to use RFC number if draft is
published as RFC. (#7718)
Co-authored-by: Tero Kivinen
---
ietf/templates/group/milestones.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/templates/group/milestones.html b/ietf/templates/group/milestones.html
index 1f259743fe..df03d370fd 100644
--- a/ietf/templates/group/milestones.html
+++ b/ietf/templates/group/milestones.html
@@ -49,7 +49,7 @@
| {{ milestone.desc|urlize_ietf_docs }} |
{% for d in milestone.docs.all %}
- {{ d.name }}
+ {% if d.became_rfc %}{{ d.became_rfc }} (was {% endif %}{{ d.name }}{% if d.became_rfc %}){% endif %}
{% endfor %}
|
From 60a3976c3df04d658e63f391bcc2c367cc340dbc Mon Sep 17 00:00:00 2001
From: Robert Sparks
Date: Sat, 20 Jul 2024 17:23:56 -0500
Subject: [PATCH 005/707] Fixed 'to' to 'review_to' and 'cc' to 'review_cc'.
(#7710)
Co-authored-by: Tero Kivinen
---
ietf/templates/doc/review/complete_review.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/templates/doc/review/complete_review.html b/ietf/templates/doc/review/complete_review.html
index 2319c40fd0..f30c641e46 100644
--- a/ietf/templates/doc/review/complete_review.html
+++ b/ietf/templates/doc/review/complete_review.html
@@ -66,7 +66,7 @@
If you enter the review below, the review will be sent
- to {% for addr in to %}{{ addr|linkify }}{% if not forloop.last %}, {% endif %}{% endfor %}{% if review_cc %}, with a CC to {% for addr in cc %}{{ addr|linkify }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endif %}.
+ to {% for addr in review_to %}{{ addr|linkify }}{% if not forloop.last %}, {% endif %}{% endfor %}{% if review_cc %}, with a CC to {% for addr in review_cc %}{{ addr|linkify }}{% if not forloop.last %}, {% endif %}{% endfor %}{% endif %}.
{% elif assignment %}
From c5ca0ea40575d93c0963480c410c2c278e9fa068 Mon Sep 17 00:00:00 2001
From: Ryan Cross
Date: Sat, 20 Jul 2024 15:24:14 -0700
Subject: [PATCH 006/707] fix: force choice of From address in Announcement
form. Fixes #7679. (#7720)
---
ietf/secr/announcement/forms.py | 7 +++++--
ietf/secr/announcement/tests.py | 2 +-
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/ietf/secr/announcement/forms.py b/ietf/secr/announcement/forms.py
index 3aacbfe622..3fe58bdaaa 100644
--- a/ietf/secr/announcement/forms.py
+++ b/ietf/secr/announcement/forms.py
@@ -42,8 +42,11 @@ def get_from_choices(user):
nomcom_choices = get_nomcom_choices(user)
if nomcom_choices:
addresses = list(addresses) + nomcom_choices
-
- return list(zip(addresses, addresses))
+
+ choices = list(zip(addresses, addresses))
+ if len(choices) > 1:
+ choices.insert(0, ('', '(Choose an option)'))
+ return choices
def get_nomcom_choices(user):
diff --git a/ietf/secr/announcement/tests.py b/ietf/secr/announcement/tests.py
index c50e997f97..c147c301b6 100644
--- a/ietf/secr/announcement/tests.py
+++ b/ietf/secr/announcement/tests.py
@@ -48,7 +48,7 @@ def test_main_announce_from(self):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
- self.assertEqual(len(q('#id_frm option')),3)
+ self.assertEqual(len(q('#id_frm option')),4)
# IAB Chair
self.client.login(username="iab-chair", password="iab-chair+password")
From 363c01e711495cdfe8338c1c1de90255c7083243 Mon Sep 17 00:00:00 2001
From: Lars Eggert
Date: Sun, 21 Jul 2024 03:14:40 +0300
Subject: [PATCH 007/707] fix: Explicitly set `executable_path` for Selenium
(#7715)
* fix: Explicitly set `executable_path` for Selenium
So it finds `geckodriver` again.
* Minimize diff
* fix: use existing executable_name
Co-authored-by: Lars Eggert
---------
Co-authored-by: Robert Sparks
---
ietf/utils/jstest.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/utils/jstest.py b/ietf/utils/jstest.py
index 07d6ed9dd0..157f97912b 100644
--- a/ietf/utils/jstest.py
+++ b/ietf/utils/jstest.py
@@ -30,7 +30,7 @@
print(" "+skip_message)
def start_web_driver():
- service = Service(log_output=f"{executable_name}.log", service_args=['--log-no-truncate'])
+ service = Service(executable_path=f"/usr/bin/{executable_name}", log_output=f"{executable_name}.log", service_args=['--log-no-truncate'])
options = Options()
options.add_argument("--headless")
os.environ["MOZ_REMOTE_SETTINGS_DEVTOOLS"] = "1"
From a3e4e634fce72ee1e9ef75f5f207408999f1a94b Mon Sep 17 00:00:00 2001
From: Sangho Na
Date: Mon, 22 Jul 2024 06:14:02 +1200
Subject: [PATCH 008/707] fix: Exclude replaced documents from IESG discusses
(#7712)
* fix: Exclude replaced documents from IESG discusses
* test: Add checks for filtering replaced IESG drafts
* chore: Improve replaced draft filter
---------
Co-authored-by: Paul Selkirk
---
ietf/iesg/tests.py | 9 +++++++++
ietf/iesg/views.py | 1 +
2 files changed, 10 insertions(+)
diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py
index 7211a6bc06..4579316f22 100644
--- a/ietf/iesg/tests.py
+++ b/ietf/iesg/tests.py
@@ -52,6 +52,15 @@ def test_feed(self):
self.assertContains(r, draft.name)
self.assertContains(r, escape(pos.balloter.plain_name()))
+ # Mark draft as replaced
+ draft.set_state(State.objects.get(type="draft", slug="repl"))
+
+ r = self.client.get(urlreverse("ietf.iesg.views.discusses"))
+ self.assertEqual(r.status_code, 200)
+
+ self.assertNotContains(r, draft.name)
+ self.assertNotContains(r, escape(pos.balloter.plain_name()))
+
def test_milestones_needing_review(self):
draft = WgDraftFactory()
RoleFactory(name_id='ad',group=draft.group,person=Person.objects.get(user__username='ad'))
diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py
index a219a6b5de..b67ef04a03 100644
--- a/ietf/iesg/views.py
+++ b/ietf/iesg/views.py
@@ -483,6 +483,7 @@ def discusses(request):
models.Q(states__type__in=("statchg", "conflrev"),
states__slug__in=("iesgeval", "defer")),
docevent__ballotpositiondocevent__pos__blocking=True)
+ possible_docs = possible_docs.exclude(states__in=State.objects.filter(type="draft", slug="repl"))
possible_docs = possible_docs.select_related("stream", "group", "ad").distinct()
docs = []
From aa36f481e13c99bc0865282a9e890e6b049cdb98 Mon Sep 17 00:00:00 2001
From: Sangho Na
Date: Mon, 22 Jul 2024 06:15:33 +1200
Subject: [PATCH 009/707] chore: Add additional log messages to directauth()
(#7716)
* chore: Add additional log messages to directauth()
* chore: Keep single log message for each successful response
---
ietf/api/views.py | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/ietf/api/views.py b/ietf/api/views.py
index 6aaed4b6a9..62857bff54 100644
--- a/ietf/api/views.py
+++ b/ietf/api/views.py
@@ -429,6 +429,7 @@ def directauth(request):
data = None
if raw_data is None or data is None:
+ log.log("Request body is either missing or invalid")
return HttpResponse(json.dumps(dict(result="failure",reason="invalid post")), content_type='application/json')
authtoken = data.get('authtoken', None)
@@ -436,9 +437,11 @@ def directauth(request):
password = data.get('password', None)
if any([item is None for item in (authtoken, username, password)]):
+ log.log("One or more mandatory fields are missing: authtoken, username, password")
return HttpResponse(json.dumps(dict(result="failure",reason="invalid post")), content_type='application/json')
if not is_valid_token("ietf.api.views.directauth", authtoken):
+ log.log("Auth token provided is invalid")
return HttpResponse(json.dumps(dict(result="failure",reason="invalid authtoken")), content_type='application/json')
user_query = User.objects.filter(username__iexact=username)
@@ -449,18 +452,20 @@ def directauth(request):
# Note well that we are using user.username, not what was passed to the API.
- if user_query.count() == 1 and authenticate(username = user_query.first().username, password = password):
+ user_count = user_query.count()
+ if user_count == 1 and authenticate(username = user_query.first().username, password = password):
user = user_query.get()
if user_query.filter(person__isnull=True).count() == 1: # Can't inspect user.person direclty here
- log.log(f"Direct auth of personless user {user.pk}:{user.username}")
+ log.log(f"Direct auth success (personless user): {user.pk}:{user.username}")
else:
- log.log(f"Direct auth: {user.pk}:{user.person.plain_name()}")
+ log.log(f"Direct auth success: {user.pk}:{user.person.plain_name()}")
return HttpResponse(json.dumps(dict(result="success")), content_type='application/json')
- log.log(f"Direct auth failure: {username}")
+ log.log(f"Direct auth failure: {username} ({user_count} user(s) found)")
return HttpResponse(json.dumps(dict(result="failure", reason="authentication failed")), content_type='application/json')
else:
+ log.log(f"Request must be POST: {request.method} received")
return HttpResponse(status=405)
From d5ceb7b20d89ae0d5b8948c151f48c8ce4d546c9 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 24 Jul 2024 14:20:18 -0700
Subject: [PATCH 010/707] fix: optional / for /person/merge/ URL (#7746)
---
ietf/person/urls.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/person/urls.py b/ietf/person/urls.py
index f37d8b46cf..867646fe39 100644
--- a/ietf/person/urls.py
+++ b/ietf/person/urls.py
@@ -2,7 +2,7 @@
from ietf.utils.urls import url
urlpatterns = [
- url(r'^merge/$', views.merge),
+ url(r'^merge/?$', views.merge),
url(r'^search/(?P(person|email))/$', views.ajax_select2_search),
url(r'^(?P[0-9]+)/email.json$', ajax.person_email_json),
url(r'^(?P[^/]+)$', views.profile),
From b5ab4b66110ed0777dae170e87db9343876b2741 Mon Sep 17 00:00:00 2001
From: Robert Sparks
Date: Thu, 25 Jul 2024 15:31:53 -0700
Subject: [PATCH 011/707] chore: update test name fixture (#7751)
---
ietf/name/fixtures/names.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index 913c6c987e..3eb2c38d6f 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -3464,7 +3464,7 @@
"parent_types": [],
"req_subm_approval": true,
"role_order": "[\n \"chair\",\n \"delegate\"\n]",
- "session_purposes": "[\n \"officehours\"\n]",
+ "session_purposes": "[\n \"officehours\",\n \"regular\"\n]",
"show_on_agenda": true
},
"model": "group.groupfeatures",
From 247361b7dd8550233991abaa289ca36241c8561f Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 30 Jul 2024 20:55:07 -0300
Subject: [PATCH 012/707] ci: better access logs+redirect auth URLs+fix
X-Request-Start header (#7700)
* fix: silence nginx healthcheck logs
* fix: nginx logs in JSON
* fix: typos in nginx conf
* refactor: repeat less nginx config
* fix: log more req headers from gunicorn
* fix: redirect auth->datatracker, not deny
* feat: log X-Forwarded-Proto
---
ietf/utils/jsonlogger.py | 8 ++++++++
k8s/auth.yaml | 3 +++
k8s/datatracker.yaml | 3 +++
k8s/kustomization.yaml | 1 +
k8s/nginx-auth.conf | 8 ++++++--
k8s/nginx-datatracker.conf | 6 +++++-
k8s/nginx-logging.conf | 20 ++++++++++++++++++++
7 files changed, 46 insertions(+), 3 deletions(-)
create mode 100644 k8s/nginx-logging.conf
diff --git a/ietf/utils/jsonlogger.py b/ietf/utils/jsonlogger.py
index a9eeb02ba9..9c7949fd58 100644
--- a/ietf/utils/jsonlogger.py
+++ b/ietf/utils/jsonlogger.py
@@ -24,3 +24,11 @@ def add_fields(self, log_record, record, message_dict):
log_record.setdefault("user_agent", record.args["a"])
log_record.setdefault("len_bytes", record.args["B"])
log_record.setdefault("duration_ms", record.args["M"])
+ log_record.setdefault("host", record.args["{host}i"])
+ log_record.setdefault("x_request_start", record.args["{x-request-start}i"])
+ log_record.setdefault("x_real_ip", record.args["{x-real-ip}i"])
+ log_record.setdefault("x_forwarded_for", record.args["{x-forwarded-for}i"])
+ log_record.setdefault("x_forwarded_proto", record.args["{x-forwarded-proto}i"])
+ log_record.setdefault("cf_connecting_ip", record.args["{cf-connecting-ip}i"])
+ log_record.setdefault("cf_connecting_ipv6", record.args["{cf-connecting-ipv6}i"])
+ log_record.setdefault("cf_ray", record.args["{cf-ray}i"])
diff --git a/k8s/auth.yaml b/k8s/auth.yaml
index 66627ed450..c35cdc8ac2 100644
--- a/k8s/auth.yaml
+++ b/k8s/auth.yaml
@@ -80,6 +80,9 @@ spec:
volumeMounts:
- name: nginx-tmp
mountPath: /tmp
+ - name: dt-cfg
+ mountPath: /etc/nginx/conf.d/00logging.conf
+ subPath: nginx-logging.conf
- name: dt-cfg
mountPath: /etc/nginx/conf.d/auth.conf
subPath: nginx-auth.conf
diff --git a/k8s/datatracker.yaml b/k8s/datatracker.yaml
index 9e1ead1a90..a8a9675687 100644
--- a/k8s/datatracker.yaml
+++ b/k8s/datatracker.yaml
@@ -80,6 +80,9 @@ spec:
volumeMounts:
- name: nginx-tmp
mountPath: /tmp
+ - name: dt-cfg
+ mountPath: /etc/nginx/conf.d/00logging.conf
+ subPath: nginx-logging.conf
- name: dt-cfg
mountPath: /etc/nginx/conf.d/datatracker.conf
subPath: nginx-datatracker.conf
diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml
index ba8b8a5826..4b79f00753 100644
--- a/k8s/kustomization.yaml
+++ b/k8s/kustomization.yaml
@@ -3,6 +3,7 @@ namePrefix: dt-
configMapGenerator:
- name: files-cfgmap
files:
+ - nginx-logging.conf
- nginx-auth.conf
- nginx-datatracker.conf
- settings_local.py
diff --git a/k8s/nginx-auth.conf b/k8s/nginx-auth.conf
index 4cbc8a0a51..6dd5d6ed56 100644
--- a/k8s/nginx-auth.conf
+++ b/k8s/nginx-auth.conf
@@ -2,9 +2,13 @@ server {
listen 8080 default_server;
server_name _;
+ # Replace default "main" formatter with the ietfjson formatter from nginx-logging.conf
+ access_log /var/log/nginx/access.log ietfjson;
+
# Note that regex location matches take priority over non-regex "prefix" matches. Use regexes so that
# our deny all rule does not squelch the other locations.
location ~ ^/health/nginx$ {
+ access_log off;
return 200;
}
@@ -19,14 +23,14 @@ server {
# n.b. (?!...) is a negative lookahead group
location ~ ^(/(?!(api/openid/|accounts/login/|accounts/logout/|accounts/reset/|person/.*/photo|group/groupmenu.json)).*) {
- deny all;
+ return 302 https://datatracker.ietf.org$${keepempty}request_uri;
}
location / {
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' data: https://datatracker.ietf.org/ https://www.ietf.org/ http://ietf.org/ https://analytics.ietf.org https://static.ietf.org; frame-ancestors 'self' ietf.org *.ietf.org meetecho.com *.meetecho.com gather.town *.gather.town";
proxy_set_header Host $${keepempty}host;
proxy_set_header Connection close;
- proxy_set_header X-Request-Start "t=${msec}";
+ proxy_set_header X-Request-Start "t=$${keepempty}msec";
proxy_set_header X-Forwarded-For $${keepempty}proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $${keepempty}remote_addr;
proxy_pass http://localhost:8000;
diff --git a/k8s/nginx-datatracker.conf b/k8s/nginx-datatracker.conf
index 63c985463c..ff439fba6a 100644
--- a/k8s/nginx-datatracker.conf
+++ b/k8s/nginx-datatracker.conf
@@ -2,7 +2,11 @@ server {
listen 8080 default_server;
server_name _;
+ # Replace default "main" formatter with the ietfjson formatter from nginx-logging.conf
+ access_log /var/log/nginx/access.log ietfjson;
+
location /health/nginx {
+ access_log off;
return 200;
}
@@ -15,7 +19,7 @@ server {
add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' data: https://datatracker.ietf.org/ https://www.ietf.org/ http://ietf.org/ https://analytics.ietf.org https://static.ietf.org; frame-ancestors 'self' ietf.org *.ietf.org meetecho.com *.meetecho.com";
proxy_set_header Host $${keepempty}host;
proxy_set_header Connection close;
- proxy_set_header X-Request-Start "t=${msec}";
+ proxy_set_header X-Request-Start "t=$${keepempty}msec";
proxy_set_header X-Forwarded-For $${keepempty}proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $${keepempty}remote_addr;
proxy_pass http://localhost:8000;
diff --git a/k8s/nginx-logging.conf b/k8s/nginx-logging.conf
new file mode 100644
index 0000000000..0938b0530e
--- /dev/null
+++ b/k8s/nginx-logging.conf
@@ -0,0 +1,20 @@
+# Define JSON log format - must be loaded before config that references it
+log_format ietfjson escape=json
+ '{'
+ '"time":"$${keepempty}time_iso8601",'
+ '"remote_ip":"$${keepempty}remote_addr",'
+ '"request":"$${keepempty}request",'
+ '"host":"$${keepempty}host",'
+ '"path":"$${keepempty}request_uri",'
+ '"method":"$${keepempty}request_method",'
+ '"status":"$${keepempty}status",'
+ '"len_bytes":"$${keepempty}body_bytes_sent",'
+ '"duration_ms":"$${keepempty}request_time",'
+ '"referer":"$${keepempty}http_referer",'
+ '"user_agent":"$${keepempty}http_user_agent",'
+ '"x_forwarded_for":"$${keepempty}http_x_forwarded_for",'
+ '"x_forwarded_proto":"$${keepempty}http_x_forwarded_proto",'
+ '"cf_connecting_ip":"$${keepempty}http_cf_connecting_ip",'
+ '"cf_connecting_ipv6":"$${keepempty}http_cf_connecting_ipv6",'
+ '"cf_ray":"$${keepempty}http_cf_ray"'
+ '}';
From fb1942a5386873eb48402fd274cc29c5877bf377 Mon Sep 17 00:00:00 2001
From: Matthew Holloway
Date: Fri, 2 Aug 2024 07:25:03 +1200
Subject: [PATCH 013/707] fix: Sort RFCs by date (#7766)
* fix: Sort RFCs by date
* fix: concluded wgs and bofs date sort #7350
---
ietf/static/js/list.js | 6 ++++--
ietf/templates/group/concluded_groups.html | 8 ++++----
ietf/templates/person/profile.html | 2 +-
3 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/ietf/static/js/list.js b/ietf/static/js/list.js
index d7e9dc944b..c16111ba63 100644
--- a/ietf/static/js/list.js
+++ b/ietf/static/js/list.js
@@ -5,8 +5,10 @@ import {
function text_sort(a, b, options) {
function prep(e, options) {
- return $($.parseHTML(e.values()[options.valueName]))
- .text()
+ const el = $($.parseHTML(e.values()[options.valueName]));
+ const cell_el = e.elm.querySelector(`.${options.valueName}`)
+ const sort_by_number = cell_el?.getAttribute('data-sort-number')
+ return sort_by_number ?? el.text()
.trim()
.replaceAll(/\s+/g, ' ');
}
diff --git a/ietf/templates/group/concluded_groups.html b/ietf/templates/group/concluded_groups.html
index c748c2061b..725e8bd3cc 100644
--- a/ietf/templates/group/concluded_groups.html
+++ b/ietf/templates/group/concluded_groups.html
@@ -40,8 +40,8 @@ {{ label }}
| Group |
Name |
- Start |
- Concluded |
+ Start |
+ Concluded |
@@ -51,8 +51,8 @@ {{ label }}
{{ g.acronym }}
{{ g.name }} |
- {{ g.start_date|date:"Y-m" }} |
- {{ g.conclude_date|date:"Y-m" }} |
+ {{ g.start_date|date:"Y-m" }} |
+ {{ g.conclude_date|date:"Y-m" }} |
{% endfor %}
diff --git a/ietf/templates/person/profile.html b/ietf/templates/person/profile.html
index 42e5d2e43a..1424f037a1 100644
--- a/ietf/templates/person/profile.html
+++ b/ietf/templates/person/profile.html
@@ -106,7 +106,7 @@
|
RFC {{ doc.rfc_number }}
|
- {{ doc.pub_date|date:"b Y"|title }} |
+ {{ doc.pub_date|date:"b Y"|title }} |
{{ doc.title|urlize_ietf_docs }} |
{% with doc.referenced_by_rfcs_as_rfc_or_draft.count as refbycount %}
From 06677a9863b541d2e322b2ec5fd1032035c175fd Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 1 Aug 2024 17:23:35 -0300
Subject: [PATCH 014/707] fix: require login to pdfize (#7775)
* fix: require login to pdfize
* fix: suppress "pdfized" button when it won't work
---
ietf/doc/views_doc.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py
index 42898d2098..dfef40e558 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -265,6 +265,8 @@ def document_main(request, name, rev=None, document_html=False):
can_change_stream = bool(can_edit or roles)
file_urls, found_types = build_file_urls(doc)
+ if not request.user.is_authenticated:
+ file_urls = [fu for fu in file_urls if fu[0] != "pdfized"]
content = doc.text_or_error() # pyflakes:ignore
content = markup_txt.markup(maybe_split(content, split=split_content))
@@ -406,6 +408,8 @@ def document_main(request, name, rev=None, document_html=False):
latest_revision = None
file_urls, found_types = build_file_urls(doc)
+ if not request.user.is_authenticated:
+ file_urls = [fu for fu in file_urls if fu[0] != "pdfized"]
content = doc.text_or_error() # pyflakes:ignore
content = markup_txt.markup(maybe_split(content, split=split_content))
@@ -1039,6 +1043,8 @@ def document_html(request, name, rev=None):
document_html=True,
)
+
+@login_required
def document_pdfized(request, name, rev=None, ext=None):
found = fuzzy_find_documents(name, rev)
From d683920cecf2cd877bf4e3b3453de6b7494bed12 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 1 Aug 2024 18:33:49 -0300
Subject: [PATCH 015/707] test: log in before pdfize test (#7778)
---
ietf/doc/tests.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py
index 5d0bb9c4f9..5e5b741fe5 100644
--- a/ietf/doc/tests.py
+++ b/ietf/doc/tests.py
@@ -3013,6 +3013,13 @@ def test_pdfized(self):
with (Path(dir) / f'{draft.name}-{r:02d}.txt').open('w') as f:
f.write('text content')
+ self.assertTrue(
+ login_testing_unauthorized(
+ self,
+ PersonFactory().user.username,
+ urlreverse(self.view, kwargs={"name": draft.name}),
+ )
+ )
self.should_succeed(dict(name=rfc.name))
self.should_succeed(dict(name=draft.name))
for r in range(0,2):
From 0b445a9f0985ed6a96569391b39cc1ef4a31a5d0 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 5 Aug 2024 10:47:32 -0300
Subject: [PATCH 016/707] docs: fix email in README.md (#7784)
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 133d08f5e7..ee9865ba21 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ Many developers are using [VS Code](https://code.visualstudio.com/) and taking a
If VS Code is not available to you, in your clone, type `cd docker; ./run`
-Once the containers are started, run the tests to make sure your checkout is a good place to start from (all tests should pass - if any fail, ask for help at tools-develop@). Inside the app container's shell type:
+Once the containers are started, run the tests to make sure your checkout is a good place to start from (all tests should pass - if any fail, ask for help at tools-help@). Inside the app container's shell type:
```sh
ietf/manage.py test --settings=settings_test
```
From b13a606a247c8603e7494822c58ff5028ccce5f5 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 5 Aug 2024 11:00:15 -0300
Subject: [PATCH 017/707] feat: recognize HTTPS via proxy (#7765)
* feat: set SECURE_PROXY_SSL_HEADER
* chore: update comment
---
k8s/settings_local.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/k8s/settings_local.py b/k8s/settings_local.py
index 6f0956d065..33ac4f1e38 100644
--- a/k8s/settings_local.py
+++ b/k8s/settings_local.py
@@ -17,6 +17,13 @@ def _multiline_to_list(s):
# Default to "development". Production _must_ set DATATRACKER_SERVER_MODE="production" in the env!
SERVER_MODE = os.environ.get("DATATRACKER_SERVER_MODE", "development")
+# Use X-Forwarded-Proto to determine request.is_secure(). This relies on CloudFlare overwriting the
+# value of the header if an incoming request sets it, which it does:
+# https://developers.cloudflare.com/fundamentals/reference/http-request-headers/#x-forwarded-proto
+# See also, especially the warnings:
+# https://docs.djangoproject.com/en/dev/ref/settings/#secure-proxy-ssl-header
+SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
+
# Secrets
_SECRET_KEY = os.environ.get("DATATRACKER_DJANGO_SECRET_KEY", None)
if _SECRET_KEY is not None:
From 8a5826a9414ddc53cb721b069e19bf96e2c2c71a Mon Sep 17 00:00:00 2001
From: Rich Salz
Date: Mon, 5 Aug 2024 10:48:48 -0400
Subject: [PATCH 018/707] fix: redundant word in banner for Legacy stream
documents (#7207)
* fix: Remove redundant "stream stream" output
fix: Change "Legacy stream" to "Legacy"
chore: Add "stream" to stream.desc as needed
Fixes: #6902
* chore: Remove unused stream_desc parameter
The stream_desc key isn't used in template/doc/docuemnt_draft.html to
don't pass it in nor compute it
Fixes: #6902
* fix: migrate the legacy StreamName
---------
Co-authored-by: Robert Sparks
---
ietf/doc/views_doc.py | 8 +------
ietf/name/fixtures/names.json | 2 +-
.../0014_change_legacy_stream_desc.py | 21 +++++++++++++++++++
ietf/secr/templates/telechat/doc.html | 2 +-
.../doc/mail/last_call_announcement.txt | 2 +-
5 files changed, 25 insertions(+), 10 deletions(-)
create mode 100644 ietf/name/migrations/0014_change_legacy_stream_desc.py
diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py
index dfef40e558..21c5eb235e 100644
--- a/ietf/doc/views_doc.py
+++ b/ietf/doc/views_doc.py
@@ -607,12 +607,7 @@ def document_main(request, name, rev=None, document_html=False):
additional_urls = doc.documenturl_set.exclude(tag_id='auth48')
# Stream description and name passing test
- if doc.stream != None:
- stream_desc = doc.stream.desc
- stream = "draft-stream-" + doc.stream.slug
- else:
- stream_desc = "(None)"
- stream = "(None)"
+ stream = ("draft-stream-" + doc.stream.slug) if doc.stream != None else "(None)"
html = None
js = None
@@ -651,7 +646,6 @@ def document_main(request, name, rev=None, document_html=False):
revisions=simple_diff_revisions if document_html else revisions,
snapshot=snapshot,
stream=stream,
- stream_desc=stream_desc,
latest_revision=latest_revision,
latest_rev=latest_rev,
can_edit=can_edit,
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index 3eb2c38d6f..59b367deb8 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -14058,7 +14058,7 @@
},
{
"fields": {
- "desc": "Legacy stream",
+ "desc": "Legacy",
"name": "Legacy",
"order": 6,
"used": true
diff --git a/ietf/name/migrations/0014_change_legacy_stream_desc.py b/ietf/name/migrations/0014_change_legacy_stream_desc.py
new file mode 100644
index 0000000000..8297e86274
--- /dev/null
+++ b/ietf/name/migrations/0014_change_legacy_stream_desc.py
@@ -0,0 +1,21 @@
+# Copyright The IETF Trust 2024, All Rights Reserved
+
+from django.db import migrations
+
+def forward(apps, schema_editor):
+ StreamName = apps.get_model("name", "StreamName")
+ StreamName.objects.filter(pk="legacy").update(desc="Legacy")
+
+def reverse(apps, schema_editor):
+ StreamName = apps.get_model("name", "StreamName")
+ StreamName.objects.filter(pk="legacy").update(desc="Legacy stream")
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("name", "0013_narrativeminutes"),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse)
+ ]
diff --git a/ietf/secr/templates/telechat/doc.html b/ietf/secr/templates/telechat/doc.html
index 7891c1b1e7..6727e157f5 100644
--- a/ietf/secr/templates/telechat/doc.html
+++ b/ietf/secr/templates/telechat/doc.html
@@ -86,7 +86,7 @@ Ballot Writeup
Downward References
{% for ref in downrefs %}
Add {{ref.target.name}}
- ({{ref.target.std_level}} - {{ref.target.stream.desc}})
+ ({{ref.target.std_level}} - {{ref.target.stream.desc}} stream)
to downref registry.
{% if not ref.target.std_level %}
+++ Warning: The standards level has not been set yet!!!
diff --git a/ietf/templates/doc/mail/last_call_announcement.txt b/ietf/templates/doc/mail/last_call_announcement.txt
index 8f15a8e2a6..5cf2e9c45b 100644
--- a/ietf/templates/doc/mail/last_call_announcement.txt
+++ b/ietf/templates/doc/mail/last_call_announcement.txt
@@ -33,7 +33,7 @@ No IPR declarations have been submitted directly on this I-D.
{% if downrefs %}
The document contains these normative downward references.
See RFC 3967 for additional information:
-{% for ref in downrefs %} {{ref.target.name}}: {{ref.target.title}} ({{ref.target.std_level}} - {{ref.target.stream.desc}})
+{% for ref in downrefs %} {{ref.target.name}}: {{ref.target.title}} ({{ref.target.std_level}} - {{ref.target.stream.desc}} stream)
{% endfor %}{%endif%}
{% endautoescape %}
From 16ac73d4b748de2ae1162452753a84bc3d6a0369 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 5 Aug 2024 12:18:01 -0300
Subject: [PATCH 019/707] fix: use BOF states in concluded_groups() (#7771)
* fix: use BOF states in concluded_groups()
* fix: handle events for older BOFs
These could be cleaned up in the database, but I think this
change does the right thing for the existing data.
* style: Black
---
ietf/group/views.py | 79 +++++++++++++++++++++++++++++++++++++--------
1 file changed, 65 insertions(+), 14 deletions(-)
diff --git a/ietf/group/views.py b/ietf/group/views.py
index f909a31b6d..e3fd7e80d9 100644
--- a/ietf/group/views.py
+++ b/ietf/group/views.py
@@ -334,35 +334,86 @@ def chartering_groups(request):
dict(charter_states=charter_states,
group_types=group_types))
+
def concluded_groups(request):
sections = OrderedDict()
- sections['WGs'] = Group.objects.filter(type='wg', state="conclude").select_related("state", "charter").order_by("parent__name","acronym")
- sections['RGs'] = Group.objects.filter(type='rg', state="conclude").select_related("state", "charter").order_by("parent__name","acronym")
- sections['BOFs'] = Group.objects.filter(type='wg', state="bof-conc").select_related("state", "charter").order_by("parent__name","acronym")
- sections['AGs'] = Group.objects.filter(type='ag', state="conclude").select_related("state", "charter").order_by("parent__name","acronym")
- sections['RAGs'] = Group.objects.filter(type='rag', state="conclude").select_related("state", "charter").order_by("parent__name","acronym")
- sections['Directorates'] = Group.objects.filter(type='dir', state="conclude").select_related("state", "charter").order_by("parent__name","acronym")
- sections['Review teams'] = Group.objects.filter(type='review', state="conclude").select_related("state", "charter").order_by("parent__name","acronym")
- sections['Teams'] = Group.objects.filter(type='team', state="conclude").select_related("state", "charter").order_by("parent__name","acronym")
- sections['Programs'] = Group.objects.filter(type='program', state="conclude").select_related("state", "charter").order_by("parent__name","acronym")
+ sections["WGs"] = (
+ Group.objects.filter(type="wg", state="conclude")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
+ sections["RGs"] = (
+ Group.objects.filter(type="rg", state="conclude")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
+ sections["BOFs"] = (
+ Group.objects.filter(type="wg", state="bof-conc")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
+ sections["AGs"] = (
+ Group.objects.filter(type="ag", state="conclude")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
+ sections["RAGs"] = (
+ Group.objects.filter(type="rag", state="conclude")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
+ sections["Directorates"] = (
+ Group.objects.filter(type="dir", state="conclude")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
+ sections["Review teams"] = (
+ Group.objects.filter(type="review", state="conclude")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
+ sections["Teams"] = (
+ Group.objects.filter(type="team", state="conclude")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
+ sections["Programs"] = (
+ Group.objects.filter(type="program", state="conclude")
+ .select_related("state", "charter")
+ .order_by("parent__name", "acronym")
+ )
for name, groups in sections.items():
-
# add start/conclusion date
d = dict((g.pk, g) for g in groups)
for g in groups:
g.start_date = g.conclude_date = None
- for e in ChangeStateGroupEvent.objects.filter(group__in=groups, state="active").order_by("-time"):
+ # Some older BOFs were created in the "active" state, so consider both "active" and "bof"
+ # ChangeStateGroupEvents when finding the start date. A group with _both_ "active" and "bof"
+ # events should not be in the "bof-conc" state so this shouldn't cause a problem (if it does,
+ # we'll need to clean up the data)
+ for e in ChangeStateGroupEvent.objects.filter(
+ group__in=groups,
+ state__in=["active", "bof"] if name == "BOFs" else ["active"],
+ ).order_by("-time"):
d[e.group_id].start_date = e.time
- for e in ChangeStateGroupEvent.objects.filter(group__in=groups, state="conclude").order_by("time"):
+ # Similarly, some older BOFs were concluded into the "conclude" state and the event was never
+ # fixed, so consider both "conclude" and "bof-conc" ChangeStateGroupEvents when finding the
+ # concluded date. A group with _both_ "conclude" and "bof-conc" events should not be in the
+ # "bof-conc" state so this shouldn't cause a problem (if it does, we'll need to clean up the
+ # data)
+ for e in ChangeStateGroupEvent.objects.filter(
+ group__in=groups,
+ state__in=["bof-conc", "conclude"] if name == "BOFs" else ["conclude"],
+ ).order_by("time"):
d[e.group_id].conclude_date = e.time
- return render(request, 'group/concluded_groups.html',
- dict(sections=sections))
+ return render(request, "group/concluded_groups.html", dict(sections=sections))
+
def prepare_group_documents(request, group, clist):
found_docs, meta = prepare_document_table(request, docs_tracked_by_community_list(clist), request.GET, max_results=500)
From 63d13074d1c6496223b85d1bd471db5211004d16 Mon Sep 17 00:00:00 2001
From: Russ Housley
Date: Mon, 5 Aug 2024 12:03:17 -0400
Subject: [PATCH 020/707] fix: use area-acronym-at-the-time in proceedings
(#7723)
When generating IETF meeting proceedings, use the Area Acronym for each WG at the time of the meeting. Fixes #7706.
---
ietf/meeting/views.py | 16 +++++++++++-----
1 file changed, 11 insertions(+), 5 deletions(-)
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 253f2852ff..3e483b193b 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -4131,6 +4131,13 @@ def _format_materials(items):
def proceedings(request, num=None):
+ def area_and_group_acronyms_from_session(s):
+ area = s.group_parent_at_the_time()
+ if area == None:
+ area = s.group.parent
+ group = s.group_at_the_time()
+ return (area.acronym, group.acronym)
+
meeting = get_meeting(num)
# Early proceedings were hosted on www.ietf.org rather than the datatracker
@@ -4181,12 +4188,11 @@ def proceedings(request, num=None):
.exclude(current_status='notmeet')
)
- ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym='edu').order_by('group__parent__acronym', 'group__acronym')
+ ietf = sessions.filter(group__parent__type__slug = 'area').exclude(group__acronym__in=['edu','iepg','tools'])
+ ietf = list(ietf)
+ ietf.sort(key=lambda s: area_and_group_acronyms_from_session(s))
ietf_areas = []
- for area, area_sessions in itertools.groupby(
- ietf,
- key=lambda s: s.group.parent
- ):
+ for area, area_sessions in itertools.groupby(ietf, key=lambda s: s.group_parent_at_the_time()):
meeting_groups, not_meeting_groups = organize_proceedings_sessions(area_sessions)
ietf_areas.append((area, meeting_groups, not_meeting_groups))
From 9ef7bff77c1208ed6c9504002aee3320856e6499 Mon Sep 17 00:00:00 2001
From: Jim Fenton
Date: Tue, 6 Aug 2024 08:03:37 -0700
Subject: [PATCH 021/707] feat: Unify slide upload and proposal (#7787)
* attempt at optional approval
* Update of meeting slides propose/upload
* Fix tests and residual coding bugs
* Remove gratuitous blank lines
---
ietf/meeting/forms.py | 7 +-
ietf/meeting/tests_views.py | 45 ++++--
ietf/meeting/urls.py | 1 -
ietf/meeting/views.py | 130 +++++++-----------
.../meeting/propose_session_slides.html | 27 ----
.../meeting/session_details_panel.html | 2 +-
ietf/templates/meeting/slides_approved.txt | 2 +-
.../meeting/upload_session_slides.html | 12 +-
8 files changed, 95 insertions(+), 131 deletions(-)
delete mode 100644 ietf/templates/meeting/propose_session_slides.html
diff --git a/ietf/meeting/forms.py b/ietf/meeting/forms.py
index b31ffb6cd7..3b66d2cd29 100644
--- a/ietf/meeting/forms.py
+++ b/ietf/meeting/forms.py
@@ -489,9 +489,12 @@ class UploadAgendaForm(ApplyToAllFileUploadForm):
class UploadSlidesForm(ApplyToAllFileUploadForm):
doc_type = 'slides'
title = forms.CharField(max_length=255)
+ approved = forms.BooleanField(label='Auto-approve', initial=True, required=False)
- def __init__(self, session, *args, **kwargs):
- super().__init__(*args, **kwargs)
+ def __init__(self, session, show_apply_to_all_checkbox, can_manage, *args, **kwargs):
+ super().__init__(show_apply_to_all_checkbox, *args, **kwargs)
+ if not can_manage:
+ self.fields.pop('approved')
self.session = session
def clean_title(self):
diff --git a/ietf/meeting/tests_views.py b/ietf/meeting/tests_views.py
index db62fe6204..da82afb329 100644
--- a/ietf/meeting/tests_views.py
+++ b/ietf/meeting/tests_views.py
@@ -6454,7 +6454,7 @@ def test_upload_slides(self, mock_slides_manager_cls):
self.assertFalse(session1.presentations.filter(document__type_id='slides'))
test_file = BytesIO(b'this is not really a slide')
test_file.name = 'not_really.txt'
- r = self.client.post(url,dict(file=test_file,title='a test slide file',apply_to_all=True))
+ r = self.client.post(url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=True))
self.assertEqual(r.status_code, 302)
self.assertEqual(session1.presentations.count(),1)
self.assertEqual(session2.presentations.count(),1)
@@ -6477,7 +6477,7 @@ def test_upload_slides(self, mock_slides_manager_cls):
url = urlreverse('ietf.meeting.views.upload_session_slides',kwargs={'num':session2.meeting.number,'session_id':session2.id})
test_file = BytesIO(b'some other thing still not slidelike')
test_file.name = 'also_not_really.txt'
- r = self.client.post(url,dict(file=test_file,title='a different slide file',apply_to_all=False))
+ r = self.client.post(url,dict(file=test_file,title='a different slide file',apply_to_all=False,approved=True))
self.assertEqual(r.status_code, 302)
self.assertEqual(session1.presentations.count(),1)
self.assertEqual(session2.presentations.count(),2)
@@ -6501,7 +6501,7 @@ def test_upload_slides(self, mock_slides_manager_cls):
self.assertIn('Revise', str(q("title")))
test_file = BytesIO(b'new content for the second slide deck')
test_file.name = 'doesnotmatter.txt'
- r = self.client.post(url,dict(file=test_file,title='rename the presentation',apply_to_all=False))
+ r = self.client.post(url,dict(file=test_file,title='rename the presentation',apply_to_all=False, approved=True))
self.assertEqual(r.status_code, 302)
self.assertEqual(session1.presentations.count(),1)
self.assertEqual(session2.presentations.count(),2)
@@ -6597,7 +6597,7 @@ def test_propose_session_slides(self):
newperson = PersonFactory()
session_overview_url = urlreverse('ietf.meeting.views.session_details',kwargs={'num':session.meeting.number,'acronym':session.group.acronym})
- propose_url = urlreverse('ietf.meeting.views.propose_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number})
+ upload_url = urlreverse('ietf.meeting.views.upload_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number})
r = self.client.get(session_overview_url)
self.assertEqual(r.status_code,200)
@@ -6612,13 +6612,13 @@ def test_propose_session_slides(self):
self.assertTrue(q('.proposeslides'))
self.client.logout()
- login_testing_unauthorized(self,newperson.user.username,propose_url)
- r = self.client.get(propose_url)
+ login_testing_unauthorized(self,newperson.user.username,upload_url)
+ r = self.client.get(upload_url)
self.assertEqual(r.status_code,200)
test_file = BytesIO(b'this is not really a slide')
test_file.name = 'not_really.txt'
empty_outbox()
- r = self.client.post(propose_url,dict(file=test_file,title='a test slide file',apply_to_all=True))
+ r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=False))
self.assertEqual(r.status_code, 302)
session = Session.objects.get(pk=session.pk)
self.assertEqual(session.slidesubmission_set.count(),1)
@@ -6639,6 +6639,25 @@ def test_propose_session_slides(self):
self.assertEqual(len(q('.proposedslidelist p')), 2)
self.client.logout()
+ login_testing_unauthorized(self,chair.user.username,upload_url)
+ r = self.client.get(upload_url)
+ self.assertEqual(r.status_code,200)
+ test_file = BytesIO(b'this is not really a slide either')
+ test_file.name = 'again_not_really.txt'
+ empty_outbox()
+ r = self.client.post(upload_url,dict(file=test_file,title='a selfapproved test slide file',apply_to_all=True,approved=True))
+ self.assertEqual(r.status_code, 302)
+ self.assertEqual(len(outbox),0)
+ self.assertEqual(session.slidesubmission_set.count(),2)
+ self.client.logout()
+
+ self.client.login(username=chair.user.username, password=chair.user.username+"+password")
+ r = self.client.get(session_overview_url)
+ self.assertEqual(r.status_code, 200)
+ q = PyQuery(r.content)
+ self.assertEqual(len(q('.uploadslidelist p')), 0)
+ self.client.logout()
+
def test_disapprove_proposed_slides(self):
submission = SlideSubmissionFactory()
submission.session.meeting.importantdate_set.create(name_id='revsub',date=date_today() + datetime.timedelta(days=20))
@@ -6759,12 +6778,12 @@ def test_submit_and_approve_multiple_versions(self, mock_slides_manager_cls):
session.meeting.importantdate_set.create(name_id='revsub',date=date_today()+datetime.timedelta(days=20))
newperson = PersonFactory()
- propose_url = urlreverse('ietf.meeting.views.propose_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number})
+ upload_url = urlreverse('ietf.meeting.views.upload_session_slides', kwargs={'session_id':session.pk, 'num': session.meeting.number})
- login_testing_unauthorized(self,newperson.user.username,propose_url)
+ login_testing_unauthorized(self,newperson.user.username,upload_url)
test_file = BytesIO(b'this is not really a slide')
test_file.name = 'not_really.txt'
- r = self.client.post(propose_url,dict(file=test_file,title='a test slide file',apply_to_all=True))
+ r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True,approved=False))
self.assertEqual(r.status_code, 302)
self.client.logout()
@@ -6787,15 +6806,15 @@ def test_submit_and_approve_multiple_versions(self, mock_slides_manager_cls):
self.assertEqual(session.presentations.first().document.rev,'00')
- login_testing_unauthorized(self,newperson.user.username,propose_url)
+ login_testing_unauthorized(self,newperson.user.username,upload_url)
test_file = BytesIO(b'this is not really a slide, but it is another version of it')
test_file.name = 'not_really.txt'
- r = self.client.post(propose_url,dict(file=test_file,title='a test slide file',apply_to_all=True))
+ r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True))
self.assertEqual(r.status_code, 302)
test_file = BytesIO(b'this is not really a slide, but it is third version of it')
test_file.name = 'not_really.txt'
- r = self.client.post(propose_url,dict(file=test_file,title='a test slide file',apply_to_all=True))
+ r = self.client.post(upload_url,dict(file=test_file,title='a test slide file',apply_to_all=True))
self.assertEqual(r.status_code, 302)
self.client.logout()
diff --git a/ietf/meeting/urls.py b/ietf/meeting/urls.py
index 26d3d93b20..f2e65578ec 100644
--- a/ietf/meeting/urls.py
+++ b/ietf/meeting/urls.py
@@ -22,7 +22,6 @@ def get_redirect_url(self, *args, **kwargs):
url(r'^session/(?P\d+)/narrativeminutes$', views.upload_session_narrativeminutes),
url(r'^session/(?P\d+)/agenda$', views.upload_session_agenda),
url(r'^session/(?P\d+)/import/minutes$', views.import_session_minutes),
- url(r'^session/(?P\d+)/propose_slides$', views.propose_session_slides),
url(r'^session/(?P\d+)/slides(?:/%(name)s)?$' % settings.URL_REGEXPS, views.upload_session_slides),
url(r'^session/(?P\d+)/add_to_session$', views.ajax_add_slides_to_session),
url(r'^session/(?P\d+)/remove_from_session$', views.ajax_remove_slides_from_session),
diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py
index 3e483b193b..c3494a13ba 100644
--- a/ietf/meeting/views.py
+++ b/ietf/meeting/views.py
@@ -1702,7 +1702,7 @@ def api_get_session_materials(request, session_id=None):
minutes = session.minutes()
slides_actions = []
- if can_manage_session_materials(request.user, session.group, session):
+ if can_manage_session_materials(request.user, session.group, session) or not session.is_material_submission_cutoff():
slides_actions.append(
{
"label": "Upload slides",
@@ -1712,16 +1712,6 @@ def api_get_session_materials(request, session_id=None):
),
}
)
- elif not session.is_material_submission_cutoff():
- slides_actions.append(
- {
- "label": "Propose slides",
- "url": reverse(
- "ietf.meeting.views.propose_session_slides",
- kwargs={"num": session.meeting.number, "session_id": session.pk},
- ),
- }
- )
else:
pass # no action available if it's past cutoff
@@ -2920,6 +2910,7 @@ def upload_session_agenda(request, session_id, num):
})
+@login_required
def upload_session_slides(request, session_id, num, name=None):
"""Upload new or replacement slides for a session
@@ -2927,10 +2918,7 @@ def upload_session_slides(request, session_id, num, name=None):
"""
# num is redundant, but we're dragging it along an artifact of where we are in the current URL structure
session = get_object_or_404(Session, pk=session_id)
- if not session.can_manage_materials(request.user):
- permission_denied(
- request, "You don't have permission to upload slides for this session."
- )
+ can_manage = session.can_manage_materials(request.user)
if session.is_material_submission_cutoff() and not has_role(
request.user, "Secretariat"
):
@@ -2955,7 +2943,7 @@ def upload_session_slides(request, session_id, num, name=None):
if request.method == "POST":
form = UploadSlidesForm(
- session, show_apply_to_all_checkbox, request.POST, request.FILES
+ session, show_apply_to_all_checkbox, can_manage, request.POST, request.FILES
)
if form.is_valid():
file = request.FILES["file"]
@@ -2963,6 +2951,46 @@ def upload_session_slides(request, session_id, num, name=None):
apply_to_all = session.type_id == "regular"
if show_apply_to_all_checkbox:
apply_to_all = form.cleaned_data["apply_to_all"]
+ if can_manage:
+ approved = form.cleaned_data["approved"]
+ else:
+ approved = False
+
+ # Propose slides if not auto-approved
+ if not approved:
+ title = form.cleaned_data['title']
+ submission = SlideSubmission.objects.create(session = session, title = title, filename = '', apply_to_all = apply_to_all, submitter=request.user.person)
+
+ if session.meeting.type_id=='ietf':
+ name = 'slides-%s-%s' % (session.meeting.number,
+ session.group.acronym)
+ if not apply_to_all:
+ name += '-%s' % (session.docname_token(),)
+ else:
+ name = 'slides-%s-%s' % (session.meeting.number, session.docname_token())
+ name = name + '-' + slugify(title).replace('_', '-')[:128]
+ filename = '%s-ss%d%s'% (name, submission.id, ext)
+ destination = io.open(os.path.join(settings.SLIDE_STAGING_PATH, filename),'wb+')
+ for chunk in file.chunks():
+ destination.write(chunk)
+ destination.close()
+
+ submission.filename = filename
+ submission.save()
+
+ (to, cc) = gather_address_lists('slides_proposed', group=session.group, proposer=request.user.person).as_strings()
+ msg_txt = render_to_string("meeting/slides_proposed.txt", {
+ "to": to,
+ "cc": cc,
+ "submission": submission,
+ "settings": settings,
+ })
+ msg = infer_message(msg_txt)
+ msg.by = request.user.person
+ msg.save()
+ send_mail_message(request, msg)
+ messages.success(request, 'Successfully submitted proposed slides.')
+ return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym)
# Handle creation / update of the Document (but do not save yet)
if doc is not None:
@@ -3076,7 +3104,7 @@ def upload_session_slides(request, session_id, num, name=None):
initial = {}
if doc is not None:
initial = {"title": doc.title}
- form = UploadSlidesForm(session, show_apply_to_all_checkbox, initial=initial)
+ form = UploadSlidesForm(session, show_apply_to_all_checkbox, can_manage, initial=initial)
return render(
request,
@@ -3085,77 +3113,12 @@ def upload_session_slides(request, session_id, num, name=None):
"session": session,
"session_number": session_number,
"slides_sp": session.presentations.filter(document=doc).first() if doc else None,
+ "manage": session.can_manage_materials(request.user),
"form": form,
},
)
-@login_required
-def propose_session_slides(request, session_id, num):
- session = get_object_or_404(Session,pk=session_id)
- if session.is_material_submission_cutoff() and not has_role(request.user, "Secretariat"):
- permission_denied(request, "The materials cutoff for this session has passed. Contact the secretariat for further action.")
-
- session_number = None
- sessions = get_sessions(session.meeting.number,session.group.acronym)
- show_apply_to_all_checkbox = len(sessions) > 1 if session.type_id == 'regular' else False
- if len(sessions) > 1:
- session_number = 1 + sessions.index(session)
-
-
- if request.method == 'POST':
- form = UploadSlidesForm(session, show_apply_to_all_checkbox,request.POST,request.FILES)
- if form.is_valid():
- file = request.FILES['file']
- _, ext = os.path.splitext(file.name)
- apply_to_all = session.type_id == 'regular'
- if show_apply_to_all_checkbox:
- apply_to_all = form.cleaned_data['apply_to_all']
- title = form.cleaned_data['title']
-
- submission = SlideSubmission.objects.create(session = session, title = title, filename = '', apply_to_all = apply_to_all, submitter=request.user.person)
-
- if session.meeting.type_id=='ietf':
- name = 'slides-%s-%s' % (session.meeting.number,
- session.group.acronym)
- if not apply_to_all:
- name += '-%s' % (session.docname_token(),)
- else:
- name = 'slides-%s-%s' % (session.meeting.number, session.docname_token())
- name = name + '-' + slugify(title).replace('_', '-')[:128]
- filename = '%s-ss%d%s'% (name, submission.id, ext)
- destination = io.open(os.path.join(settings.SLIDE_STAGING_PATH, filename),'wb+')
- for chunk in file.chunks():
- destination.write(chunk)
- destination.close()
-
- submission.filename = filename
- submission.save()
-
- (to, cc) = gather_address_lists('slides_proposed', group=session.group, proposer=request.user.person).as_strings()
- msg_txt = render_to_string("meeting/slides_proposed.txt", {
- "to": to,
- "cc": cc,
- "submission": submission,
- "settings": settings,
- })
- msg = infer_message(msg_txt)
- msg.by = request.user.person
- msg.save()
- send_mail_message(request, msg)
- messages.success(request, 'Successfully submitted proposed slides.')
- return redirect('ietf.meeting.views.session_details',num=num,acronym=session.group.acronym)
- else:
- initial = {}
- form = UploadSlidesForm(session, show_apply_to_all_checkbox, initial=initial)
-
- return render(request, "meeting/propose_session_slides.html",
- {'session': session,
- 'session_number': session_number,
- 'form': form,
- })
-
-
def remove_sessionpresentation(request, session_id, num, name):
sp = get_object_or_404(
SessionPresentation, session_id=session_id, document__name=name
@@ -5072,6 +5035,7 @@ def approve_proposed_slides(request, slidesubmission_id, num):
"cc": cc,
"submission": submission,
"settings": settings,
+ "approver": request.user.person
})
send_mail_text(request, to, None, subject, body, cc=cc)
return redirect('ietf.meeting.views.session_details',num=num,acronym=acronym)
diff --git a/ietf/templates/meeting/propose_session_slides.html b/ietf/templates/meeting/propose_session_slides.html
deleted file mode 100644
index e5a0b451e6..0000000000
--- a/ietf/templates/meeting/propose_session_slides.html
+++ /dev/null
@@ -1,27 +0,0 @@
-{% extends "base.html" %}
-{# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin static django_bootstrap5 tz %}
-{% block title %}Propose Slides for {{ session.meeting }} : {{ session.group.acronym }}{% endblock %}
-{% block content %}
- {% origin %}
-
- Propose Slides for {{ session.meeting }}
-
- {{ session.group.acronym }}
- {% if session.name %}: {{ session.name }}{% endif %}
-
-
- {% if session_number %}
-
- Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}
-
- {% endif %}
-
- This form will allow you to propose a slide deck to the session chairs. After you upload your proposal, mail will be sent to the session chairs asking for their approval.
-
-
-{% endblock %}
\ No newline at end of file
diff --git a/ietf/templates/meeting/session_details_panel.html b/ietf/templates/meeting/session_details_panel.html
index d053ba1c1c..1dcbded913 100644
--- a/ietf/templates/meeting/session_details_panel.html
+++ b/ietf/templates/meeting/session_details_panel.html
@@ -187,7 +187,7 @@ Slides
{% elif request.user.is_authenticated and not session.is_material_submission_cutoff %}
+ href="{% url 'ietf.meeting.views.upload_session_slides' session_id=session.pk num=session.meeting.number %}">
Propose slides
{% endif %}
diff --git a/ietf/templates/meeting/slides_approved.txt b/ietf/templates/meeting/slides_approved.txt
index db288ad853..61ffafcd18 100644
--- a/ietf/templates/meeting/slides_approved.txt
+++ b/ietf/templates/meeting/slides_approved.txt
@@ -1,4 +1,4 @@
-{% load ietf_filters %}{% autoescape off %}Your proposed slides have been approved for {{ submission.session.meeting }} : {{ submission.session.group.acronym }}{% if submission.session.name %} : {{submission.session.name}}{% endif %}
+{% load ietf_filters %}{% autoescape off %}Your proposed slides have been approved for {{ submission.session.meeting }} : {{ submission.session.group.acronym }}{% if submission.session.name %} : {{submission.session.name}}{% endif %} by {{approver}}
Title: {{submission.title}}
diff --git a/ietf/templates/meeting/upload_session_slides.html b/ietf/templates/meeting/upload_session_slides.html
index 8e3e064df3..059ffae16f 100644
--- a/ietf/templates/meeting/upload_session_slides.html
+++ b/ietf/templates/meeting/upload_session_slides.html
@@ -17,15 +17,21 @@
{% else %}
Upload new
{% endif %}
- slides for {{ session.meeting }}
-
+ slides for {{ session.meeting }}
{{ session.group.acronym }}
{% if session.name %}: {{ session.name }}{% endif %}
{% if session_number %}
- Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}
+
+ Session {{ session_number }} : {{ session.official_timeslotassignment.timeslot.time|timezone:session.meeting.time_zone|date:"D M-d-Y Hi" }}
+
+ {% endif %}
+ {% if not manage %}
+
+ This form will allow you to propose a slide deck to the session chairs. After you upload your proposal, mail will be sent to the session chairs asking for their approval.
+
{% endif %}
{% if slides_sp %}{{ slides_sp.document.name }}{% endif %}
|