Skip to content

Commit 4e6abcb

Browse files
refactor: make WG summary view into a task (ietf-tools#7529)
* feat: generate_wg_summary_files_task() * refactor: wg summaries from filesys for view * refactor: use new helper for charter views * refactor: use FileResponse * refactor: don't use FileResponse FileResponse generates a StreamingHttpResponse which brings with it differences I don't fully understand, so let's stay with HttpResponse * test: update view tests * test: test_generate_wg_summary_files_task() * chore: create PeriodicTask N.B. that this makes it hourly instead of daily
1 parent 774fe78 commit 4e6abcb

5 files changed

Lines changed: 171 additions & 82 deletions

File tree

ietf/group/tasks.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from .models import Group
1616
from .utils import fill_in_charter_info, fill_in_wg_drafts, fill_in_wg_roles
17+
from .views import extract_last_name, roles
1718

1819

1920
@shared_task
@@ -59,3 +60,40 @@ def generate_wg_charters_files_task():
5960
log.log(
6061
f"Error copying {charters_by_acronym_file} to {charter_copy_dest}: {err}"
6162
)
63+
64+
65+
@shared_task
66+
def generate_wg_summary_files_task():
67+
# Active WGs (all should have a parent, but filter to be sure)
68+
groups = (
69+
Group.objects.filter(type="wg", state="active")
70+
.exclude(parent=None)
71+
.order_by("acronym")
72+
)
73+
# Augment groups with chairs list
74+
for group in groups:
75+
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
76+
77+
# Active areas with one or more active groups in them
78+
areas = Group.objects.filter(
79+
type="area",
80+
state="active",
81+
group__in=groups,
82+
).distinct().order_by("name")
83+
# Augment areas with their groups
84+
for area in areas:
85+
area.groups = [g for g in groups if g.parent_id == area.pk]
86+
summary_path = Path(settings.GROUP_SUMMARY_PATH)
87+
summary_file = summary_path / "1wg-summary.txt"
88+
summary_file.write_text(
89+
render_to_string("group/1wg-summary.txt", {"areas": areas}),
90+
encoding="utf8",
91+
)
92+
summary_by_acronym_file = summary_path / "1wg-summary-by-acronym.txt"
93+
summary_by_acronym_file.write_text(
94+
render_to_string(
95+
"group/1wg-summary-by-acronym.txt",
96+
{"areas": areas, "groups": groups},
97+
),
98+
encoding="utf8",
99+
)

ietf/group/tests_info.py

Lines changed: 109 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88
import io
99
import bleach
1010

11-
from unittest.mock import patch
11+
from unittest.mock import call, patch
1212
from pathlib import Path
1313
from pyquery import PyQuery
1414
from tempfile import NamedTemporaryFile
1515

1616
import debug # pyflakes:ignore
1717

1818
from django.conf import settings
19+
from django.http import Http404, HttpResponse
1920
from django.test import RequestFactory
2021
from django.test.utils import override_settings
2122
from django.urls import reverse as urlreverse
@@ -35,7 +36,8 @@
3536
DatedGroupMilestoneFactory, DatelessGroupMilestoneFactory)
3637
from ietf.group.forms import GroupForm
3738
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions, Role
38-
from ietf.group.tasks import generate_wg_charters_files_task
39+
from ietf.group.tasks import generate_wg_charters_files_task, generate_wg_summary_files_task
40+
from ietf.group.views import response_from_file
3941
from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group
4042
from ietf.meeting.factories import SessionFactory
4143
from ietf.name.models import DocTagName, GroupStateName, GroupTypeName, ExtResourceName, RoleName
@@ -58,7 +60,11 @@ def pklist(docs):
5860
return [ str(doc.pk) for doc in docs.all() ]
5961

6062
class GroupPagesTests(TestCase):
61-
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + ['CHARTER_PATH', 'CHARTER_COPY_PATH']
63+
settings_temp_path_overrides = TestCase.settings_temp_path_overrides + [
64+
"CHARTER_PATH",
65+
"CHARTER_COPY_PATH",
66+
"GROUP_SUMMARY_PATH",
67+
]
6268

