Skip to content

Commit 3728911

Browse files
feat: Move IESG agenda items from filesystem to DB (ietf-tools#5366)
* feat: Add TelechatAgendaContent model and related support * feat: Add UI for managing TelechatAgendaContents * refactor: Rename _view view to _manage * feat: Add a view to dump the TelechatAgendaContent as text/plain * refactor: Point agenda_data() helpers at content in the DB * refactor: Replace references to settings URLs/paths with new plumbing * chore: Remove now-obsolete settings from settings.py * feat: Link to telechat_agenda_content_manage view from iesg agenda * fix: Use correct view name * feat: Link from agenda content management page to IESG agenda view * chore: Create resources * chore: Add new names to names.json * chore: Renumber migration after rebase * chore: Remove unused import * fix: Clean up partially removed code * chore: Add admin model for TelechatAgendaContent * chore: Simplify __str__ method for TelechatAgendaContent * test: Add TelechatAgendaContentFactory * test: Test the fill_in_agenda_administrivia() function * test: Test that agenda contains action_items content * test: Test that sensitive agenda links are restricted by role * test: Test the telechat_agenda_content_view view * test: Add test of telechat_agenda_content_edit view * fix: Add type attribute to button to satisfy html validator * test: Filter TelechatAgendaSectionName to used=True for tests * test: More thoroughly test for likely(ish) permission errors * fix: Fix typo in "tablist" role * test: Test telechat_agenda_content_manage view * style: Put back newlines at EOF * chore: Add admin for TelechatAgendaSectionName * chore: Renumber migrations * fix: Depend on the correct migration Forgot to update the number, but was also depending on the wrong migration.
1 parent 24277f6 commit 3728911

20 files changed

Lines changed: 456 additions & 38 deletions

ietf/iesg/admin.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import debug # pyflakes:ignore
44

55
from ietf.doc.models import TelechatDocEvent
6-
from ietf.iesg.models import TelechatDate, TelechatAgendaItem
6+
from ietf.iesg.models import TelechatDate, TelechatAgendaItem, TelechatAgendaContent
77

88
class TelechatAgendaItemAdmin(admin.ModelAdmin):
99
pass
@@ -22,3 +22,6 @@ def save_model(self, request, obj, form, change):
2222

2323
admin.site.register(TelechatDate, TelechatDateAdmin)
2424

25+
class TelechatAgendaContentAdmin(admin.ModelAdmin):
26+
list_display = ('section',)
27+
admin.site.register(TelechatAgendaContent, TelechatAgendaContentAdmin)

ietf/iesg/agenda.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
# utilities for constructing agendas for IESG telechats
66

7-
import io
87
import datetime
98
from collections import OrderedDict
109

@@ -15,7 +14,7 @@
1514

1615
from ietf.doc.models import Document, LastCallDocEvent, ConsensusDocEvent
1716
from ietf.doc.utils_search import fill_in_telechat_date
18-
from ietf.iesg.models import TelechatDate, TelechatAgendaItem
17+
from ietf.iesg.models import TelechatDate, TelechatAgendaItem, TelechatAgendaContent
1918
from ietf.review.utils import review_assignments_to_list_for_docs
2019
from ietf.utils.timezone import date_today, make_aware
2120

@@ -140,20 +139,18 @@ def agenda_sections():
140139
])
141140

142141
def fill_in_agenda_administrivia(date, sections):
143-
extra_info_files = (
144-
("1.1", "roll_call", settings.IESG_ROLL_CALL_FILE),
145-
("1.3", "minutes", settings.IESG_MINUTES_FILE),
146-
("1.4", "action_items", settings.IESG_TASK_FILE),
147-
)
142+
extra_info = (
143+
("1.1", "roll_call"),
144+
("1.3", "minutes"),
145+
("1.4", "action_items"),
146+
)
148147

149-
for s, key, filename in extra_info_files:
148+
for s, key in extra_info:
150149
try:
151-
with io.open(filename, 'r', encoding='utf-8', errors='replace') as f:
152-
t = f.read().strip()
153-
except IOError:
154-
t = "(Error reading %s)" % filename
155-
156-
sections[s]["text"] = t
150+
text = TelechatAgendaContent.objects.get(section__slug=key).text
151+
except TelechatAgendaContent.DoesNotExist:
152+
text = ""
153+
sections[s]["text"] = text
157154

158155
def fill_in_agenda_docs(date, sections, docs=None):
159156
if not docs:

ietf/iesg/factories.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import debug # pyflakes:ignore
55
import factory
66

7-
from ietf.iesg.models import TelechatAgendaItem
7+
from ietf.iesg.models import TelechatAgendaItem, TelechatAgendaContent
88

99

