Skip to content

Commit 7e57be2

Browse files
committed
merged forward ^/personal/rjs/explore-extref
- Legacy-Id: 17840
2 parents cf94987 + 7587d56 commit 7e57be2

26 files changed

Lines changed: 15658 additions & 14537 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Copyright The IETF Trust 2020, All Rights Reserved
2+
import json
3+
4+
from django.core.management.base import BaseCommand
5+
6+
from ietf.extresource.models import ExtResource
7+
from ietf.doc.models import DocExtResource
8+
from ietf.group.models import GroupExtResource
9+
from ietf.person.models import PersonExtResource
10+
11+
class Command(BaseCommand):
12+
help = ('Locate information about gihub repositories to backup')
13+
14+
def handle(self, *args, **options):
15+
16+
info_dict = {}
17+
for repo in ExtResource.objects.filter(name__slug='github_repo'):
18+
if repo not in info_dict:
19+
info_dict[repo.value] = []
20+
21+
for username in DocExtResource.objects.filter(extresource__name__slug='github_username', doc__name__in=repo.docextresource_set.values_list('doc__name',flat=True).distinct()):
22+
info_dict[repo.value].push(username.value)
23+
24+
for username in GroupExtResource.objects.filter(extresource__name__slug='github_username', group__acronym__in=repo.groupextresource_set.values_list('group__acronym',flat=True).distinct()):
25+
info_dict[repo.value].push(username.value)
26+
27+
for username in PersonExtResource.objects.filter(extresource__name__slug='github_username', person_id__in=repo.personextresource_set.values_list('person__id',flat=True).distinct()):
28+
info_dict[repo.value].push(username.value)
29+
30+
print (json.dumps(info_dict))

ietf/doc/migrations/0032_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', '0012_extres'),
14+
('doc', '0031_set_state_for_charters_of_replaced_groups'),
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: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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
9+
10+
from collections import OrderedDict, Counter
11+
12+
from django.db import migrations
13+
14+
from ietf.utils.validators import validate_external_resource_value
15+
from django.core.exceptions import ValidationError
16+
17+
18+
name_map = {
19+
"Issue.*": "tracker",
20+
".*FAQ.*": "faq",
21+
".*Area Web Page": "webpage",
22+
".*Wiki": "wiki",
23+
"Home Page": "webpage",
24+
"Slack.*": "slack",
25+
"Additional .* Web Page": "webpage",
26+
"Additional .* Page": "webpage",
27+
"Yang catalog entry.*": "yc_entry",
28+
"Yang impact analysis.*": "yc_impact",
29+
"GitHub": "github_repo",
30+
"Github page": "github_repo",
31+
"GitHub repo.*": "github_repo",
32+
"Github repository.*": "github_repo",
33+
"GitHub notifications": "github_notify",
34+
"GitHub org.*": "github_org",
35+
"GitHub User.*": "github_username",
36+
"GitLab User": "gitlab_username",
37+
"GitLab User Name": "gitlab_username",
38+
}
39+
40+
# TODO: Review all the None values below and make sure ignoring the URLs they match is really the right thing to do.
41+
url_map = OrderedDict({
42+
"https?://github\\.com": "github_repo",
43+
"https?://trac\\.ietf\\.org/.*/wiki": "wiki",
44+
"ietf\\.org.*/trac/wiki": "wiki",
45+
"trac.*wiki": "wiki",
46+
"www\\.ietf\\.org/mailman" : None,
47+
"www\\.ietf\\.org/mail-archive" : None,
48+
"mailarchive\\.ietf\\.org" : None,
49+
"ietf\\.org/logs": "jabber_log",
50+
"ietf\\.org/jabber/logs": "jabber_log",
51+
"xmpp:.*?join": "jabber_room",
52+
"bell-labs\\.com": None,
53+
"html\\.charters": None,
54+
"datatracker\\.ietf\\.org": None,
55+
})
56+
57+
def forward(apps, schema_editor):
58+
DocExtResource = apps.get_model('doc', 'DocExtResource')
59+
ExtResourceName = apps.get_model('name', 'ExtResourceName')
60+
DocumentUrl = apps.get_model('doc', 'DocumentUrl')
61+
62+
stats = Counter()
63+
64+
for doc_url in DocumentUrl.objects.all():
65+
match_found = False
66+
for regext,slug in name_map.items():
67+
if re.match(regext, doc_url.desc):
68+
match_found = True
69+
stats['mapped'] += 1
70+
name = ExtResourceName.objects.get(slug=slug)
71+
DocExtResource.objects.create(doc=doc_url.doc, name_id=slug, value=doc_url.url, display_name=doc_url.desc) # TODO: validate this value against name.type
72+
break
73+
if not match_found:
74+
for regext, slug in url_map.items():
75+
doc_url.url = doc_url.url.strip()
76+
if re.search(regext, doc_url.url):
77+
match_found = True
78+
if slug:
79+
stats['mapped'] +=1
80+
name = ExtResourceName.objects.get(slug=slug)
81+
# Munge the URL if it's the first github repo match
82+
# Remove "/tree/master" substring if it exists
83+
# Remove trailing "/issues" substring if it exists
84+
# Remove "/blob/master/.*" pattern if present
85+
if regext == "https?://github\\.com":
86+
doc_url.url = doc_url.url.replace("/tree/master","")
87+
doc_url.url = re.sub('/issues$', '', doc_url.url)
88+
doc_url.url = re.sub('/blob/master.*$', '', doc_url.url)
89+
try:
90+
validate_external_resource_value(name, doc_url.url)
91+
DocExtResource.objects.create(doc=doc_url.doc, name=name, value=doc_url.url, display_name=doc_url.desc) # TODO: validate this value against name.type
92+
except ValidationError as e: # pyflakes:ignore
93+
debug.show('("Failed validation:", doc_url.url, e)')
94+
stats['failed_validation'] +=1
95+
else:
96+
stats['ignored'] +=1
97+
break
98+
if not match_found:
99+
debug.show('("Not Mapped:",doc_url.desc, doc_url.tag.slug, doc_url.doc.name, doc_url.url)')
100+
stats['not_mapped'] += 1
101+
print (stats)
102+
103+
def reverse(apps, schema_editor):
104+
DocExtResource = apps.get_model('doc', 'DocExtResource')
105+
DocExtResource.objects.all().delete()
106+
107+
class Migration(migrations.Migration):
108+
109+
dependencies = [
110+
('doc', '0032_extres'),
111+
('name', '0013_populate_extres'),
112+
]
113+
114+
operations = [
115+
migrations.RunPython(forward, reverse)
116+
]

