Skip to content

Commit 606cedd

Browse files
committed
Merged in the ExtResource work from rjsparks@nostrum.com, based on a feature request and contributions from rsalz@akamai.com. This provides external resource models for Person, Group, and Document models, as a generalisation of the previous related-URL concept. This provides a consistent labelling and classification of URLs and other resources related to a Group, Document, or Person. The GroupURL (and similar) classes will be removed in a later step. Some DocumentURL instances (such as auth48 URLs) remain to convert, as they have come in with other merged-in work while the ExtResource work was in transit.
- Legacy-Id: 18192
2 parents 0435ee7 + 6227162 commit 606cedd

59 files changed

Lines changed: 1440 additions & 944 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

ietf/doc/admin.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
1212
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
1313
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL,
14-
ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent )
14+
ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource )
1515

16+
from ietf.utils.validators import validate_external_resource_value
1617

1718
class StateTypeAdmin(admin.ModelAdmin):
1819
list_display = ["slug", "label"]
@@ -183,3 +184,14 @@ class DocumentUrlAdmin(admin.ModelAdmin):
183184
search_fields = ['doc__name', 'url', ]
184185
raw_id_fields = ['doc', ]
185186
admin.site.register(DocumentURL, DocumentUrlAdmin)
187+
188+
class DocExtResourceAdminForm(forms.ModelForm):
189+
def clean(self):
190+
validate_external_resource_value(self.cleaned_data['name'],self.cleaned_data['value'])
191+
192+
class DocExtResourceAdmin(admin.ModelAdmin):
193+
form = DocExtResourceAdminForm
194+
list_display = ['id', 'doc', 'name', 'display_name', 'value',]
195+
search_fields = ['doc__name', 'value', 'display_name', 'name__slug',]
196+
raw_id_fields = ['doc', ]
197+
admin.site.register(DocExtResource, DocExtResourceAdmin)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright The IETF Trust 2020, All Rights Reserved
2+
3+
from django.core.management.base import BaseCommand
4+
from django.db.models import F
5+
6+
from ietf.doc.models import DocExtResource
7+
from ietf.group.models import GroupExtResource
8+
from ietf.person.models import PersonExtResource
9+
10+
class Command(BaseCommand):
11+
help = ('Locate information about gihub repositories to backup')
12+
13+
def handle(self, *args, **options):
14+
15+
info_dict = {}
16+
17+
18+
for repo in DocExtResource.objects.filter(name__slug='github_repo'):
19+
if not repo.value.endswith('/'):
20+
repo.value += '/'
21+
if repo not in info_dict:
22+
info_dict[repo.value] = []
23+
for username in DocExtResource.objects.filter(name__slug='github_username', doc=F('doc')):
24+
info_dict[repo.value].push(username.value)
25+
26+
for repo in GroupExtResource.objects.filter(name__slug='github_repo'):
27+
if not repo.value.endswith('/'):
28+
repo.value += '/'
29+
if repo not in info_dict:
30+
info_dict[repo.value] = []
31+
for username in GroupExtResource.objects.filter(name__slug='github_username', group=F('group')):
32+
info_dict[repo.value].push(username.value)
33+
34+
for repo in PersonExtResource.objects.filter(name__slug='github_repo'):
35+
if not repo.value.endswith('/'):
36+
repo.value += '/'
37+
if repo not in info_dict:
38+
info_dict[repo.value] = []
39+
for username in PersonExtResource.objects.filter(name__slug='github_username', person=F('person')):
40+
info_dict[repo.value].push(username.value)
41+
42+
#print (json.dumps(info_dict))
43+
# For now, all we need are the repo names
44+
for name in info_dict.keys():
45+
print(name)

