Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions ietf/doc/forms.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2013-2020, All Rights Reserved
# Copyright The IETF Trust 2013-2025, All Rights Reserved
# -*- coding: utf-8 -*-


Expand All @@ -9,7 +9,7 @@
from django.core.validators import validate_email

from ietf.doc.fields import SearchableDocumentField, SearchableDocumentsField
from ietf.doc.models import RelatedDocument, DocExtResource
from ietf.doc.models import RelatedDocument, DocExtResource, State
from ietf.iesg.models import TelechatDate
from ietf.iesg.utils import telechat_page_count
from ietf.person.fields import SearchablePersonField, SearchablePersonsField
Expand Down Expand Up @@ -61,7 +61,7 @@ class DocAuthorChangeBasisForm(forms.Form):
basis = forms.CharField(max_length=255,
label='Reason for change',
help_text='What is the source or reasoning for the changes to the author list?')

class AdForm(forms.Form):
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type='area').order_by('name'),
label="Shepherding AD", empty_label="(None)", required=True)
Expand Down Expand Up @@ -288,3 +288,10 @@ def clean_name_fragment(self):
if any(c in name_fragment for c in disallowed_characters):
raise ValidationError(f"The following characters are disallowed: {', '.join(disallowed_characters)}")
return name_fragment


class ChangeStatementStateForm(forms.Form):
state = forms.ModelChoiceField(
State.objects.filter(used=True, type="statement"),
empty_label=None,
)
35 changes: 34 additions & 1 deletion ietf/doc/tests_statement.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2023, All Rights Reserved
# Copyright The IETF Trust 2023-2025, All Rights Reserved

import debug # pyflakes:ignore

Expand Down Expand Up @@ -372,3 +372,36 @@ def test_submit_non_markdown_formats(self):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue("Unexpected content" in q("#id_statement_file").next().text())

def test_change_statement_state(self):
statement = StatementFactory() # starts in "active" state
active_state = State.objects.get(type_id="statement", slug="active")
replaced_state = State.objects.get(type_id="statement", slug="replaced")
url = urlreverse(
"ietf.doc.views_statement.change_statement_state",
kwargs={"name": statement.name},
)

events_before = statement.docevent_set.count()
login_testing_unauthorized(self, "secretary", url)

r = self.client.get(url)
self.assertEqual(r.status_code,200)

r = self.client.post(url, {"state": active_state.pk}, follow=True)
self.assertContains(r, "State not changed", status_code=200)
statement = Document.objects.get(pk=statement.pk) # bust the state cache
self.assertEqual(statement.get_state(), active_state)

r = self.client.post(url, {"state": replaced_state.pk}, follow=True)
self.assertContains(r, "State changed to", status_code=200)
statement = Document.objects.get(pk=statement.pk) # bust the state cache
self.assertEqual(statement.get_state(), replaced_state)

events_after = statement.docevent_set.count()
self.assertEqual(events_after, events_before + 1)
event = statement.docevent_set.first()
self.assertEqual(event.type, "changed_state")
self.assertEqual(
event.desc, "Statement State changed to <b>Replaced</b> from Active"
)
3 changes: 2 additions & 1 deletion ietf/doc/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright The IETF Trust 2009-2023, All Rights Reserved
# Copyright The IETF Trust 2009-2025, All Rights Reserved
# -*- coding: utf-8 -*-
# Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
Expand Down Expand Up @@ -145,6 +145,7 @@
url(r'^%(name)s/edit/adopt/$' % settings.URL_REGEXPS, views_draft.adopt_draft),
url(r'^%(name)s/edit/release/$' % settings.URL_REGEXPS, views_draft.release_draft),
url(r'^%(name)s/edit/state/(?P<state_type>draft-stream-[a-z]+)/$' % settings.URL_REGEXPS, views_draft.change_stream_state),
url(r'^%(name)s/edit/state/statement/$' % settings.URL_REGEXPS, views_statement.change_statement_state),