1010
class IESGMgmtItemFactory(factory.django.DjangoModelFactory):
@@ -14,3 +14,10 @@ class Meta:
1414
type = 3
1515
text = factory.Faker('paragraph', nb_sentences=3)
1616
title = factory.Faker('sentence', nb_words=3)
17+
18+
19+
class TelechatAgendaContentFactory(factory.django.DjangoModelFactory):
20+
class Meta:
21+
model = TelechatAgendaContent
22+
23+
text = factory.Faker('paragraph', nb_sentences=5)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 2.2.28 on 2023-03-10 16:47
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('name', '0002_telechatagendasectionname'),
11+
('iesg', '0001_initial'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='TelechatAgendaContent',
17+
fields=[
18+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19+
('text', models.TextField(blank=True)),
20+
('section', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='name.TelechatAgendaSectionName')),
21+
],
22+
),
23+
]

ietf/iesg/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from django.conf import settings
4040
from django.db import models
4141

42+
from ietf.name.models import TelechatAgendaSectionName
4243
from ietf.utils.timezone import date_today
4344

4445

@@ -95,3 +96,11 @@ class Meta:
9596
indexes = [
9697
models.Index(fields=['-date',]),
9798
]
99+
100+
101+
class TelechatAgendaContent(models.Model):
102+
section = models.ForeignKey(TelechatAgendaSectionName, on_delete=models.PROTECT)
103+
text = models.TextField(blank=True)
104+
105+
def __str__(self):
106+
return f"{self.section.name} content"

ietf/iesg/resources.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
# Autogenerated by the mkresources management command 2014-11-13 23:53
44

55

6-
from ietf.api import ModelResource
7-
from tastypie.constants import ALL
6+
from ietf.api import ModelResource, ToOneField
7+
from tastypie.constants import ALL, ALL_WITH_RELATIONS
88
from tastypie.cache import SimpleCache
99

1010
from ietf import api
1111

12-
from ietf.iesg.models import TelechatDate, Telechat, TelechatAgendaItem
12+
from ietf.iesg.models import TelechatDate, Telechat, TelechatAgendaItem, TelechatAgendaContent
1313

1414

1515
class TelechatDateResource(ModelResource):
@@ -59,3 +59,20 @@ class Meta:
5959
}
6060
api.iesg.register(TelechatAgendaItemResource())
6161

62+
63+
64+
from ietf.name.resources import TelechatAgendaSectionNameResource
65+
class TelechatAgendaContentResource(ModelResource):
66+
section = ToOneField(TelechatAgendaSectionNameResource, 'section')
67+
class Meta:
68+
queryset = TelechatAgendaContent.objects.none()
69+
serializer = api.Serializer()
70+
cache = SimpleCache()
71+
#resource_name = 'telechatagendacontent'
72+
ordering = ['id', ]
73+
filtering = {
74+
"id": ALL,
75+
"text": ALL,
76+
"section": ALL_WITH_RELATIONS,
77+
}
78+
api.iesg.register(TelechatAgendaContentResource())

ietf/iesg/tests.py

Lines changed: 141 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@
2222
from ietf.doc.utils import create_ballot_if_not_open
2323
from ietf.group.factories import RoleFactory, GroupFactory
2424
from ietf.group.models import Group, GroupMilestone, Role
25-
from ietf.iesg.agenda import get_agenda_date, agenda_data
26-
from ietf.iesg.models import TelechatDate
27-
from ietf.name.models import StreamName
25+
from ietf.iesg.agenda import get_agenda_date, agenda_data, fill_in_agenda_administrivia, agenda_sections
26+
from ietf.iesg.models import TelechatDate, TelechatAgendaContent
27+
from ietf.name.models import StreamName, TelechatAgendaSectionName
2828
from ietf.person.models import Person
2929
from ietf.utils.test_utils import TestCase, login_testing_unauthorized, unicontent
30-
from ietf.iesg.factories import IESGMgmtItemFactory
30+
from ietf.iesg.factories import IESGMgmtItemFactory, TelechatAgendaContentFactory
3131
from ietf.utils.timezone import date_today, DEADLINE_TZINFO
3232

3333

@@ -141,6 +141,16 @@ def setUp(self):
141141
for i in range(0, 10):
142142
self.mgmt_items.append(IESGMgmtItemFactory())
143143

144+
def test_fill_in_agenda_administrivia(self):
145+
roll_call = TelechatAgendaContentFactory(section_id='roll_call')
146+
minutes = TelechatAgendaContentFactory(section_id='minutes')
147+
action_items = TelechatAgendaContentFactory(section_id='action_items')
148+
sections = agenda_sections()
149+
fill_in_agenda_administrivia(None, sections) # n.b., date parameter is unused at present
150+
self.assertIn(roll_call.text, sections["1.1"]["text"])
151+
self.assertIn(minutes.text, sections["1.3"]["text"])
152+
self.assertIn(action_items.text, sections["1.4"]["text"])
153+
144154
def test_fill_in_agenda_docs(self):
145155
draft = self.telechat_docs["ietf_draft"]
146156
statchg = self.telechat_docs["statchg"]
@@ -337,9 +347,12 @@ def test_agenda_json(self):
337347
self.assertTrue(r.json())
338348