ietf/doc/migrations/0034_extres.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.29 on 2020-04-15 10:20
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
import ietf.utils.models
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
('name', '0014_extres'),
14+
('doc', '0033_populate_auth48_urls'),
15+
]
16+
17+
operations = [
18+
migrations.CreateModel(
19+
name='DocExtResource',
20+
fields=[
21+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22+
('display_name', models.CharField(blank=True, default='', max_length=255)),
23+
('value', models.CharField(max_length=2083)),
24+
('doc', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document')),
25+
('name', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ExtResourceName')),
26+
],
27+
),
28+
]
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Copyright The IETF Trust 2020, All Rights Reserved
2+
# -*- coding: utf-8 -*-
3+
# Generated by Django 1.11.29 on 2020-03-19 13:06
4+
from __future__ import unicode_literals
5+
6+
import re
7+
8+
import debug # pyflakes:ignore
9+
10+
from collections import OrderedDict, Counter
11+
from io import StringIO
12+
13+
from django.db import migrations
14+
15+
from ietf.utils.validators import validate_external_resource_value
16+
from django.core.exceptions import ValidationError
17+
18+
19+
name_map = {
20+
"Issue.*": "tracker",
21+
".*FAQ.*": "faq",
22+
".*Area Web Page": "webpage",
23+
".*Wiki": "wiki",
24+
"Home Page": "webpage",
25+
"Slack.*": "slack",
26+
"Additional .* Web Page": "webpage",
27+
"Additional .* Page": "webpage",
28+
"Yang catalog entry.*": "yc_entry",
29+
"Yang impact analysis.*": "yc_impact",
30+
"GitHub": "github_repo",
31+
"Github page": "github_repo",
32+
"GitHub repo.*": "github_repo",
33+
"Github repository.*": "github_repo",
34+
"GitHub org.*": "github_org",
35+
"GitHub User.*": "github_username",
36+
"GitLab User": "gitlab_username",
37+
"GitLab User Name": "gitlab_username",
38+
}
39+
40+
url_map = OrderedDict({
41+
"https?://github\\.com": "github_repo",
42+
"https://git.sr.ht/": "repo",
43+
"https://todo.sr.ht/": "tracker",
44+
"https?://trac\\.ietf\\.org/.*/wiki": "wiki",
45+
"ietf\\.org.*/trac/wiki": "wiki",
46+
"trac.*wiki": "wiki",
47+
"www\\.ietf\\.org/mailman" : None,
48+
"www\\.ietf\\.org/mail-archive" : None,
49+
"mailarchive\\.ietf\\.org" : None,
50+
"ietf\\.org/logs": "jabber_log",
51+
"ietf\\.org/jabber/logs": "jabber_log",
52+
"xmpp:.*?join": "jabber_room",
53+
"bell-labs\\.com": None,
54+
"html\\.charters": None,
55+
"datatracker\\.ietf\\.org": None,
56+
})
57+
58+
def forward(apps, schema_editor):
59+
DocExtResource = apps.get_model('doc', 'DocExtResource')
60+
ExtResourceName = apps.get_model('name', 'ExtResourceName')
61+
DocumentUrl = apps.get_model('doc', 'DocumentUrl')
62+
63+
stats = Counter()
64+
stats_file = StringIO()
65+
66+
for doc_url in DocumentUrl.objects.all():
67+
doc_url.url = doc_url.url.strip()
68+
match_found = False
69+
for regext,slug in name_map.items():
70+
if re.fullmatch(regext, doc_url.desc):
71+
match_found = True
72+
stats['mapped'] += 1
73+
name = ExtResourceName.objects.get(slug=slug)
74+
try:
75+
validate_external_resource_value(name, doc_url.url)
76+
DocExtResource.objects.create(doc=doc_url.doc, name_id=slug, value=doc_url.url, display_name=doc_url.desc)
77+
except ValidationError as e: # pyflakes:ignore
78+
print("Failed validation:", doc_url.url, e, file=stats_file)
79+
stats['failed_validation'] +=1
80+
break
81+
if not match_found:
82+
for regext, slug in url_map.items():
83+
if re.search(regext, doc_url.url):
84+
match_found = True
85+
if slug:
86+
stats['mapped'] +=1
87+
name = ExtResourceName.objects.get(slug=slug)
88+
# Munge the URL if it's the first github repo match
89+
# Remove "/tree/master" substring if it exists
90+
# Remove trailing "/issues" substring if it exists
91+
# Remove "/blob/master/.*" pattern if present
92+
if regext == "https?://github\\.com":
93+
doc_url.url = doc_url.url.replace("/tree/master","")
94+
doc_url.url = re.sub('/issues$', '', doc_url.url)
95+
doc_url.url = re.sub('/blob/master.*$', '', doc_url.url)
96+
try:
97+
validate_external_resource_value(name, doc_url.url)
98+
DocExtResource.objects.create(doc=doc_url.doc, name=name, value=doc_url.url, display_name=doc_url.desc)
99+
except ValidationError as e: # pyflakes:ignore
100+
print("Failed validation:", doc_url.url, e, file=stats_file)
101+
stats['failed_validation'] +=1
102+
else:
103+
stats['ignored'] +=1
104+
break
105+
if not match_found:
106+
print("Not Mapped:", doc_url.desc, doc_url.tag.slug, doc_url.doc.name, doc_url.url, file=stats_file)
107+
stats['not_mapped'] += 1
108+
print('')
109+
print(stats_file.getvalue())
110+
print (stats)
111+
112+
def reverse(apps, schema_editor):
113+
DocExtResource = apps.get_model('doc', 'DocExtResource')
114+
DocExtResource.objects.all().delete()
115+
116+
class Migration(migrations.Migration):
117+
118+
dependencies = [
119+
('doc', '0034_extres'),
120+
('name', '0015_populate_extres'),
121+
]
122+
123+
operations = [
124+
migrations.RunPython(forward, reverse)
125+
]

ietf/doc/models.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from ietf.group.models import Group
2525
from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName,
2626
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, ReviewAssignmentStateName, FormalLanguageName,
27-
DocUrlTagName)
27+
DocUrlTagName, ExtResourceName)
2828
from ietf.person.models import Email, Person
2929
from ietf.person.utils import get_active_balloters
3030
from ietf.utils import log
@@ -862,6 +862,15 @@ class DocumentURL(models.Model):
862862
desc = models.CharField(max_length=255, default='', blank=True)
863863
url = models.URLField(max_length=2083) # 2083 is the legal max for URLs
864864

