Skip to content

Commit 6338f45

Browse files
refactor: don't use filesystem for draft aliases (ietf-tools#7555)
* refactor: compute draft aliases on demand n.b., very slow for full set of aliases * refactor: simplify and cache email_aliases The name != "" case is, as far as I can see, unused. * chore: remove draft alias checks * chore: remove draft alias/virtual settings * chore: remove lint * test: update tests * test: better mocking * refactor: move utility to utils * test: add tests
1 parent 2a90447 commit 6338f45

7 files changed

Lines changed: 139 additions & 127 deletions

File tree

ietf/checks.py

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -56,33 +56,6 @@ def check_group_email_aliases_exists(app_configs, **kwargs):
5656

5757
return errors
5858

59-
@checks.register('files')
60-
def check_doc_email_aliases_exists(app_configs, **kwargs):
61-
from ietf.doc.views_doc import check_doc_email_aliases
62-
#
63-
if already_ran():
64-
return []
65-
#
66-
errors = []
67-
try:
68-
ok = check_doc_email_aliases()
69-
if not ok:
70-
errors.append(checks.Error(
71-
"Found no aliases in the document email aliases file\n'%s'."%settings.DRAFT_VIRTUAL_PATH,
72-
hint="These should be created by the infrastructure using ietf/bin/aliases-from-json.py.",
73-
obj=None,
74-
id="datatracker.E0004",
75-
))
76-
except IOError as e:
77-
errors.append(checks.Error(
78-
"Could not read document email aliases:\n %s" % e,
79-
hint="These should be created by the infrastructure using ietf/bin/aliases-from-json.py.",
80-
obj=None,
81-
id="datatracker.E0005",
82-
))
83-
84-
return errors
85-
8659
@checks.register('directories')
8760
def check_id_submission_directories(app_configs, **kwargs):
8861
#

ietf/doc/tests.py

Lines changed: 92 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from pathlib import Path
1717
from pyquery import PyQuery
1818
from urllib.parse import urlparse, parse_qs
19-
from tempfile import NamedTemporaryFile
2019
from collections import defaultdict
2120
from zoneinfo import ZoneInfo
2221

@@ -51,6 +50,7 @@
5150
DraftAliasGenerator,
5251
generate_idnits2_rfc_status,
5352
generate_idnits2_rfcs_obsoleted,
53+
get_doc_email_aliases,
5454
)
5555
from ietf.group.models import Group, Role
5656
from ietf.group.factories import GroupFactory, RoleFactory
@@ -2169,24 +2169,6 @@ def test_references(self):
21692169
self.assertContains(r, doc1.name)
21702170

21712171
class GenerateDraftAliasesTests(TestCase):
2172-
def setUp(self):
2173-
super().setUp()
2174-
self.doc_aliases_file = NamedTemporaryFile(delete=False, mode="w+")
2175-
self.doc_aliases_file.close()
2176-
self.doc_virtual_file = NamedTemporaryFile(delete=False, mode="w+")
2177-
self.doc_virtual_file.close()
2178-
self.saved_draft_aliases_path = settings.DRAFT_ALIASES_PATH
2179-
self.saved_draft_virtual_path = settings.DRAFT_VIRTUAL_PATH
2180-
settings.DRAFT_ALIASES_PATH = self.doc_aliases_file.name
2181-
settings.DRAFT_VIRTUAL_PATH = self.doc_virtual_file.name
2182-
2183-
def tearDown(self):
2184-
settings.DRAFT_ALIASES_PATH = self.saved_draft_aliases_path
2185-
settings.DRAFT_VIRTUAL_PATH = self.saved_draft_virtual_path
2186-
os.unlink(self.doc_aliases_file.name)
2187-
os.unlink(self.doc_virtual_file.name)
2188-
super().tearDown()
2189-
21902172
@override_settings(TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org")
21912173
def test_generator_class(self):
21922174
"""The DraftAliasGenerator should generate the same lists as the old mgmt cmd"""
@@ -2286,6 +2268,28 @@ def test_generator_class(self):
22862268
{k: sorted(v) for k, v in expected_dict.items()},
22872269
)
22882270