ietf/doc/models.py

Lines changed: 8 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
@@ -105,6 +105,7 @@ class DocumentInfo(models.Model):
105105
note = models.TextField(blank=True)
106106
internal_comments = models.TextField(blank=True)
107107

108+
108109
def file_extension(self):
109110
if not hasattr(self, '_cached_extension'):
110111
if self.uploaded_filename:
@@ -861,6 +862,12 @@ class DocumentURL(models.Model):
861862
desc = models.CharField(max_length=255, default='', blank=True)
862863
url = models.URLField(max_length=2083) # 2083 is the legal max for URLs
863864

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+
864871
class RelatedDocHistory(models.Model):
865872
source = ForeignKey('DocHistory')
866873
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: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,45 @@ def test_doc_change_document_urls(self):
11231123
self.assertIn('wiki https://wiki.org/', doc.latest_event(DocEvent,type="changed_document").desc)
11241124
self.assertIn('https://wiki.org/', [ u.url for u in doc.documenturl_set.all() ])
11251125

1126+
def test_edit_doc_extresources(self):
1127+
url = urlreverse('ietf.doc.views_draft.edit_doc_extresources', kwargs=dict(name=self.docname))
1128+
1129+
login_testing_unauthorized(self, "secretary", url)
1130+
1131+
r = self.client.get(url)
1132+
self.assertEqual(r.status_code,200)
1133+
q = PyQuery(r.content)
1134+
self.assertEqual(len(q('form textarea[id=id_resources]')),1)
1135+
1136+
badlines = (
1137+
'github_repo https://github3.com/some/repo',
1138+
'github_notify badaddr',
1139+
'website /not/a/good/url'
1140+
'notavalidtag blahblahblah'
1141+
)
1142+
1143+
for line in badlines:
1144+
r = self.client.post(url, dict(resources=line, submit="1"))
1145+
self.assertEqual(r.status_code, 200)
1146+
q = PyQuery(r.content)
1147+
self.assertTrue(q('.alert-danger'))
1148+
1149+
goodlines = """
1150+
github_repo https://github.com/some/repo Some display text
1151+
github_notify notify@example.com
1152+
github_username githubuser
1153+
website http://example.com/http/is/fine
1154+
"""
1155+
1156+
r = self.client.post(url, dict(resources=goodlines, submit="1"))
1157+
self.assertEqual(r.status_code,302)
1158+
doc = Document.objects.get(name=self.docname)
1159+
self.assertEqual(doc.latest_event(DocEvent,type="changed_document").desc[:35], 'Changed document external resources')
1160+
self.assertIn('github_username githubuser', doc.latest_event(DocEvent,type="changed_document").desc)
1161+
self.assertEqual(doc.docextresource_set.count(), 4)
1162+
self.assertEqual(doc.docextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
1163+
1164+
11261165
class SubmitToIesgTests(TestCase):
11271166

11281167
def setUp(self):

ietf/doc/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
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),
131131
url(r'^%(name)s/edit/urls/$' % settings.URL_REGEXPS, views_draft.edit_document_urls),
132+
url(r'^%(name)s/edit/resources/$' % settings.URL_REGEXPS, views_draft.edit_doc_extresources),
132133
url(r'^%(name)s/edit/issueballot/irsg/$' % settings.URL_REGEXPS, views_ballot.issue_irsg_ballot),
133134
url(r'^%(name)s/edit/closeballot/irsg/$' % settings.URL_REGEXPS, views_ballot.close_irsg_ballot),
134135