865+
class DocExtResource(models.Model):
866+
doc = ForeignKey(Document) # Should this really be to DocumentInfo rather than Document?
867+
name = models.ForeignKey(ExtResourceName, on_delete=models.CASCADE)
868+
display_name = models.CharField(max_length=255, default='', blank=True)
869+
value = models.CharField(max_length=2083) # 2083 is the maximum legal URL length
870+
def __str__(self):
871+
priority = self.display_name or self.name.name
872+
return u"%s (%s) %s" % (priority, self.name.slug, self.value)
873+
865874
class RelatedDocHistory(models.Model):
866875
source = ForeignKey('DocHistory')
867876
target = ForeignKey('DocAlias', related_name="reversely_related_document_history_set")

ietf/doc/resources.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
1818
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
1919
ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL,
20-
IanaExpertDocEvent, IRSGBallotDocEvent )
20+
IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource )
2121

2222
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
2323
class BallotTypeResource(ModelResource):
@@ -767,3 +767,23 @@ class Meta:
767767
"ballotdocevent_ptr": ALL_WITH_RELATIONS,
768768
}
769769
api.doc.register(IRSGBallotDocEventResource())
770+
771+
772+
from ietf.name.resources import ExtResourceNameResource
773+
class DocExtResourceResource(ModelResource):
774+
doc = ToOneField(DocumentResource, 'doc')
775+
name = ToOneField(ExtResourceNameResource, 'name')
776+
class Meta:
777+
queryset = DocExtResource.objects.all()
778+
serializer = api.Serializer()
779+
cache = SimpleCache()
780+
resource_name = 'docextresource'
781+
ordering = ['id', ]
782+
filtering = {
783+
"id": ALL,
784+
"display_name": ALL,
785+
"value": ALL,
786+
"doc": ALL_WITH_RELATIONS,
787+
"name": ALL_WITH_RELATIONS,
788+
}
789+
api.doc.register(DocExtResourceResource())