6369
def test_active_groups(self):
6470
area = GroupFactory.create(type_id='area')
@@ -112,63 +118,90 @@ def test_group_home(self):
112118
self.assertContains(r, draft.name)
113119
self.assertContains(r, draft.title)
114120

115-
def test_wg_summaries(self):
116-
group = CharterFactory(group__type_id='wg',group__parent=GroupFactory(type_id='area')).group
117-
RoleFactory(group=group,name_id='chair',person=PersonFactory())
118-
RoleFactory(group=group,name_id='ad',person=PersonFactory())
119-
120-
chair = Email.objects.filter(role__group=group, role__name="chair")[0]
121-
122-
url = urlreverse('ietf.group.views.wg_summary_area', kwargs=dict(group_type="wg"))
123-
r = self.client.get(url)
121+
def test_response_from_file(self):
122+
# n.b., GROUP_SUMMARY_PATH is a temp dir that will be cleaned up automatically
123+
fp = Path(settings.GROUP_SUMMARY_PATH) / "some-file.txt"
124+
fp.write_text("This is a charters file with an é")
125+
r = response_from_file(fp)
124126
self.assertEqual(r.status_code, 200)
125-
self.assertContains(r, group.parent.name)
126-
self.assertContains(r, group.acronym)
127-
self.assertContains(r, group.name)
128-
self.assertContains(r, chair.address)
129-
130-
url = urlreverse('ietf.group.views.wg_summary_acronym', kwargs=dict(group_type="wg"))
131-
r = self.client.get(url)
132-
self.assertEqual(r.status_code, 200)
133-
self.assertContains(r, group.acronym)
134-
self.assertContains(r, group.name)
135-
self.assertContains(r, chair.address)
136-
137-
def test_wg_charters(self):
138-
# file does not exist = 404
139-
url = urlreverse("ietf.group.views.wg_charters", kwargs=dict(group_type="wg"))
140-
r = self.client.get(url)
127+
self.assertEqual(r.headers["Content-Type"], "text/plain; charset=utf-8")
128+
self.assertEqual(r.content.decode("utf8"), "This is a charters file with an é")
129+
# now try with a nonexistent file
130+
fp.unlink()
131+
with self.assertRaises(Http404):
132+
response_from_file(fp)
133+
134+
@patch("ietf.group.views.response_from_file")
135+
def test_wg_summary_area(self, mock):
136+
r = self.client.get(
137+
urlreverse("ietf.group.views.wg_summary_area", kwargs={"group_type": "rg"})
138+
) # not wg
141139
self.assertEqual(r.status_code, 404)
142-
143-
# should return expected file with expected encoding
144-
wg_path = Path(settings.CHARTER_PATH) / "1wg-charters.txt"
145-
wg_path.write_text("This is a charters file with an é")
146-
r = self.client.get(url)
140+
self.assertFalse(mock.called)
141+
mock.return_value = HttpResponse("yay")
142+
r = self.client.get(
143+
urlreverse("ietf.group.views.wg_summary_area", kwargs={"group_type": "wg"})
144+
)
147145
self.assertEqual(r.status_code, 200)
148-
self.assertEqual(r.charset, "UTF-8")
149-
self.assertEqual(r.content.decode("utf8"), "This is a charters file with an é")
150-
151-
# non-wg request = 404 even if the file exists
152-
url = urlreverse("ietf.group.views.wg_charters", kwargs=dict(group_type="rg"))
153-
r = self.client.get(url)
146+
self.assertEqual(r.content.decode(), "yay")
147+
self.assertEqual(mock.call_args, call(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary.txt"))
148+
149+
@patch("ietf.group.views.response_from_file")
150+
def test_wg_summary_acronym(self, mock):
151+
r = self.client.get(
152+
urlreverse(
153+
"ietf.group.views.wg_summary_acronym", kwargs={"group_type": "rg"}
154+
)
155+
) # not wg
154156
self.assertEqual(r.status_code, 404)
157+
self.assertFalse(mock.called)
158+
mock.return_value = HttpResponse("yay")
159+
r = self.client.get(
160+
urlreverse(
161+
"ietf.group.views.wg_summary_acronym", kwargs={"group_type": "wg"}
162+
)
163+
)
164+
self.assertEqual(r.status_code, 200)
165+
self.assertEqual(r.content.decode(), "yay")
166+
self.assertEqual(
167+
mock.call_args, call(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary-by-acronym.txt")
168+
)
155169

156-
def test_wg_charters_by_acronym(self):
157-
url = urlreverse("ietf.group.views.wg_charters_by_acronym", kwargs=dict(group_type="wg"))
158-
r = self.client.get(url)
170+
@patch("ietf.group.views.response_from_file")
171+
def test_wg_charters(self, mock):
172+
r = self.client.get(
173+
urlreverse("ietf.group.views.wg_charters", kwargs={"group_type": "rg"})
174+
) # not wg
159175
self.assertEqual(r.status_code, 404)
160-
161-
wg_path = Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt"
162-
wg_path.write_text("This is a charters file with an é")
163-
r = self.client.get(url)
176+
self.assertFalse(mock.called)
177+
mock.return_value = HttpResponse("yay")
178+
r = self.client.get(
179+
urlreverse("ietf.group.views.wg_charters", kwargs={"group_type": "wg"})
180+
)
164181
self.assertEqual(r.status_code, 200)
165-
self.assertEqual(r.charset, "UTF-8")
166-
self.assertEqual(r.content.decode("utf8"), "This is a charters file with an é")
167-
168-
# non-wg request = 404 even if the file exists
169-
url = urlreverse("ietf.group.views.wg_charters_by_acronym", kwargs=dict(group_type="rg"))
170-
r = self.client.get(url)
182+
self.assertEqual(r.content.decode(), "yay")
183+
self.assertEqual(mock.call_args, call(Path(settings.CHARTER_PATH) / "1wg-charters.txt"))
184+
185+
@patch("ietf.group.views.response_from_file")
186+
def test_wg_charters_by_acronym(self, mock):
187+
r = self.client.get(
188+
urlreverse(
189+
"ietf.group.views.wg_charters_by_acronym", kwargs={"group_type": "rg"}
190+
)
191+
) # not wg
171192
self.assertEqual(r.status_code, 404)
193+
self.assertFalse(mock.called)
194+
mock.return_value = HttpResponse("yay")
195+
r = self.client.get(
196+
urlreverse(
197+
"ietf.group.views.wg_charters_by_acronym", kwargs={"group_type": "wg"}
198+
)
199+
)
200+
self.assertEqual(r.status_code, 200)
201+
self.assertEqual(r.content.decode(), "yay")
202+
self.assertEqual(
203+
mock.call_args, call(Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt")
204+
)
172205

173206
def test_generate_wg_charters_files_task(self):
174207
group = CharterFactory(
@@ -254,6 +287,30 @@ def test_generate_wg_charters_files_task_without_copy(self):
254287
)
255288
self.assertEqual(not_a_dir.read_text(), "Not a dir")
256289

290+
def test_generate_wg_summary_files_task(self):
291+
group = CharterFactory(group__type_id='wg',group__parent=GroupFactory(type_id='area')).group
292+
RoleFactory(group=group,name_id='chair',person=PersonFactory())
293+
RoleFactory(group=group,name_id='ad',person=PersonFactory())
294+
295+
chair = Email.objects.filter(role__group=group, role__name="chair")[0]
296+
297+
generate_wg_summary_files_task()
298+
299+
summary_by_area_contents = (
300+
Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary.txt"
301+
).read_text(encoding="utf8")
302+
self.assertIn(group.parent.name, summary_by_area_contents)
303+
self.assertIn(group.acronym, summary_by_area_contents)
304+
self.assertIn(group.name, summary_by_area_contents)
305+
self.assertIn(chair.address, summary_by_area_contents)
306+
307+
summary_by_acronym_contents = (
308+
Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary-by-acronym.txt"
309+
).read_text(encoding="utf8")
310+
self.assertIn(group.acronym, summary_by_acronym_contents)
311+
self.assertIn(group.name, summary_by_acronym_contents)
312+
self.assertIn(chair.address, summary_by_acronym_contents)
313+
257314
def test_chartering_groups(self):
258315
group = CharterFactory(group__type_id='wg',group__parent=GroupFactory(type_id='area'),states=[('charter','intrev')]).group
259316

ietf/group/views.py

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -152,56 +152,39 @@ def check_group_email_aliases():
152152
return False
153153

154154

155+
def response_from_file(fpath: Path) -> HttpResponse:
156+
"""Helper to shovel a file back in an HttpResponse"""
157+
try:
158+
content = fpath.read_bytes()
159+
except IOError:
160+
raise Http404
161+
return HttpResponse(content, content_type="text/plain; charset=utf-8")
162+
163+
155164
# --- View functions ---------------------------------------------------
156165

157166
def wg_summary_area(request, group_type):
158167
if group_type != "wg":
159168
raise Http404
160-
areas = Group.objects.filter(type="area", state="active").order_by("name")
161-
for area in areas:
162-
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym")
163-
for group in area.groups:
164-
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
165-
166-
areas = [a for a in areas if a.groups]
169+
return response_from_file(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary.txt")
167170

168-
return render(request, 'group/1wg-summary.txt',
169-
{ 'areas': areas },
170-
content_type='text/plain; charset=UTF-8')
171171

172172
def wg_summary_acronym(request, group_type):
173173
if group_type != "wg":
174174
raise Http404
175-
areas = Group.objects.filter(type="area", state="active").order_by("name")
176-
groups = Group.objects.filter(type="wg", state="active").order_by("acronym").select_related("parent")
177-
for group in groups:
178-
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
179-
return render(request, 'group/1wg-summary-by-acronym.txt',
180-
{ 'areas': areas,
181-
'groups': groups },
182-
content_type='text/plain; charset=UTF-8')
175+
return response_from_file(Path(settings.GROUP_SUMMARY_PATH) / "1wg-summary-by-acronym.txt")
183176

184177

185178
def wg_charters(request, group_type):
186179
if group_type != "wg":
187180
raise Http404
188-
fpath = Path(settings.CHARTER_PATH) / "1wg-charters.txt"
189-
try:
190-
content = fpath.read_bytes()
191-
except IOError:
192-
raise Http404
193-
return HttpResponse(content, content_type="text/plain; charset=UTF-8")
181+
return response_from_file(Path(settings.CHARTER_PATH) / "1wg-charters.txt")
194182

195183

196184
def wg_charters_by_acronym(request, group_type):
197185
if group_type != "wg":
198186
raise Http404
199-
fpath = Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt"
200-
try:
201-
content = fpath.read_bytes()
202-
except IOError:
203-
raise Http404
204-
return HttpResponse(content, content_type="text/plain; charset=UTF-8")
187+
return response_from_file(Path(settings.CHARTER_PATH) / "1wg-charters-by-acronym.txt")
205188

206189

207190
def active_groups(request, group_type=None):

ietf/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,7 @@ def skip_unreadable_post(record):
671671
RFC_PATH = '/a/www/ietf-ftp/rfc/'
672672
CHARTER_PATH = '/a/ietfdata/doc/charter/'
673673
CHARTER_COPY_PATH = '/a/www/ietf-ftp/ietf' # copy 1wg-charters files here if set
674+
GROUP_SUMMARY_PATH = '/a/www/ietf-ftp/ietf'
674675
BOFREQ_PATH = '/a/ietfdata/doc/bofreq/'
675676
CONFLICT_REVIEW_PATH = '/a/ietfdata/doc/conflict-review'
676677
STATUS_CHANGE_PATH = '/a/ietfdata/doc/status-change'

ietf/utils/management/commands/periodic_tasks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,16 @@ def create_default_tasks(self):
231231
),
232232
)
233233

234+
PeriodicTask.objects.get_or_create(
235+
name="Generate WG summary files",
236+
task="ietf.group.tasks.generate_wg_summary_files_task",
237+
defaults=dict(
238+
enabled=False,
239+
crontab=self.crontabs["hourly"],
240+
description="Update 1wg-summary.txt and 1wg-summary-by-acronym.txt",
241+
),
242+
)
243+
234244
PeriodicTask.objects.get_or_create(
235245
name="Generate I-D bibxml files",
236246
task="ietf.doc.tasks.generate_draft_bibxml_files_task",

0 commit comments

Comments
 (0)