339349
def test_agenda(self):
350+
action_items = TelechatAgendaContentFactory(section_id='action_items')
340351
r = self.client.get(urlreverse("ietf.iesg.views.agenda"))
341352
self.assertEqual(r.status_code, 200)
342353

354+
self.assertContains(r, action_items.text)
355+
343356
for k, d in self.telechat_docs.items():
344357
if d.type_id == "charter":
345358
self.assertContains(r, d.group.name, msg_prefix="%s '%s' not in response" % (k, d.group.name))
@@ -356,6 +369,29 @@ def test_agenda(self):
356369
# Make sure the sort places 6.9 before 6.10
357370
self.assertLess(r.content.find(b"6.9"), r.content.find(b"6.10"))
358371

372+
def test_agenda_restricted_sections(self):
373+
r = self.client.get(urlreverse("ietf.iesg.views.agenda"))
374+
# not logged in
375+
for section_id in ("roll_call", "minutes"):
376+
self.assertNotContains(
377+
r, urlreverse("ietf.iesg.views.telechat_agenda_content_view", kwargs={"section": section_id})
378+
)
379+
380+
self.client.login(username="plain", password="plain+password")
381+
for section_id in ("roll_call", "minutes"):
382+
self.assertNotContains(
383+
r, urlreverse("ietf.iesg.views.telechat_agenda_content_view", kwargs={"section": section_id})
384+
)
385+
386+
for username in ("ad", "secretary", "iab chair"):
387+
self.client.login(username=username, password=f"{username}+password")
388+
r = self.client.get(urlreverse("ietf.iesg.views.agenda"))
389+
self.assertEqual(r.status_code, 200)
390+
for section_id in ("roll_call", "minutes"):
391+
self.assertContains(
392+
r, urlreverse("ietf.iesg.views.telechat_agenda_content_view", kwargs={"section": section_id})
393+
)
394+
359395
def test_agenda_txt(self):
360396
r = self.client.get(urlreverse("ietf.iesg.views.agenda_txt"))
361397
self.assertEqual(r.status_code, 200)
@@ -549,3 +585,104 @@ def test_reschedule(self):
549585
self.assertEqual(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date, d)
550586
self.assertTrue(not draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").returning_item)
551587
self.assertEqual(draft.docevent_set.count(), events_before + 1)
588+
589+
590+
class TelechatAgendaContentTests(TestCase):
591+
def test_telechat_agenda_content_view(self):
592+
self.client.login(username="ad", password="ad+password")
593+
r = self.client.get(urlreverse("ietf.iesg.views.telechat_agenda_content_view", kwargs={"section": "fake"}))
594+
self.assertEqual(r.status_code, 404, "Nonexistent section should 404")
595+
for section in TelechatAgendaSectionName.objects.filter(used=True).values_list("slug", flat=True):
596+
r = self.client.get(
597+
urlreverse("ietf.iesg.views.telechat_agenda_content_view", kwargs={"section": section})
598+
)
599+
self.assertEqual(r.status_code, 404, "Section with no content should 404")
600+
for section in TelechatAgendaSectionName.objects.filter(used=True).values_list("slug", flat=True):
601+
content = TelechatAgendaContentFactory(section_id=section).text
602+
r = self.client.get(
603+
urlreverse("ietf.iesg.views.telechat_agenda_content_view", kwargs={"section": section})
604+
)
605+
self.assertContains(r, content, status_code=200)
606+
self.assertEqual(r.get("Content-Type", None), "text/plain")
607+
608+
def test_telechat_agenda_content_view_permissions(self):
609+
for section in TelechatAgendaSectionName.objects.filter(used=True).values_list("slug", flat=True):
610+
TelechatAgendaContentFactory(section_id=section)
611+
url = urlreverse("ietf.iesg.views.telechat_agenda_content_view", kwargs={"section": section})
612+
self.client.logout()
613+
login_testing_unauthorized(self, "plain", url)
614+
login_testing_unauthorized(self, "ad", url)
615+
self.assertEqual(self.client.get(url).status_code, 200)
616+
self.client.login(username="iab chair", password="iab chair+password")
617+
self.assertEqual(self.client.get(url).status_code, 200)
618+
self.client.login(username="secretary", password="secretary+password")
619+
self.assertEqual(self.client.get(url).status_code, 200)
620+
621+
def test_telechat_agenda_content_edit(self):
622+
for section in TelechatAgendaSectionName.objects.filter(used=True):
623+
self.assertFalse(TelechatAgendaContent.objects.filter(section=section).exists())
624+
url = urlreverse("ietf.iesg.views.telechat_agenda_content_edit", kwargs={"section": section.slug})
625+
self.client.logout()
626+
login_testing_unauthorized(self, "plain", url, method="get")
627+
login_testing_unauthorized(self, "ad", url, method="get")
628+
login_testing_unauthorized(self, "iab chair", url, method="get")
629+
login_testing_unauthorized(self, "secretary", url, method="get")
630+
r = self.client.get(url)
631+
self.assertContains(r, str(section), status_code=200)
632+
633+
self.client.logout()
634+
login_testing_unauthorized(self, "plain", url, method="post")
635+
login_testing_unauthorized(self, "ad", url, method="post")
636+
login_testing_unauthorized(self, "iab chair", url, method="post")
637+
login_testing_unauthorized(self, "secretary", url, method="post")
638+
r = self.client.post(url, {"text": "This is some content"})
639+
self.assertRedirects(r, urlreverse("ietf.iesg.views.telechat_agenda_content_manage"))
640+
contents = TelechatAgendaContent.objects.filter(section=section)
641+
self.assertEqual(contents.count(), 1)
642+
self.assertEqual(contents.first().text, "This is some content")
643+
644+
self.client.logout()
645+
login_testing_unauthorized(self, "plain", url, method="post")
646+
login_testing_unauthorized(self, "ad", url, method="post")
647+
login_testing_unauthorized(self, "iab chair", url, method="post")
648+
login_testing_unauthorized(self, "secretary", url, method="post")
649+
r = self.client.post(url, {"text": "This is some different content"})
650+
self.assertRedirects(r, urlreverse("ietf.iesg.views.telechat_agenda_content_manage"))
651+
contents = TelechatAgendaContent.objects.filter(section=section)
652+
self.assertEqual(contents.count(), 1)
653+
self.assertEqual(contents.first().text, "This is some different content")
654+
655+
def test_telechat_agenda_content_manage(self):
656+
url = urlreverse("ietf.iesg.views.telechat_agenda_content_manage")
657+
login_testing_unauthorized(self, "plain", url)
658+
login_testing_unauthorized(self, "ad", url)
659+
login_testing_unauthorized(self, "iab chair", url)
660+
login_testing_unauthorized(self, "secretary", url)
661+
self.assertEqual(TelechatAgendaContent.objects.count(), 0)
662+
r = self.client.get(url)
663+
self.assertEqual(r.status_code, 200)
664+
pq = PyQuery(r.content)
665+
for section in TelechatAgendaSectionName.objects.filter(used=True):
666+
# check that there's a tab even when section is empty
667+
nav_button = pq(f"button.nav-link#{section.slug}-tab")
668+
self.assertEqual(nav_button.text(), str(section))
669+
edit_url = urlreverse("ietf.iesg.views.telechat_agenda_content_edit", kwargs={"section": section.pk})
670+
edit_button = pq(f'div#{section.slug} a[href="{edit_url}"]')
671+
self.assertEqual(len(edit_button), 1)
672+
self.assertIn(f"No {section}", pq(f"div#{section.slug}").text())
673+
# and create a section for the next test
674+
TelechatAgendaContentFactory(section_id=section.slug)
675+
r = self.client.get(url)
676+
self.assertEqual(r.status_code, 200)
677+
pq = PyQuery(r.content)
678+
for section in TelechatAgendaSectionName.objects.filter(used=True):
679+
# check that there's a tab with the content
680+
nav_button = pq(f"button.nav-link#{section.slug}-tab")
681+
self.assertEqual(nav_button.text(), str(section))
682+
edit_url = urlreverse("ietf.iesg.views.telechat_agenda_content_edit", kwargs={"section": section.pk})
683+
edit_button = pq(f'div#{section.slug} a[href="{edit_url}"]')
684+
self.assertEqual(len(edit_button), 1)
685+
self.assertIn(
686+
TelechatAgendaContent.objects.get(section=section).text, pq(f"div#{section.slug}").text()
687+
)
688+

ietf/iesg/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@
5252

5353
url(r'^agenda/documents.txt$', views.agenda_documents_txt),
5454
url(r'^agenda/documents/$', views.agenda_documents),
55+
url(r'^agenda/sections$', views.telechat_agenda_content_manage),
56+
url(r'^agenda/section/(?P<section>[a-z_]+)$', views.telechat_agenda_content_view),
57+
url(r'^agenda/section/(?P<section>[a-z_]+)/edit$', views.telechat_agenda_content_edit),
5558
url(r'^past/documents/$', views.past_documents),
5659
url(r'^agenda/telechat-(?:%(date)s-)?docs.tgz' % settings.URL_REGEXPS, views.telechat_docs_tarfile),
5760
url(r'^discusses/$', views.discusses),

0 commit comments

Comments
 (0)