ietf/doc/tests_draft.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,24 +1104,44 @@ def test_doc_change_shepherd_writeup(self):
11041104
q = PyQuery(r.content)
11051105
self.assertTrue(q('textarea')[0].text.strip().startswith("As required by RFC 4858"))
11061106

1107-
def test_doc_change_document_urls(self):
1108-
url = urlreverse('ietf.doc.views_draft.edit_document_urls', kwargs=dict(name=self.docname))
1109-
1110-
# get
1107+
def test_edit_doc_extresources(self):
1108+
url = urlreverse('ietf.doc.views_draft.edit_doc_extresources', kwargs=dict(name=self.docname))
1109+
11111110
login_testing_unauthorized(self, "secretary", url)
11121111

11131112
r = self.client.get(url)
11141113
self.assertEqual(r.status_code,200)
11151114
q = PyQuery(r.content)
1116-
self.assertEqual(len(q('form textarea[id=id_urls]')),1)
1115+
self.assertEqual(len(q('form textarea[id=id_resources]')),1)
11171116

1118-
# direct edit
1119-
r = self.client.post(url, dict(urls='wiki https://wiki.org/ Wiki\nrepository https://repository.org/ Repo\n', submit="1"))
1117+
badlines = (
1118+
'github_repo https://github3.com/some/repo',
1119+
'github_notify badaddr',
1120+
'website /not/a/good/url',
1121+
'notavalidtag blahblahblah',
1122+
)
1123+
1124+
for line in badlines:
1125+
r = self.client.post(url, dict(resources=line, submit="1"))
1126+
self.assertEqual(r.status_code, 200)
1127+
q = PyQuery(r.content)
1128+
self.assertTrue(q('.alert-danger'))
1129+
1130+
goodlines = """
1131+
github_repo https://github.com/some/repo Some display text
1132+
github_username githubuser
1133+
webpage http://example.com/http/is/fine
1134+
"""
1135+
1136+
r = self.client.post(url, dict(resources=goodlines, submit="1"))
11201137
self.assertEqual(r.status_code,302)
11211138
doc = Document.objects.get(name=self.docname)
1122-
self.assertTrue(doc.latest_event(DocEvent,type="changed_document").desc.startswith('Changed document URLs'))
1123-
self.assertIn('wiki https://wiki.org/', doc.latest_event(DocEvent,type="changed_document").desc)
1124-
self.assertIn('https://wiki.org/', [ u.url for u in doc.documenturl_set.all() ])
1139+
self.assertEqual(doc.latest_event(DocEvent,type="changed_document").desc[:35], 'Changed document external resources')
1140+
self.assertIn('github_username githubuser', doc.latest_event(DocEvent,type="changed_document").desc)
1141+
self.assertEqual(doc.docextresource_set.count(), 3)
1142+
self.assertEqual(doc.docextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
1143+
self.assertIn(doc.docextresource_set.first().name.slug,str(doc.docextresource_set.first()))
1144+
11251145

11261146
class SubmitToIesgTests(TestCase):
11271147

ietf/doc/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
url(r'^%(name)s/edit/approveballot/$' % settings.URL_REGEXPS, views_ballot.approve_ballot),
129129
url(r'^%(name)s/edit/approvedownrefs/$' % settings.URL_REGEXPS, views_ballot.approve_downrefs),
130130
url(r'^%(name)s/edit/makelastcall/$' % settings.URL_REGEXPS, views_ballot.make_last_call),
131-
url(r'^%(name)s/edit/urls/$' % settings.URL_REGEXPS, views_draft.edit_document_urls),
131+
url(r'^%(name)s/edit/resources/$' % settings.URL_REGEXPS, views_draft.edit_doc_extresources),
132132
url(r'^%(name)s/edit/issueballot/irsg/$' % settings.URL_REGEXPS, views_ballot.issue_irsg_ballot),
133133
url(r'^%(name)s/edit/closeballot/irsg/$' % settings.URL_REGEXPS, views_ballot.close_irsg_ballot),
134134

0 commit comments

Comments
 (0)