2271+
# check single name
2272+
output = [(alias, alist) for alias, alist in DraftAliasGenerator(Document.objects.filter(name=doc1.name))]
2273+
alias_dict = dict(output)
2274+
self.assertEqual(len(alias_dict), len(output)) # no duplicate aliases
2275+
expected_dict = {
2276+
doc1.name: [author1.email_address()],
2277+
doc1.name + ".ad": [ad.email_address()],
2278+
doc1.name + ".authors": [author1.email_address()],
2279+
doc1.name + ".shepherd": [shepherd.email_address()],
2280+
doc1.name
2281+
+ ".all": [
2282+
author1.email_address(),
2283+
ad.email_address(),
2284+
shepherd.email_address(),
2285+
],
2286+
}
2287+
# Sort lists for comparison
2288+
self.assertEqual(
2289+
{k: sorted(v) for k, v in alias_dict.items()},
2290+
{k: sorted(v) for k, v in expected_dict.items()},
2291+
)
2292+
22892293
@override_settings(TOOLS_SERVER="tools.example.org", DRAFT_ALIAS_DOMAIN="draft.example.org")
22902294
def test_get_draft_notify_emails(self):
22912295
ad = PersonFactory()
@@ -2336,37 +2340,20 @@ def setUp(self):
23362340
WgDraftFactory(name='draft-ietf-mars-test',group__acronym='mars')
23372341
WgDraftFactory(name='draft-ietf-ames-test',group__acronym='ames')
23382342
RoleFactory(group__type_id='review', group__acronym='yangdoctors', name_id='secr')
2339-
self.doc_alias_file = NamedTemporaryFile(delete=False, mode='w+')
2340-
self.doc_alias_file.write("""# Generated by hand at 2015-02-12_16:26:45
2341-
virtual.ietf.org anything
2342-
draft-ietf-mars-test@ietf.org xfilter-draft-ietf-mars-test
2343-
expand-draft-ietf-mars-test@virtual.ietf.org mars-author@example.com, mars-collaborator@example.com
2344-
draft-ietf-mars-test.authors@ietf.org xfilter-draft-ietf-mars-test.authors
2345-
expand-draft-ietf-mars-test.authors@virtual.ietf.org mars-author@example.mars, mars-collaborator@example.mars
2346-
draft-ietf-mars-test.chairs@ietf.org xfilter-draft-ietf-mars-test.chairs
2347-
expand-draft-ietf-mars-test.chairs@virtual.ietf.org mars-chair@example.mars
2348-
draft-ietf-mars-test.all@ietf.org xfilter-draft-ietf-mars-test.all
2349-
expand-draft-ietf-mars-test.all@virtual.ietf.org mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars
2350-
draft-ietf-ames-test@ietf.org xfilter-draft-ietf-ames-test
2351-
expand-draft-ietf-ames-test@virtual.ietf.org ames-author@example.com, ames-collaborator@example.com
2352-
draft-ietf-ames-test.authors@ietf.org xfilter-draft-ietf-ames-test.authors
2353-
expand-draft-ietf-ames-test.authors@virtual.ietf.org ames-author@example.ames, ames-collaborator@example.ames
2354-
draft-ietf-ames-test.chairs@ietf.org xfilter-draft-ietf-ames-test.chairs
2355-
expand-draft-ietf-ames-test.chairs@virtual.ietf.org ames-chair@example.ames
2356-
draft-ietf-ames-test.all@ietf.org xfilter-draft-ietf-ames-test.all
2357-
expand-draft-ietf-ames-test.all@virtual.ietf.org ames-author@example.ames, ames-collaborator@example.ames, ames-chair@example.ames
2358-
2359-
""")
2360-
self.doc_alias_file.close()
2361-
self.saved_draft_virtual_path = settings.DRAFT_VIRTUAL_PATH
2362-
settings.DRAFT_VIRTUAL_PATH = self.doc_alias_file.name
2363-
2364-
def tearDown(self):
2365-
settings.DRAFT_VIRTUAL_PATH = self.saved_draft_virtual_path
2366-
os.unlink(self.doc_alias_file.name)
2367-
super().tearDown()
2368-
2369-
def testAliases(self):
2343+
2344+
2345+
@mock.patch("ietf.doc.views_doc.get_doc_email_aliases")
2346+
def testAliases(self, mock_get_aliases):
2347+
mock_get_aliases.return_value = [
2348+
{"doc_name": "draft-ietf-mars-test", "alias_type": "", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"},
2349+
{"doc_name": "draft-ietf-mars-test", "alias_type": ".authors", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"},
2350+
{"doc_name": "draft-ietf-mars-test", "alias_type": ".chairs", "expansion": "mars-chair@example.mars"},
2351+
{"doc_name": "draft-ietf-mars-test", "alias_type": ".all", "expansion": "mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars"},
2352+
{"doc_name": "draft-ietf-ames-test", "alias_type": "", "expansion": "ames-author@example.ames, ames-collaborator@example.ames"},
2353+
{"doc_name": "draft-ietf-ames-test", "alias_type": ".authors", "expansion": "ames-author@example.ames, ames-collaborator@example.ames"},
2354+
{"doc_name": "draft-ietf-ames-test", "alias_type": ".chairs", "expansion": "ames-chair@example.ames"},
2355+
{"doc_name": "draft-ietf-ames-test", "alias_type": ".all", "expansion": "ames-author@example.ames, ames-collaborator@example.ames, ames-chair@example.ames"},
2356+
]
23702357
PersonFactory(user__username='plain')
23712358
url = urlreverse('ietf.doc.urls.redirect.document_email', kwargs=dict(name="draft-ietf-mars-test"))
23722359
r = self.client.get(url)
@@ -2376,16 +2363,70 @@ def testAliases(self):
23762363
login_testing_unauthorized(self, "plain", url)
23772364
r = self.client.get(url)
23782365
self.assertEqual(r.status_code, 200)
2366+
self.assertEqual(mock_get_aliases.call_args, mock.call())
23792367
self.assertTrue(all([x in unicontent(r) for x in ['mars-test@','mars-test.authors@','mars-test.chairs@']]))
23802368
self.assertTrue(all([x in unicontent(r) for x in ['ames-test@','ames-test.authors@','ames-test.chairs@']]))
23812369

2382-
def testExpansions(self):
2370+
2371+
@mock.patch("ietf.doc.views_doc.get_doc_email_aliases")
2372+
def testExpansions(self, mock_get_aliases):
2373+
mock_get_aliases.return_value = [
2374+
{"doc_name": "draft-ietf-mars-test", "alias_type": "", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"},
2375+
{"doc_name": "draft-ietf-mars-test", "alias_type": ".authors", "expansion": "mars-author@example.mars, mars-collaborator@example.mars"},
2376+
{"doc_name": "draft-ietf-mars-test", "alias_type": ".chairs", "expansion": "mars-chair@example.mars"},
2377+
{"doc_name": "draft-ietf-mars-test", "alias_type": ".all", "expansion": "mars-author@example.mars, mars-collaborator@example.mars, mars-chair@example.mars"},
2378+
]
23832379
url = urlreverse('ietf.doc.views_doc.document_email', kwargs=dict(name="draft-ietf-mars-test"))
23842380
r = self.client.get(url)
2381+
self.assertEqual(mock_get_aliases.call_args, mock.call("draft-ietf-mars-test"))
23852382
self.assertEqual(r.status_code, 200)
23862383
self.assertContains(r, 'draft-ietf-mars-test.all@ietf.org')
23872384
self.assertContains(r, 'iesg_ballot_saved')
2385+
2386+
@mock.patch("ietf.doc.utils.DraftAliasGenerator")
2387+
def test_get_doc_email_aliases(self, mock_alias_gen_cls):
2388+
mock_alias_gen_cls.return_value = [
2389+
("draft-something-or-other.some-type", ["somebody@example.com"]),
2390+
("draft-something-or-other", ["somebody@example.com"]),
2391+
("draft-nothing-at-all", ["nobody@example.com"]),
2392+
("draft-nothing-at-all.some-type", ["nobody@example.com"]),
2393+
]
2394+
# order is important in the response - should be sorted by doc name and otherwise left
2395+
# in order
2396+
self.assertEqual(
2397+
get_doc_email_aliases(),
2398+
[
2399+
{
2400+
"doc_name": "draft-nothing-at-all",
2401+
"alias_type": "",
2402+
"expansion": "nobody@example.com",
2403+
},
2404+
{
2405+
"doc_name": "draft-nothing-at-all",
2406+
"alias_type": ".some-type",
2407+
"expansion": "nobody@example.com",
2408+
},
2409+
{
2410+
"doc_name": "draft-something-or-other",
2411+
"alias_type": ".some-type",
2412+
"expansion": "somebody@example.com",
2413+
},
2414+
{
2415+
"doc_name": "draft-something-or-other",
2416+
"alias_type": "",
2417+
"expansion": "somebody@example.com",
2418+
},
2419+
],
2420+
)
2421+
self.assertEqual(mock_alias_gen_cls.call_args, mock.call(None))
23882422

2423+
# Repeat with a name, no need to re-test that the alias list is actually passed through, just
2424+
# check that the DraftAliasGenerator is called correctly
2425+
draft = WgDraftFactory()
2426+
get_doc_email_aliases(draft.name)
2427+
self.assertQuerySetEqual(mock_alias_gen_cls.call_args[0][0], Document.objects.filter(pk=draft.pk))
2428+
2429+
23892430
class DocumentMeetingTests(TestCase):
23902431

23912432
def setUp(self):

ietf/doc/utils.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from collections import defaultdict, namedtuple, Counter
1515
from dataclasses import dataclass
1616
from pathlib import Path
17-
from typing import Iterator, Union
17+
from typing import Iterator, Optional, Union
1818
from zoneinfo import ZoneInfo
1919

2020
from django.conf import settings
@@ -1265,6 +1265,12 @@ def bibxml_for_draft(doc, rev=None):
12651265
class DraftAliasGenerator:
12661266
days = 2 * 365
12671267

1268+
def __init__(self, draft_queryset=None):
1269+
if draft_queryset is not None:
1270+
self.draft_queryset = draft_queryset.filter(type_id="draft") # only drafts allowed
1271+
else:
1272+
self.draft_queryset = Document.objects.filter(type_id="draft")
1273+
12681274
def get_draft_ad_emails(self, doc):
12691275
"""Get AD email addresses for the given draft, if any."""
12701276
from ietf.group.utils import get_group_ad_emails # avoid circular import
@@ -1333,7 +1339,7 @@ def get_draft_notify_emails(self, doc):
13331339
def __iter__(self) -> Iterator[tuple[str, list[str]]]:
13341340
# Internet-Drafts with active status or expired within self.days
13351341
show_since = timezone.now() - datetime.timedelta(days=self.days)
1336-
drafts = Document.objects.filter(type_id="draft")
1342+
drafts = self.draft_queryset
13371343
active_drafts = drafts.filter(states__slug='active')
13381344
inactive_recent_drafts = drafts.exclude(states__slug='active').filter(expires__gte=show_since)
13391345
interesting_drafts = active_drafts | inactive_recent_drafts
@@ -1384,6 +1390,22 @@ def __iter__(self) -> Iterator[tuple[str, list[str]]]:
13841390
if all:
13851391
yield alias + ".all", list(all)
13861392

1393+
1394+
def get_doc_email_aliases(name: Optional[str] = None):
1395+
aliases = []
1396+
for (alias, alist) in DraftAliasGenerator(
1397+
Document.objects.filter(type_id="draft", name=name) if name else None
1398+
):
1399+
# alias is draft-name.alias_type
1400+
doc_name, _dot, alias_type = alias.partition(".")
1401+
aliases.append({
1402+
"doc_name": doc_name,
1403+
"alias_type": f".{alias_type}" if alias_type else "",
1404+
"expansion": ", ".join(sorted(alist)),
1405+
})
1406+
return sorted(aliases, key=lambda a: (a["doc_name"]))
1407+
1408+
13871409
def investigate_fragment(name_fragment):
13881410
can_verify = set()
13891411
for root in [settings.INTERNET_DRAFT_PATH, settings.INTERNET_DRAFT_ARCHIVE_DIR]:

ietf/doc/views_doc.py

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,21 @@
3535

3636

3737
import glob
38-
import io
3938
import json
4039
import os
4140
import re
4241

4342
from pathlib import Path
4443

44+
from django.core.cache import caches
4545
from django.db.models import Max
4646
from django.http import HttpResponse, Http404
4747
from django.shortcuts import render, get_object_or_404, redirect
4848
from django.template.loader import render_to_string
4949
from django.urls import reverse as urlreverse
5050
from django.conf import settings
5151
from django import forms
52+
from django.contrib.auth.decorators import login_required
5253
from django.contrib.staticfiles import finders
5354

5455
import debug # pyflakes:ignore
@@ -64,7 +65,7 @@
6465
add_events_message_info, get_unicode_document_content,
6566
augment_docs_and_person_with_person_info, irsg_needed_ballot_positions, add_action_holder_change_event,
6667
build_file_urls, update_documentauthors, fuzzy_find_documents,
67-
bibxml_for_draft)
68+
bibxml_for_draft, get_doc_email_aliases)
6869
from ietf.doc.utils_bofreq import bofreq_editors, bofreq_responsible
6970
from ietf.group.models import Role, Group
7071
from ietf.group.utils import can_manage_all_groups_of_type, can_manage_materials, group_features_role_filter
@@ -1071,32 +1072,6 @@ def document_pdfized(request, name, rev=None, ext=None):
10711072
else:
10721073
raise Http404
10731074

1074-
def check_doc_email_aliases():
1075-
pattern = re.compile(r'^expand-(.*?)(\..*?)?@.*? +(.*)$')
1076-
good_count = 0
1077-
tot_count = 0
1078-
with io.open(settings.DRAFT_VIRTUAL_PATH,"r") as virtual_file:
1079-
for line in virtual_file.readlines():
1080-
m = pattern.match(line)
1081-
tot_count += 1
1082-
if m:
1083-
good_count += 1
1084-
if good_count > 50 and tot_count < 3*good_count:
1085-
return True
1086-
return False
1087-
1088-
def get_doc_email_aliases(name):
1089-
if name:
1090-
pattern = re.compile(r'^expand-(%s)(\..*?)?@.*? +(.*)$'%name)
1091-
else:
1092-
pattern = re.compile(r'^expand-(.*?)(\..*?)?@.*? +(.*)$')
1093-
aliases = []
1094-
with io.open(settings.DRAFT_VIRTUAL_PATH,"r") as virtual_file:
1095-
for line in virtual_file.readlines():
1096-
m = pattern.match(line)
1097-
if m:
1098-
aliases.append({'doc_name':m.group(1),'alias_type':m.group(2),'expansion':m.group(3)})
1099-
return aliases
11001075

11011076
def document_email(request,name):
11021077
doc = get_object_or_404(Document, name=name)
@@ -2021,16 +1996,26 @@ def remind_action_holders(request, name):
20211996
)
20221997

