From f1d58da877a886ac381ff36dbca2ae8a2ab5e298 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 12 Jul 2024 12:10:46 -0300
Subject: [PATCH 001/656] feat: send doc event emails via celery (#7680)
* feat: notify_event_to_subscribers_task
* fix: avoid circular import, handle error
* fix: don't queue task in test mode
* fix: don't even send mail in test mode
* test: separately test signal
* fix: if/else error
* test: better naming
* test: test the new task
* test: better test name
* test: refactor notify email test
* fix: save, not update
* test: restore template coverage
---
ietf/community/models.py | 11 +++-
ietf/community/tasks.py | 15 ++++++
ietf/community/tests.py | 109 ++++++++++++++++++++++++++++++++++-----
3 files changed, 119 insertions(+), 16 deletions(-)
create mode 100644 ietf/community/tasks.py
diff --git a/ietf/community/models.py b/ietf/community/models.py
index 2e40031768..b1295461d6 100644
--- a/ietf/community/models.py
+++ b/ietf/community/models.py
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*-
+from django.conf import settings
from django.db import models
from django.db.models import signals
from django.urls import reverse as urlreverse
@@ -11,6 +12,9 @@
from ietf.person.models import Person, Email
from ietf.utils.models import ForeignKey
+from .tasks import notify_event_to_subscribers_task
+
+
class CommunityList(models.Model):
person = ForeignKey(Person, blank=True, null=True)
group = ForeignKey(Group, blank=True, null=True)
@@ -106,8 +110,11 @@ def notify_events(sender, instance, **kwargs):
if getattr(instance, "skip_community_list_notification", False):
return
- from ietf.community.utils import notify_event_to_subscribers
- notify_event_to_subscribers(instance)
+ # kludge alert: queuing a celery task in response to a signal can cause unexpected attempts to
+ # start a Celery task during tests. To prevent this, don't queue a celery task if we're running
+ # tests.
+ if settings.SERVER_MODE != "test":
+ notify_event_to_subscribers_task.delay(event_id=instance.pk)
signals.post_save.connect(notify_events)
diff --git a/ietf/community/tasks.py b/ietf/community/tasks.py
new file mode 100644
index 0000000000..763a596495
--- /dev/null
+++ b/ietf/community/tasks.py
@@ -0,0 +1,15 @@
+# Copyright The IETF Trust 2024, All Rights Reserved
+from celery import shared_task
+
+from ietf.doc.models import DocEvent
+from ietf.utils.log import log
+
+
+@shared_task
+def notify_event_to_subscribers_task(event_id):
+ from .utils import notify_event_to_subscribers
+ event = DocEvent.objects.filter(pk=event_id).first()
+ if event is None:
+ log(f"Unable to send subscriber notifications because DocEvent {event_id} was not found")
+ else:
+ notify_event_to_subscribers(event)
diff --git a/ietf/community/tests.py b/ietf/community/tests.py
index 387877887f..d76347b70a 100644
--- a/ietf/community/tests.py
+++ b/ietf/community/tests.py
@@ -2,15 +2,18 @@
# -*- coding: utf-8 -*-
+import mock
from pyquery import PyQuery
+from django.test.utils import override_settings
from django.urls import reverse as urlreverse
import debug # pyflakes:ignore
from ietf.community.models import CommunityList, SearchRule, EmailSubscription
from ietf.community.utils import docs_matching_community_list_rule, community_list_rules_matching_doc
-from ietf.community.utils import reset_name_contains_index_for_rule
+from ietf.community.utils import reset_name_contains_index_for_rule, notify_event_to_subscribers
+from ietf.community.tasks import notify_event_to_subscribers_task
import ietf.community.views
from ietf.group.models import Group
from ietf.group.utils import setup_default_community_list_for_group
@@ -18,8 +21,7 @@
from ietf.doc.utils import add_state_change_event
from ietf.person.models import Person, Email, Alias
from ietf.utils.test_utils import TestCase, login_testing_unauthorized
-from ietf.utils.mail import outbox
-from ietf.doc.factories import WgDraftFactory
+from ietf.doc.factories import DocEventFactory, WgDraftFactory
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.person.factories import PersonFactory, EmailFactory, AliasFactory
@@ -423,7 +425,49 @@ def test_subscription_for_group(self):
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
- def test_notification(self):
+ @mock.patch("ietf.community.models.notify_event_to_subscribers_task")
+ def test_notification_signal_receiver(self, mock_notify_task):
+ """Saving a DocEvent should notify subscribers
+
+ This implicitly tests that notify_events is hooked up to the post_save signal.
+ """
+ # Arbitrary model that's not a DocEvent
+ p = PersonFactory()
+ mock_notify_task.reset_mock() # clear any calls that resulted from the factories
+ # be careful overriding SERVER_MODE - we do it here because the method
+ # under test does not make this call when in "test" mode
+ with override_settings(SERVER_MODE="not-test"):
+ p.save()
+ self.assertFalse(mock_notify_task.delay.called)
+
+ d = DocEventFactory()
+ mock_notify_task.reset_mock() # clear any calls that resulted from the factories
+ # be careful overriding SERVER_MODE - we do it here because the method
+ # under test does not make this call when in "test" mode
+ with override_settings(SERVER_MODE="not-test"):
+ d.save()
+ self.assertEqual(mock_notify_task.delay.call_count, 1)
+ self.assertEqual(mock_notify_task.delay.call_args, mock.call(event_id = d.pk))
+
+ mock_notify_task.reset_mock()
+ d.skip_community_list_notification = True
+ # be careful overriding SERVER_MODE - we do it here because the method
+ # under test does not make this call when in "test" mode
+ with override_settings(SERVER_MODE="not-test"):
+ d.save()
+ self.assertFalse(mock_notify_task.delay.called)
+
+ del(d.skip_community_list_notification)
+ d.doc.type_id="rfc" # not "draft"
+ d.doc.save()
+ # be careful overriding SERVER_MODE - we do it here because the method
+ # under test does not make this call when in "test" mode
+ with override_settings(SERVER_MODE="not-test"):
+ d.save()
+ self.assertFalse(mock_notify_task.delay.called)
+
+ @mock.patch("ietf.utils.mail.send_mail_text")
+ def test_notify_event_to_subscribers(self, mock_send_mail_text):
person = PersonFactory(user__username='plain')
draft = WgDraftFactory()
@@ -431,18 +475,55 @@ def test_notification(self):
if not draft in clist.added_docs.all():
clist.added_docs.add(draft)
- EmailSubscription.objects.create(community_list=clist, email=Email.objects.filter(person__user__username="plain").first(), notify_on="significant")
+ sub_to_significant = EmailSubscription.objects.create(
+ community_list=clist,
+ email=Email.objects.filter(person__user__username="plain").first(),
+ notify_on="significant",
+ )
+ sub_to_all = EmailSubscription.objects.create(
+ community_list=clist,
+ email=Email.objects.filter(person__user__username="plain").first(),
+ notify_on="all",
+ )
- mailbox_before = len(outbox)
active_state = State.objects.get(type="draft", slug="active")
system = Person.objects.get(name="(System)")
- add_state_change_event(draft, system, None, active_state)
- self.assertEqual(len(outbox), mailbox_before)
+ event = add_state_change_event(draft, system, None, active_state)
+ notify_event_to_subscribers(event)
+ self.assertEqual(mock_send_mail_text.call_count, 1)
+ address = mock_send_mail_text.call_args[0][1]
+ subject = mock_send_mail_text.call_args[0][3]
+ content = mock_send_mail_text.call_args[0][4]
+ self.assertEqual(address, sub_to_all.email.address)
+ self.assertIn(draft.name, subject)
+ self.assertIn(clist.long_name(), content)
- mailbox_before = len(outbox)
rfc_state = State.objects.get(type="draft", slug="rfc")
- add_state_change_event(draft, system, active_state, rfc_state)
- self.assertEqual(len(outbox), mailbox_before + 1)
- self.assertTrue(draft.name in outbox[-1]["Subject"])
-
-
+ event = add_state_change_event(draft, system, active_state, rfc_state)
+ mock_send_mail_text.reset_mock()
+ notify_event_to_subscribers(event)
+ self.assertEqual(mock_send_mail_text.call_count, 2)
+ addresses = [call_args[0][1] for call_args in mock_send_mail_text.call_args_list]
+ subjects = {call_args[0][3] for call_args in mock_send_mail_text.call_args_list}
+ contents = {call_args[0][4] for call_args in mock_send_mail_text.call_args_list}
+ self.assertCountEqual(
+ addresses,
+ [sub_to_significant.email.address, sub_to_all.email.address],
+ )
+ self.assertEqual(len(subjects), 1)
+ self.assertIn(draft.name, subjects.pop())
+ self.assertEqual(len(contents), 1)
+ self.assertIn(clist.long_name(), contents.pop())
+
+ @mock.patch("ietf.community.utils.notify_event_to_subscribers")
+ def test_notify_event_to_subscribers_task(self, mock_notify):
+ d = DocEventFactory()
+ notify_event_to_subscribers_task(event_id=d.pk)
+ self.assertEqual(mock_notify.call_count, 1)
+ self.assertEqual(mock_notify.call_args, mock.call(d))
+ mock_notify.reset_mock()
+
+ d.delete()
+ notify_event_to_subscribers_task(event_id=d.pk)
+ self.assertFalse(mock_notify.called)
+
From 26ca821f313d8583e47c7a28e31bcc37475a900a Mon Sep 17 00:00:00 2001
From: Robert Sparks
Date: Mon, 15 Jul 2024 12:16:13 -0500
Subject: [PATCH 002/656] Added a note on updating older clones
---
docker/README.md | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/docker/README.md b/docker/README.md
index 2e4b50d8ff..14fcc38995 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -156,6 +156,11 @@ On Windows:
docker compose down -v --rmi all
docker image prune
```
+### Updating an older environment
+
+If you already have a clone, such as from a previous codesprint, and are updating that clone, before starting the datatracker from the updated image:
+* rm ietf/settings_local.py # The startup script will put a new one, appropriate to the current release, in place
+* Execute the `Clean all` sequence above.
### Accessing PostgreSQL Port
@@ -197,4 +202,4 @@ drwxrwxr-x 5 100999 100999 4096 May 25 07:56 client
(etc...)
```
-Try uninstalling Docker Desktop and installing Docker Compose manually. The Docker Compose bundled with Docker Desktop is incompatible with our software. See also [Rootless Docker: file ownership changes #3343](https://github.com/lando/lando/issues/3343), [Docker context desktop-linux has container permission issues #75](https://github.com/docker/desktop-linux/issues/75).
\ No newline at end of file
+Try uninstalling Docker Desktop and installing Docker Compose manually. The Docker Compose bundled with Docker Desktop is incompatible with our software. See also [Rootless Docker: file ownership changes #3343](https://github.com/lando/lando/issues/3343), [Docker context desktop-linux has container permission issues #75](https://github.com/docker/desktop-linux/issues/75).
From 392511f78fc2000b79c45a46b946cbd434d133c9 Mon Sep 17 00:00:00 2001
From: Nicolas Giard
Date: Mon, 15 Jul 2024 15:03:07 -0400
Subject: [PATCH 003/656] chore: Update dependabot.yml
---
.github/dependabot.yml | 14 --------------
1 file changed, 14 deletions(-)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 3d36676762..17d89f1aab 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -9,14 +9,10 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- reviewers:
- - "ngpixel"
- package-ecosystem: "docker"
directory: "/docker"
schedule:
interval: "weekly"
- reviewers:
- - "ngpixel"
- package-ecosystem: "pip"
directory: "/"
schedule:
@@ -27,8 +23,6 @@ updates:
directory: "/"
schedule:
interval: "weekly"
- reviewers:
- - "ngpixel"
groups:
yarn:
patterns:
@@ -37,8 +31,6 @@ updates:
directory: "/playwright"
schedule:
interval: "weekly"
- reviewers:
- - "ngpixel"
groups:
npm:
patterns:
@@ -47,8 +39,6 @@ updates:
directory: "/dev/coverage-action"
schedule:
interval: "weekly"
- reviewers:
- - "ngpixel"
groups:
npm:
patterns:
@@ -57,8 +47,6 @@ updates:
directory: "/dev/deploy-to-container"
schedule:
interval: "weekly"
- reviewers:
- - "ngpixel"
groups:
npm:
patterns:
@@ -67,8 +55,6 @@ updates:
directory: "/dev/diff"
schedule:
interval: "weekly"
- reviewers:
- - "ngpixel"
groups:
npm:
patterns:
From 475cf2fc8516f8eb796ae20a554819dbc2e6e0b1 Mon Sep 17 00:00:00 2001
From: Rich Salz
Date: Mon, 15 Jul 2024 17:32:28 -0400
Subject: [PATCH 004/656] fix: Ensure called from the docker directory (#7689)
---
docker/cleanall | 7 ++++++-
docker/cleandb | 8 +++++++-
2 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/docker/cleanall b/docker/cleanall
index 91eac1764b..c6104aaef9 100755
--- a/docker/cleanall
+++ b/docker/cleanall
@@ -1,5 +1,11 @@
#!/bin/bash
+if test $(basename $PWD ) != "docker"
+then
+ echo "Run this from the docker directory" 1>&2
+ exit 1
+fi
+
read -p "Stop and remove all containers, volumes and images for this project? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
@@ -7,6 +13,5 @@ then
cd ..
echo "Shutting down any instance still running and purge images..."
docker compose down -v --rmi all
- cd docker
echo "Done!"
fi
diff --git a/docker/cleandb b/docker/cleandb
index 322e4639a2..c881503eae 100755
--- a/docker/cleandb
+++ b/docker/cleandb
@@ -1,5 +1,11 @@
#!/bin/bash
+if test $(basename $PWD ) != "docker"
+then
+ echo "Run this from the docker directory" 1>&2
+ exit 1
+fi
+
cd ..
echo "Shutting down any instance still running..."
docker compose down
@@ -9,5 +15,5 @@ docker volume rm -f "${PROJNAME}_postgresdb-data"
echo "Rebuilding the DB image..."
docker compose pull db
docker compose build --no-cache db
-cd docker
+
echo "Done!"
From c9dab33f8387b944925127eaf435e301ec83dd7a Mon Sep 17 00:00:00 2001
From: Nicolas Giard
Date: Mon, 15 Jul 2024 17:32:47 -0400
Subject: [PATCH 005/656] ci: migrate to using secret instead of configmap
(#7685)
---
k8s/README.md | 5 ++
k8s/auth.yaml | 58 ++++++++++++------------
k8s/beat.yaml | 7 ++-
k8s/celery.yaml | 54 +++++++++++-----------
k8s/datatracker.yaml | 54 +++++++++++-----------
k8s/kustomization.yaml | 1 -
k8s/memcached.yaml | 34 ++++++++------
k8s/rabbitmq.yaml | 41 +++++++++--------
k8s/{django-config.yaml => secrets.yaml} | 10 ++--
9 files changed, 140 insertions(+), 124 deletions(-)
create mode 100644 k8s/README.md
rename k8s/{django-config.yaml => secrets.yaml} (93%)
diff --git a/k8s/README.md b/k8s/README.md
new file mode 100644
index 0000000000..73b5978675
--- /dev/null
+++ b/k8s/README.md
@@ -0,0 +1,5 @@
+# Kustomize deployment
+
+## Run locally
+
+The `secrets.yaml` file is provided as a reference only and must be referenced manually in the `kustomization.yaml` file.
\ No newline at end of file
diff --git a/k8s/auth.yaml b/k8s/auth.yaml
index 8aa1d53cbd..bc99af79d3 100644
--- a/k8s/auth.yaml
+++ b/k8s/auth.yaml
@@ -19,34 +19,9 @@ spec:
runAsNonRoot: true
containers:
# -----------------------------------------------------
- # ScoutAPM Container
- # -----------------------------------------------------
- - name: scoutapm
- image: "scoutapp/scoutapm:version-1.4.0"
- imagePullPolicy: IfNotPresent
- # Replace command with one that will shut down on a TERM signal
- # The ./core-agent start command line is from the scoutapm docker image
- command:
- - "sh"
- - "-c"
- - >-
- trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM;
- ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 &
- wait $!
- livenessProbe:
- exec:
- command:
- - "sh"
- - "-c"
- - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'"
- securityContext:
- readOnlyRootFilesystem: true
- runAsUser: 65534 # "nobody" user by default
- runAsGroup: 65534 # "nogroup" group by default
- # -----------------------------------------------------
- # Datatracker Container
+ # Auth Container
# -----------------------------------------------------
- - name: datatracker
+ - name: auth
image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG"
imagePullPolicy: Always
ports:
@@ -72,8 +47,8 @@ spec:
- name: "DEPLOY_UID"
value: "$DEPLOY_UID"
envFrom:
- - configMapRef:
- name: django-config
+ - secretRef:
+ name: dt-secrets-env
securityContext:
allowPrivilegeEscalation: false
capabilities:
@@ -82,6 +57,31 @@ spec:
readOnlyRootFilesystem: true
runAsUser: 1000
runAsGroup: 1000
+ # -----------------------------------------------------
+ # ScoutAPM Container
+ # -----------------------------------------------------
+ - name: scoutapm
+ image: "scoutapp/scoutapm:version-1.4.0"
+ imagePullPolicy: IfNotPresent
+ # Replace command with one that will shut down on a TERM signal
+ # The ./core-agent start command line is from the scoutapm docker image
+ command:
+ - "sh"
+ - "-c"
+ - >-
+ trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM;
+ ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 &
+ wait $!
+ livenessProbe:
+ exec:
+ command:
+ - "sh"
+ - "-c"
+ - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'"
+ securityContext:
+ readOnlyRootFilesystem: true
+ runAsUser: 65534 # "nobody" user by default
+ runAsGroup: 65534 # "nogroup" group by default
volumes:
# To be overriden with the actual shared volume
- name: dt-vol
diff --git a/k8s/beat.yaml b/k8s/beat.yaml
index 99317ab77a..72d74e11e4 100644
--- a/k8s/beat.yaml
+++ b/k8s/beat.yaml
@@ -20,6 +20,9 @@ spec:
securityContext:
runAsNonRoot: true
containers:
+ # -----------------------------------------------------
+ # Beat Container
+ # -----------------------------------------------------
- name: beat
image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG"
imagePullPolicy: Always
@@ -39,8 +42,8 @@ spec:
- name: "CONTAINER_ROLE"
value: "beat"
envFrom:
- - configMapRef:
- name: django-config
+ - secretRef:
+ name: dt-secrets-env
securityContext:
allowPrivilegeEscalation: false
capabilities:
diff --git a/k8s/celery.yaml b/k8s/celery.yaml
index dfb20fa40a..10f58f0161 100644
--- a/k8s/celery.yaml
+++ b/k8s/celery.yaml
@@ -21,31 +21,6 @@ spec:
runAsNonRoot: true
containers:
# -----------------------------------------------------
- # ScoutAPM Container
- # -----------------------------------------------------
- - name: scoutapm
- image: "scoutapp/scoutapm:version-1.4.0"
- imagePullPolicy: IfNotPresent
- # Replace command with one that will shut down on a TERM signal
- # The ./core-agent start command line is from the scoutapm docker image
- command:
- - "sh"
- - "-c"
- - >-
- trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM;
- ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 &
- wait $!
- livenessProbe:
- exec:
- command:
- - "sh"
- - "-c"
- - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'"
- securityContext:
- readOnlyRootFilesystem: true
- runAsUser: 65534 # "nobody" user by default
- runAsGroup: 65534 # "nogroup" group by default
- # -----------------------------------------------------
# Celery Container
# -----------------------------------------------------
- name: celery
@@ -71,8 +46,8 @@ spec:
- name: "CONTAINER_ROLE"
value: "celery"
envFrom:
- - configMapRef:
- name: django-config
+ - secretRef:
+ name: dt-secrets-env
securityContext:
allowPrivilegeEscalation: false
capabilities:
@@ -81,6 +56,31 @@ spec:
readOnlyRootFilesystem: true
runAsUser: 1000
runAsGroup: 1000
+ # -----------------------------------------------------
+ # ScoutAPM Container
+ # -----------------------------------------------------
+ - name: scoutapm
+ image: "scoutapp/scoutapm:version-1.4.0"
+ imagePullPolicy: IfNotPresent
+ # Replace command with one that will shut down on a TERM signal
+ # The ./core-agent start command line is from the scoutapm docker image
+ command:
+ - "sh"
+ - "-c"
+ - >-
+ trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM;
+ ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 &
+ wait $!
+ livenessProbe:
+ exec:
+ command:
+ - "sh"
+ - "-c"
+ - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'"
+ securityContext:
+ readOnlyRootFilesystem: true
+ runAsUser: 65534 # "nobody" user by default
+ runAsGroup: 65534 # "nogroup" group by default
volumes:
# To be overriden with the actual shared volume
- name: dt-vol
diff --git a/k8s/datatracker.yaml b/k8s/datatracker.yaml
index 5ad4336614..81dc048d0b 100644
--- a/k8s/datatracker.yaml
+++ b/k8s/datatracker.yaml
@@ -19,31 +19,6 @@ spec:
runAsNonRoot: true
containers:
# -----------------------------------------------------
- # ScoutAPM Container
- # -----------------------------------------------------
- - name: scoutapm
- image: "scoutapp/scoutapm:version-1.4.0"
- imagePullPolicy: IfNotPresent
- # Replace command with one that will shut down on a TERM signal
- # The ./core-agent start command line is from the scoutapm docker image
- command:
- - "sh"
- - "-c"
- - >-
- trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM;
- ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 &
- wait $!
- livenessProbe:
- exec:
- command:
- - "sh"
- - "-c"
- - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'"
- securityContext:
- readOnlyRootFilesystem: true
- runAsUser: 65534 # "nobody" user by default
- runAsGroup: 65534 # "nogroup" group by default
- # -----------------------------------------------------
# Datatracker Container
# -----------------------------------------------------
- name: datatracker
@@ -72,8 +47,8 @@ spec:
- name: "DEPLOY_UID"
value: "$DEPLOY_UID"
envFrom:
- - configMapRef:
- name: django-config
+ - secretRef:
+ name: dt-secrets-env
securityContext:
allowPrivilegeEscalation: false
capabilities:
@@ -82,6 +57,31 @@ spec:
readOnlyRootFilesystem: true
runAsUser: 1000
runAsGroup: 1000
+ # -----------------------------------------------------
+ # ScoutAPM Container
+ # -----------------------------------------------------
+ - name: scoutapm
+ image: "scoutapp/scoutapm:version-1.4.0"
+ imagePullPolicy: IfNotPresent
+ # Replace command with one that will shut down on a TERM signal
+ # The ./core-agent start command line is from the scoutapm docker image
+ command:
+ - "sh"
+ - "-c"
+ - >-
+ trap './core-agent shutdown --tcp 0.0.0.0:6590' TERM;
+ ./core-agent start --daemonize false --log-level debug --tcp 0.0.0.0:6590 &
+ wait $!
+ livenessProbe:
+ exec:
+ command:
+ - "sh"
+ - "-c"
+ - "./core-agent probe --tcp 0.0.0.0:6590 | grep -q 'Agent found'"
+ securityContext:
+ readOnlyRootFilesystem: true
+ runAsUser: 65534 # "nobody" user by default
+ runAsGroup: 65534 # "nogroup" group by default
initContainers:
- name: migration
image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG"
diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml
index cfc17f35d7..ab381369b2 100644
--- a/k8s/kustomization.yaml
+++ b/k8s/kustomization.yaml
@@ -9,6 +9,5 @@ resources:
- beat.yaml
- celery.yaml
- datatracker.yaml
- - django-config.yaml
- memcached.yaml
- rabbitmq.yaml
diff --git a/k8s/memcached.yaml b/k8s/memcached.yaml
index e94066c9e0..4b362c88c1 100644
--- a/k8s/memcached.yaml
+++ b/k8s/memcached.yaml
@@ -16,12 +16,16 @@ spec:
securityContext:
runAsNonRoot: true
containers:
- - image: "quay.io/prometheus/memcached-exporter:v0.14.3"
+ # -----------------------------------------------------
+ # Memcached
+ # -----------------------------------------------------
+ - image: "memcached:1.6-alpine"
imagePullPolicy: IfNotPresent
- name: memcached-exporter
+ args: ["-m", "1024"]
+ name: memcached
ports:
- - name: metrics
- containerPort: 9150
+ - name: memcached
+ containerPort: 11211
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
@@ -29,15 +33,18 @@ spec:
drop:
- ALL
readOnlyRootFilesystem: true
- runAsUser: 65534 # nobody
- runAsGroup: 65534 # nobody
- - image: "memcached:1.6-alpine"
+ # memcached image sets up uid/gid 11211
+ runAsUser: 11211
+ runAsGroup: 11211
+ # -----------------------------------------------------
+ # Memcached Exporter for Prometheus
+ # -----------------------------------------------------
+ - image: "quay.io/prometheus/memcached-exporter:v0.14.3"
imagePullPolicy: IfNotPresent
- args: ["-m", "1024"]
- name: memcached
+ name: memcached-exporter
ports:
- - name: memcached
- containerPort: 11211
+ - name: metrics
+ containerPort: 9150
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
@@ -45,9 +52,8 @@ spec:
drop:
- ALL
readOnlyRootFilesystem: true
- # memcached image sets up uid/gid 11211
- runAsUser: 11211
- runAsGroup: 11211
+ runAsUser: 65534 # nobody
+ runAsGroup: 65534 # nobody
dnsPolicy: ClusterFirst
restartPolicy: Always
terminationGracePeriodSeconds: 30
diff --git a/k8s/rabbitmq.yaml b/k8s/rabbitmq.yaml
index 132ca79ded..b016b3a5ab 100644
--- a/k8s/rabbitmq.yaml
+++ b/k8s/rabbitmq.yaml
@@ -15,23 +15,6 @@ spec:
spec:
securityContext:
runAsNonRoot: true
- initContainers:
- # -----------------------------------------------------
- # Init RabbitMQ data
- # -----------------------------------------------------
- - name: init-rabbitmq
- image: busybox:stable
- command:
- - "sh"
- - "-c"
- - "mkdir -p -m700 /mnt/rabbitmq && chown 100:101 /mnt/rabbitmq"
- securityContext:
- runAsNonRoot: false
- runAsUser: 0
- readOnlyRootFilesystem: true
- volumeMounts:
- - name: "rabbitmq-data"
- mountPath: "/mnt"
containers:
# -----------------------------------------------------
# RabbitMQ Container
@@ -52,8 +35,11 @@ spec:
- name: rabbitmq-config
mountPath: "/etc/rabbitmq"
env:
- - name: "CELERY_PASSWORD"
- value: "this-is-a-secret"
+ - name: CELERY_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: dt-secrets-env
+ key: CELERY_PASSWORD
livenessProbe:
exec:
command: ["rabbitmq-diagnostics", "-q", "ping"]
@@ -76,6 +62,23 @@ spec:
# rabbitmq image sets up uid/gid 100/101
runAsUser: 100
runAsGroup: 101
+ initContainers:
+ # -----------------------------------------------------
+ # Init RabbitMQ data
+ # -----------------------------------------------------
+ - name: init-rabbitmq
+ image: busybox:stable
+ command:
+ - "sh"
+ - "-c"
+ - "mkdir -p -m700 /mnt/rabbitmq && chown 100:101 /mnt/rabbitmq"
+ securityContext:
+ runAsNonRoot: false
+ runAsUser: 0
+ readOnlyRootFilesystem: true
+ volumeMounts:
+ - name: "rabbitmq-data"
+ mountPath: "/mnt"
volumes:
- name: rabbitmq-tmp
emptyDir:
diff --git a/k8s/django-config.yaml b/k8s/secrets.yaml
similarity index 93%
rename from k8s/django-config.yaml
rename to k8s/secrets.yaml
index 07e2d710de..4e76a86a53 100644
--- a/k8s/django-config.yaml
+++ b/k8s/secrets.yaml
@@ -1,9 +1,9 @@
apiVersion: v1
-kind: ConfigMap
+kind: Secret
metadata:
- name: django-config
-data:
- # n.b., these are debug values / non-secret secrets
+ name: secrets-env
+type: Opaque
+stringData:
DATATRACKER_SERVER_MODE: "development" # development for staging, production for production
DATATRACKER_ADMINS: |-
Robert Sparks
@@ -80,4 +80,4 @@ data:
# Scout configuration
DATATRACKER_SCOUT_KEY: "this-is-the-scout-key"
- DATATRACKER_SCOUT_NAME: "StagingDatatracker"
+ DATATRACKER_SCOUT_NAME: "StagingDatatracker"
\ No newline at end of file
From 17e0f573b3e0814e833788d4f022d1ce1e81703b Mon Sep 17 00:00:00 2001
From: Robert Sparks
Date: Mon, 15 Jul 2024 16:33:00 -0500
Subject: [PATCH 006/656] fix: check correct state machine when clearing status
change ballots (#7684)
* fix: check correct state machine when clearing status change ballots
Fixes #7335
* fix: Improve ballot clearing tests
* fix: look at the right state machines for defer state for a ballot
* fix: also do the right thing with conflrev defers
---
ietf/doc/tests_ballot.py | 7 +++++-
ietf/doc/tests_status_change.py | 40 +++++++++++++++++++++++++++++++++
ietf/doc/views_ballot.py | 13 ++++++++++-
3 files changed, 58 insertions(+), 2 deletions(-)
diff --git a/ietf/doc/tests_ballot.py b/ietf/doc/tests_ballot.py
index e18b2abfd9..034ba6f4b2 100644
--- a/ietf/doc/tests_ballot.py
+++ b/ietf/doc/tests_ballot.py
@@ -806,7 +806,7 @@ def test_clear_ballot(self):
ballot = create_ballot_if_not_open(None, draft, ad, 'approve')
old_ballot_id = ballot.id
draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="iesg-eva"))
- url = urlreverse('ietf.doc.views_ballot.clear_ballot', kwargs=dict(name=draft.name,ballot_type_slug=draft.ballot_open('approve').ballot_type.slug))
+ url = urlreverse('ietf.doc.views_ballot.clear_ballot', kwargs=dict(name=draft.name,ballot_type_slug="approve"))
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@@ -816,6 +816,11 @@ def test_clear_ballot(self):
self.assertIsNotNone(ballot)
self.assertEqual(ballot.ballotpositiondocevent_set.count(),0)
self.assertNotEqual(old_ballot_id, ballot.id)
+ # It's not valid to clear a ballot of a type where there's no matching state
+ url = urlreverse('ietf.doc.views_ballot.clear_ballot', kwargs=dict(name=draft.name,ballot_type_slug="statchg"))
+ r = self.client.post(url,{})
+ self.assertEqual(r.status_code, 404)
+
def test_ballot_downref_approve(self):
ad = Person.objects.get(name="Areað Irector")
diff --git a/ietf/doc/tests_status_change.py b/ietf/doc/tests_status_change.py
index 229390447c..bec48ed4ef 100644
--- a/ietf/doc/tests_status_change.py
+++ b/ietf/doc/tests_status_change.py
@@ -484,7 +484,47 @@ def verify_relations(doc,target_name,status):
verify_relations(doc,'rfc9998','tobcp' )
verify_relations(doc,'rfc14' ,'tohist')
self.assertTrue(doc.latest_event(DocEvent,type="added_comment").desc.startswith('Affected RFC list changed.'))
+
+ def test_clear_ballot(self):
+ doc = Document.objects.get(name='status-change-imaginary-mid-review')
+ url = urlreverse('ietf.doc.views_ballot.clear_ballot',kwargs=dict(name=doc.name, ballot_type_slug="statchg"))
+ login_testing_unauthorized(self, "secretary", url)
+
+ # Some additional setup
+ doc.relateddocument_set.create(target=Document.objects.get(name='rfc9999'),relationship_id='tois')
+ doc.relateddocument_set.create(target=Document.objects.get(name='rfc9998'),relationship_id='tohist')
+ create_ballot_if_not_open(None, doc, Person.objects.get(user__username="secretary"), "statchg")
+ doc.set_state(State.objects.get(slug='iesgeval',type='statchg'))
+ old_ballot = doc.ballot_open("statchg")
+ self.assertIsNotNone(old_ballot)
+
+ r = self.client.post(url, dict())
+ self.assertEqual(r.status_code,302)
+ new_ballot = doc.ballot_open("statchg")
+ self.assertIsNotNone(new_ballot)
+ self.assertNotEqual(new_ballot, old_ballot)
+ self.assertEqual(doc.get_state_slug("statchg"),"iesgeval")
+
+ def test_clear_deferred_ballot(self):
+ doc = Document.objects.get(name='status-change-imaginary-mid-review')
+ url = urlreverse('ietf.doc.views_ballot.clear_ballot',kwargs=dict(name=doc.name, ballot_type_slug="statchg"))
+ login_testing_unauthorized(self, "secretary", url)
+
+ # Some additional setup
+ doc.relateddocument_set.create(target=Document.objects.get(name='rfc9999'),relationship_id='tois')
+ doc.relateddocument_set.create(target=Document.objects.get(name='rfc9998'),relationship_id='tohist')
+ create_ballot_if_not_open(None, doc, Person.objects.get(user__username="secretary"), "statchg")
+ doc.set_state(State.objects.get(slug='defer',type='statchg'))
+ old_ballot = doc.ballot_open("statchg")
+ self.assertIsNotNone(old_ballot)
+ r = self.client.post(url, dict())
+ self.assertEqual(r.status_code,302)
+ new_ballot = doc.ballot_open("statchg")
+ self.assertIsNotNone(new_ballot)
+ self.assertNotEqual(new_ballot, old_ballot)
+ self.assertEqual(doc.get_state_slug("statchg"),"iesgeval")
+
def setUp(self):
super().setUp()
IndividualRfcFactory(rfc_number=14,std_level_id='unkn') # draft was never issued
diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py
index 02b55249d6..83ccb07be6 100644
--- a/ietf/doc/views_ballot.py
+++ b/ietf/doc/views_ballot.py
@@ -399,11 +399,22 @@ def send_ballot_comment(request, name, ballot_id):
def clear_ballot(request, name, ballot_type_slug):
"""Clear all positions and discusses on every open ballot for a document."""
doc = get_object_or_404(Document, name=name)
+ # If there's no appropriate ballot type state, clearing would be an invalid action.
+ # This will need to be updated if we ever allow defering IRTF ballots
+ if ballot_type_slug == "approve":
+ state_machine = "draft-iesg"
+ elif ballot_type_slug in ["statchg","conflrev"]:
+ state_machine = ballot_type_slug
+ else:
+ state_machine = None
+ state_slug = state_machine and doc.get_state_slug(state_machine)
+ if state_machine is None or state_slug is None:
+ raise Http404
if request.method == 'POST':
by = request.user.person
if close_ballot(doc, by, ballot_type_slug):
create_ballot_if_not_open(request, doc, by, ballot_type_slug)
- if doc.get_state('draft-iesg').slug == 'defer':
+ if state_slug == "defer":
do_undefer_ballot(request,doc)
return redirect("ietf.doc.views_doc.document_main", name=doc.name)
From 18bb793b2d3cf6e5e4df56eed70577215bb46b25 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 15 Jul 2024 18:45:51 -0300
Subject: [PATCH 007/656] feat: add nginx, robots.txt, HTTP headers (#7683)
* feat: nginx + robots.txt
* feat: minimal /health/ endpoint
* ci: startupProbe for datatracker pod
* ci: probe auth pod; set timeoutSeconds
* feat: add CSP and other headers to nginx
* fix: typo in nginx.conf
* feat: split auth/dt nginx confs
* test: test health endpoint
* ci: auth service on port 80
We'll remove http-old (8080) in the future.
* ci: rename auth container/nginx cfg
---
ietf/urls.py | 2 ++
ietf/utils/tests.py | 9 ++++++++
k8s/auth.yaml | 43 +++++++++++++++++++++++++++++++++-----
k8s/datatracker.yaml | 37 ++++++++++++++++++++++++++++----
k8s/kustomization.yaml | 2 ++
k8s/nginx-auth.conf | 34 ++++++++++++++++++++++++++++++
k8s/nginx-datatracker.conf | 23 ++++++++++++++++++++
7 files changed, 141 insertions(+), 9 deletions(-)
create mode 100644 k8s/nginx-auth.conf
create mode 100644 k8s/nginx-datatracker.conf
diff --git a/ietf/urls.py b/ietf/urls.py
index 78dbb861b2..4b29a3aa81 100644
--- a/ietf/urls.py
+++ b/ietf/urls.py
@@ -5,6 +5,7 @@
from django.contrib import admin
from django.contrib.sitemaps import views as sitemap_views
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+from django.http import HttpResponse
from django.urls import include, path
from django.views import static as static_view
from django.views.generic import TemplateView
@@ -35,6 +36,7 @@
urlpatterns = [
url(r'^$', views_search.frontpage),
+ url(r'^health/', lambda _: HttpResponse()),
url(r'^accounts/', include('ietf.ietfauth.urls')),
url(r'^admin/', admin.site.urls),
url(r'^admin/docs/', include('django.contrib.admindocs.urls')),
diff --git a/ietf/utils/tests.py b/ietf/utils/tests.py
index 08adefc826..476c257a38 100644
--- a/ietf/utils/tests.py
+++ b/ietf/utils/tests.py
@@ -679,3 +679,12 @@ class TestForm(Form):
self.assertTrue(changed_form.has_changed())
unchanged_form = TestForm(initial={'test_field': [1]}, data={'test_field': [1]})
self.assertFalse(unchanged_form.has_changed())
+
+
+class HealthTests(TestCase):
+ def test_health(self):
+ self.assertEqual(
+ self.client.get("/health/").status_code,
+ 200,
+ )
+
diff --git a/k8s/auth.yaml b/k8s/auth.yaml
index bc99af79d3..66627ed450 100644
--- a/k8s/auth.yaml
+++ b/k8s/auth.yaml
@@ -24,10 +24,6 @@ spec:
- name: auth
image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG"
imagePullPolicy: Always
- ports:
- - containerPort: 8000
- name: http
- protocol: TCP
volumeMounts:
- name: dt-vol
mountPath: /a
@@ -49,6 +45,14 @@ spec:
envFrom:
- secretRef:
name: dt-secrets-env
+ startupProbe:
+ httpGet:
+ port: 8000
+ path: /health/
+ initialDelaySeconds: 10
+ periodSeconds: 5
+ failureThreshold: 30
+ timeoutSeconds: 3
securityContext:
allowPrivilegeEscalation: false
capabilities:
@@ -58,6 +62,28 @@ spec:
runAsUser: 1000
runAsGroup: 1000
# -----------------------------------------------------
+ # Nginx Container
+ # -----------------------------------------------------
+ - name: nginx
+ image: "ghcr.io/nginxinc/nginx-unprivileged:1.27"
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ livenessProbe:
+ httpGet:
+ port: 8080
+ path: /health/nginx
+ securityContext:
+ readOnlyRootFilesystem: true
+ volumeMounts:
+ - name: nginx-tmp
+ mountPath: /tmp
+ - name: dt-cfg
+ mountPath: /etc/nginx/conf.d/auth.conf
+ subPath: nginx-auth.conf
+ # -----------------------------------------------------
# ScoutAPM Container
# -----------------------------------------------------
- name: scoutapm
@@ -97,6 +123,9 @@ spec:
- name: dt-cfg
configMap:
name: files-cfgmap
+ - name: nginx-tmp
+ emptyDir:
+ sizeLimit: "500Mi"
dnsPolicy: ClusterFirst
restartPolicy: Always
terminationGracePeriodSeconds: 60
@@ -108,9 +137,13 @@ metadata:
spec:
type: ClusterIP
ports:
- - port: 8080
+ - port: 80
targetPort: http
protocol: TCP
name: http
+ - port: 8080
+ targetPort: http
+ protocol: TCP
+ name: http-old
selector:
app: auth
diff --git a/k8s/datatracker.yaml b/k8s/datatracker.yaml
index 81dc048d0b..59a35fbbc6 100644
--- a/k8s/datatracker.yaml
+++ b/k8s/datatracker.yaml
@@ -24,10 +24,6 @@ spec:
- name: datatracker
image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG"
imagePullPolicy: Always
- ports:
- - containerPort: 8000
- name: http
- protocol: TCP
volumeMounts:
- name: dt-vol
mountPath: /a
@@ -49,6 +45,14 @@ spec:
envFrom:
- secretRef:
name: dt-secrets-env
+ startupProbe:
+ httpGet:
+ port: 8000
+ path: /health/
+ initialDelaySeconds: 10
+ periodSeconds: 5
+ failureThreshold: 30
+ timeoutSeconds: 3
securityContext:
allowPrivilegeEscalation: false
capabilities:
@@ -58,6 +62,28 @@ spec:
runAsUser: 1000
runAsGroup: 1000
# -----------------------------------------------------
+ # Nginx Container
+ # -----------------------------------------------------
+ - name: nginx
+ image: "ghcr.io/nginxinc/nginx-unprivileged:1.27"
+ imagePullPolicy: IfNotPresent
+ ports:
+ - containerPort: 8080
+ name: http
+ protocol: TCP
+ livenessProbe:
+ httpGet:
+ port: 8080
+ path: /health/nginx
+ securityContext:
+ readOnlyRootFilesystem: true
+ volumeMounts:
+ - name: nginx-tmp
+ mountPath: /tmp
+ - name: dt-cfg
+ mountPath: /etc/nginx/conf.d/datatracker.conf
+ subPath: nginx-datatracker.conf
+ # -----------------------------------------------------
# ScoutAPM Container
# -----------------------------------------------------
- name: scoutapm
@@ -126,6 +152,9 @@ spec:
- name: dt-cfg
configMap:
name: files-cfgmap
+ - name: nginx-tmp
+ emptyDir:
+ sizeLimit: "500Mi"
dnsPolicy: ClusterFirst
restartPolicy: Always
terminationGracePeriodSeconds: 60
diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml
index ab381369b2..ba8b8a5826 100644
--- a/k8s/kustomization.yaml
+++ b/k8s/kustomization.yaml
@@ -3,6 +3,8 @@ namePrefix: dt-
configMapGenerator:
- name: files-cfgmap
files:
+ - nginx-auth.conf
+ - nginx-datatracker.conf
- settings_local.py
resources:
- auth.yaml
diff --git a/k8s/nginx-auth.conf b/k8s/nginx-auth.conf
new file mode 100644
index 0000000000..4cbc8a0a51
--- /dev/null
+++ b/k8s/nginx-auth.conf
@@ -0,0 +1,34 @@
+server {
+ listen 8080 default_server;
+ server_name _;
+
+ # 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$ {
+ return 200;
+ }
+
+ location ~ ^/robots.txt$ {
+ add_header Content-Type text/plain;
+ return 200 "User-agent: *\nDisallow: /\n";
+ }
+
+ location ~ ^/accounts/create.* {
+ return 302 https://datatracker.ietf.org/accounts/create;
+ }
+
+ # n.b. (?!...) is a negative lookahead group
+ location ~ ^(/(?!(api/openid/|accounts/login/|accounts/logout/|accounts/reset/|person/.*/photo|group/groupmenu.json)).*) {
+ deny all;
+ }
+
+ 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-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
new file mode 100644
index 0000000000..63c985463c
--- /dev/null
+++ b/k8s/nginx-datatracker.conf
@@ -0,0 +1,23 @@
+server {
+ listen 8080 default_server;
+ server_name _;
+
+ location /health/nginx {
+ return 200;
+ }
+
+ location /robots.txt {
+ add_header Content-Type text/plain;
+ return 200 "User-agent: *\nDisallow: /doc/pdf/\n";
+ }
+
+ 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";
+ proxy_set_header Host $${keepempty}host;
+ proxy_set_header Connection close;
+ proxy_set_header X-Request-Start "t=${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;
+ }
+}
From 30510d97d8f0a34e2a683c4b0582e99d5990b541 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 15 Jul 2024 19:57:25 -0300
Subject: [PATCH 008/656] ci: fix migration container secrets (#7694)
---
k8s/datatracker.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/k8s/datatracker.yaml b/k8s/datatracker.yaml
index 59a35fbbc6..9e1ead1a90 100644
--- a/k8s/datatracker.yaml
+++ b/k8s/datatracker.yaml
@@ -115,8 +115,8 @@ spec:
- name: "CONTAINER_ROLE"
value: "migrations"
envFrom:
- - configMapRef:
- name: django-config
+ - secretRef:
+ name: dt-secrets-env
securityContext:
allowPrivilegeEscalation: false
capabilities:
From b00dfd3c9909cf6d1be5868fcf4b56d672b6fd04 Mon Sep 17 00:00:00 2001
From: Matthew Holloway
Date: Wed, 17 Jul 2024 12:31:00 +1200
Subject: [PATCH 009/656] feat: Overflow shadows
---
ietf/static/css/ietf.scss | 17 +++++++++++++++++
ietf/static/js/ietf.js | 25 ++++++++++++++++++++++++-
2 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/ietf/static/css/ietf.scss b/ietf/static/css/ietf.scss
index 062358c0e2..5bd520f041 100644
--- a/ietf/static/css/ietf.scss
+++ b/ietf/static/css/ietf.scss
@@ -1188,3 +1188,20 @@ blockquote {
padding-left: 1rem;
border-left: solid 1px var(--bs-body-color);
}
+
+.overflow-shadows {
+ transition: box-shadow 0.5s;
+}
+
+.overflow-shadows--both {
+ box-shadow: inset 0px 21px 18px -20px var(--bs-body-color),
+ inset 0px -21px 18px -20px var(--bs-body-color);
+}
+
+.overflow-shadows--top-only {
+ box-shadow: inset 0px 21px 18px -20px var(--bs-body-color);
+}
+
+.overflow-shadows--bottom-only {
+ box-shadow: inset 0px -21px 18px -20px var(--bs-body-color);
+}
diff --git a/ietf/static/js/ietf.js b/ietf/static/js/ietf.js
index 74fd39a85f..dde00c6d1e 100644
--- a/ietf/static/js/ietf.js
+++ b/ietf/static/js/ietf.js
@@ -91,6 +91,27 @@ $(document)
// });
});
+function overflowShadows(el) {
+ function handleScroll(){
+ const canScrollUp = el.scrollTop > 0
+ const canScrollDown = el.offsetHeight + el.scrollTop < el.scrollHeight
+ el.classList.toggle("overflow-shadows--both", canScrollUp && canScrollDown)
+ el.classList.toggle("overflow-shadows--top-only", canScrollUp && !canScrollDown)
+ el.classList.toggle("overflow-shadows--bottom-only", !canScrollUp && canScrollDown)
+ }
+
+ el.addEventListener("scroll", handleScroll, {passive: true})
+ handleScroll()
+
+ const observer = new IntersectionObserver(handleScroll)
+ observer.observe(el) // el won't have scrollTop etc when hidden, so we need to recalculate when it's revealed
+
+ return () => {
+ el.removeEventListener("scroll", handleScroll)
+ observer.unobserve(el)
+ }
+}
+
$(document)
.ready(function () {
// load data for the menu
@@ -108,7 +129,7 @@ $(document)
}
attachTo.find(".dropdown-menu")
.remove();
- var menu = ['
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 017/656] 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 018/656] 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 019/656] 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 020/656] 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 021/656] 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 022/656] 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 023/656] 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 024/656] 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 025/656] 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 026/656] 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 027/656] 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 028/656] 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 029/656] 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 030/656] 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 031/656] 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 032/656] 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 %}
|