ietf/doc/views_draft.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,12 @@
4343
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, is_individual_draft_author
4444
from ietf.ietfauth.utils import role_required
4545
from ietf.message.models import Message
46-
from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName, DocUrlTagName
46+
from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName, DocUrlTagName, ExtResourceName
4747
from ietf.person.fields import SearchableEmailField
4848
from ietf.person.models import Person, Email
4949
from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of
5050
from ietf.utils.textupload import get_cleaned_text_file_content
51+
from ietf.utils.validators import validate_external_resource_value
5152
from ietf.utils import log
5253
from ietf.mailtrigger.utils import gather_address_lists
5354

@@ -1253,6 +1254,88 @@ def format_urls(urls, fs="\n"):
12531254
title = "Additional document URLs"
12541255
return render(request, 'doc/edit_field.html',dict(doc=doc, form=form, title=title, info=info) )
12551256

1257+
1258+
def edit_doc_extresources(request, name):
1259+
class DocExtResourceForm(forms.Form):
1260+
resources = forms.CharField(widget=forms.Textarea, label="Additional Resources", required=False,
1261+
help_text=("Format: 'tag value (Optional description)'."
1262+
" Separate multiple entries with newline. When the value is a URL, use https:// where possible.") )
1263+
1264+
def clean_resources(self):
1265+
lines = [x.strip() for x in self.cleaned_data["resources"].splitlines() if x.strip()]
1266+
errors = []
1267+
for l in lines:
1268+
parts = l.split()
1269+
if len(parts) == 1:
1270+
errors.append("Too few fields: Expected at least tag and value: '%s'" % l)
1271+
elif len(parts) >= 2:
1272+
name_slug = parts[0]
1273+
try:
1274+
name = ExtResourceName.objects.get(slug=name_slug)
1275+
except ObjectDoesNotExist:
1276+
errors.append("Bad tag in '%s': Expected one of %s" % (l, ', '.join([ o.slug for o in ExtResourceName.objects.all() ])))
1277+
continue
1278+
value = parts[1]
1279+
try:
1280+
validate_external_resource_value(name, value)
1281+
except ValidationError as e:
1282+
e.message += " : " + value
1283+
errors.append(e)
1284+
if errors:
1285+
raise ValidationError(errors)
1286+
return lines
1287+
1288+
def format_resources(resources, fs="\n"):
1289+
res = []
1290+
for r in resources:
1291+
if r.display_name:
1292+
res.append("%s %s (%s)" % (r.name.slug, r.value, r.display_name.strip('()')))
1293+
else:
1294+
res.append("%s %s" % (r.name.slug, r.value))
1295+
# TODO: This is likely problematic if value has spaces. How then to delineate value and display_name? Perhaps in the short term move to comma or pipe separation.
1296+
# Might be better to shift to a formset instead of parsing these lines.
1297+
return fs.join(res)
1298+
1299+
doc = get_object_or_404(Document, name=name)
1300+
1301+
if not (has_role(request.user, ("Secretariat", "Area Director"))
1302+
or is_authorized_in_doc_stream(request.user, doc)
1303+
or is_individual_draft_author(request.user, doc)):
1304+
return HttpResponseForbidden("You do not have the necessary permissions to view this page")
1305+
1306+
old_resources = format_resources(doc.docextresource_set.all())
1307+
1308+
if request.method == 'POST':
1309+
form = DocExtResourceForm(request.POST)
1310+
if form.is_valid():
1311+
old_resources = sorted(old_resources.splitlines())
1312+
new_resources = sorted(form.cleaned_data['resources'])
1313+
if old_resources != new_resources:
1314+
doc.docextresource_set.all().delete()
1315+
for u in new_resources:
1316+
parts = u.split(None, 2)
1317+
name = parts[0]
1318+
value = parts[1]
1319+
display_name = ' '.join(parts[2:]).strip('()')
1320+
doc.docextresource_set.create(value=value, name_id=name, display_name=display_name)
1321+
new_resources = format_resources(doc.docextresource_set.all())
1322+
e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='changed_document')
1323+
e.desc = "Changed document external resources from:\n\n%s\n\nto:\n\n%s" % (old_resources, new_resources)
1324+
e.save()
1325+
doc.save_with_history([e])
1326+
messages.success(request,"Document resources updated.")
1327+
else:
1328+
messages.info(request,"No change in Document resources.")
1329+
return redirect('ietf.doc.views_doc.document_main', name=doc.name)
1330+
else:
1331+
form = DocExtResourceForm(initial={'resources': old_resources, })
1332+
1333+
info = "Valid tags:<br><br> %s" % ', '.join([ o.slug for o in ExtResourceName.objects.all().order_by('slug') ])
1334+
# May need to explain the tags more - probably more reason to move to a formset.
1335+
title = "Additional document resources"
1336+
return render(request, 'doc/edit_field.html',dict(doc=doc, form=form, title=title, info=info) )
1337+
1338+
12561339
def request_publication(request, name):
12571340
"""Request publication by RFC Editor for a document which hasn't
12581341
been through the IESG ballot process."""

0 commit comments

Comments
 (0)