url(r'^%(name)s/edit/clearballot/(?P<ballot_type_slug>[\w-]+)/$' % settings.URL_REGEXPS, views_ballot.clear_ballot),
url(r'^%(name)s/edit/deferballot/$' % settings.URL_REGEXPS, views_ballot.defer_ballot),
Expand Down
45 changes: 43 additions & 2 deletions ietf/doc/views_statement.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
# Copyright The IETF Trust 2023, All Rights Reserved
# Copyright The IETF Trust 2023-2025, All Rights Reserved
from django.contrib import messages

import debug # pyflakes: ignore

from pathlib import Path

from django import forms
from django.conf import settings
from django.http import FileResponse, Http404
from django.http import FileResponse, Http404, HttpResponseRedirect
from django.views.decorators.cache import cache_control
from django.shortcuts import get_object_or_404, render, redirect
from django.template.loader import render_to_string

from ietf.doc.forms import ChangeStatementStateForm
from ietf.doc.utils import add_state_change_event
from ietf.utils import markdown
from django.utils.html import escape

Expand Down Expand Up @@ -278,3 +282,40 @@ def new_statement(request):
}
form = NewStatementForm(initial=init)
return render(request, "doc/statement/new_statement.html", {"form": form})


@role_required("Secretariat")
def change_statement_state(request, name):
"""Change state of a statement Document"""
statement = get_object_or_404(
Document.objects.filter(type_id="statement"),
name=name,
)
if request.method == "POST":
form = ChangeStatementStateForm(request.POST)
if form.is_valid():
new_state = form.cleaned_data["state"]
prev_state = statement.get_state()
if new_state == prev_state:
messages.info(request, f"State not changed, remains {prev_state}.")
else:
statement.set_state(new_state)
e = add_state_change_event(
statement,
request.user.person,
prev_state,
new_state,
)
statement.save_with_history([e])
messages.success(request, f"State changed to {new_state}.")
return HttpResponseRedirect(statement.get_absolute_url())
else:
form = ChangeStatementStateForm(initial={"state": statement.get_state()})
return render(
request,
"doc/statement/change_statement_state.html",
{
"form": form,
"statement": statement,
},
)
9 changes: 7 additions & 2 deletions ietf/templates/doc/document_statement.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2023, All Rights Reserved #}
{# Copyright The IETF Trust 2023-2025, All Rights Reserved #}
{% load origin %}
{% load static %}
{% load ietf_filters %}
Expand Down Expand Up @@ -49,7 +49,12 @@
<th scope="row">
<a href="{% url 'ietf.doc.views_help.state_help' type='statement' %}">State</a>
</th>
<td class="edit"></td>
<td class="edit">{% if can_manage %}
<a class="btn btn-primary btn-sm"
href="{% url 'ietf.doc.views_statement.change_statement_state' name=doc.name %}">
Edit
</a>
{% endif %}</td>
<td id="statement-state">
{% if doc.get_state %}
<span title="{{ doc.get_state.desc }}" class="badge rounded-pill {% if doc.get_state.name|slugify == 'active' %}text-bg-success{% else %}text-bg-warning{% endif %}">{{ doc.get_state.name }}</span>
Expand Down
22 changes: 22 additions & 0 deletions ietf/templates/doc/statement/change_statement_state.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{# Copyright The IETF Trust 2025, All Rights Reserved #}
{% extends "base.html" %}
{% load origin %}
{% load django_bootstrap5 %}
{% block title %}Change state for {{ statement }}{% endblock %}
{% block content %}
{% origin %}
<h1>
Change state
<br>
<small class="text-body-secondary">{{ statement }}</small>
</h1>
<form class="mt-3" method="post">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-primary">Submit</button>
<a class="btn btn-secondary float-end"
href="{{ statement.get_absolute_url }}">
Back
</a>
</form>
{% endblock %}
Loading