20231998

2024-
def email_aliases(request,name=''):
2025-
doc = get_object_or_404(Document, name=name) if name else None
2026-
if not name:
2027-
# require login for the overview page, but not for the
2028-
# document-specific pages
2029-
if not request.user.is_authenticated:
2030-
return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
2031-
aliases = get_doc_email_aliases(name)
2032-
2033-
return render(request,'doc/email_aliases.html',{'aliases':aliases,'ietf_domain':settings.IETF_DOMAIN,'doc':doc})
1999+
@login_required
2000+
def email_aliases(request):
2001+
"""List of all email aliases
2002+
2003+
This is currently slow except when cached
2004+
"""
2005+
slowcache = caches["slowpages"]
2006+
cache_key = "emailaliasesview"
2007+
aliases = slowcache.get(cache_key)
2008+
if not aliases:
2009+
aliases = get_doc_email_aliases() # gets all aliases
2010+
slowcache.set(cache_key, aliases, 3600)
2011+
return render(
2012+
request,
2013+
"doc/email_aliases.html",
2014+
{
2015+
"aliases": aliases,
2016+
"ietf_domain": settings.IETF_DOMAIN,
2017+
},
2018+
)
20342019

20352020
class VersionForm(forms.Form):
20362021

ietf/settings.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,11 +1065,6 @@ def skip_unreadable_post(record):
10651065

10661066
TEST_DATA_DIR = os.path.abspath(BASE_DIR + "/../test/data")
10671067

1068-
# Path to the email alias lists. Used by ietf.utils.aliases
1069-
DRAFT_ALIASES_PATH = os.path.join(TEST_DATA_DIR, "draft-aliases")
1070-
DRAFT_VIRTUAL_PATH = os.path.join(TEST_DATA_DIR, "draft-virtual")
1071-
DRAFT_VIRTUAL_DOMAIN = "virtual.ietf.org"
1072-
10731068
GROUP_ALIASES_PATH = os.path.join(TEST_DATA_DIR, "group-aliases")
10741069
GROUP_VIRTUAL_PATH = os.path.join(TEST_DATA_DIR, "group-virtual")
10751070
GROUP_VIRTUAL_DOMAIN = "virtual.ietf.org"

0 commit comments

Comments
 (0)