merged forward ^/personal/rjs/explore-extref

- Legacy-Id: 17840
This commit is contained in:
Robert Sparks 2020-05-19 18:47:47 +00:00
commit 7e57be2bd3
26 changed files with 15658 additions and 14537 deletions

View file

@ -0,0 +1,30 @@
# Copyright The IETF Trust 2020, All Rights Reserved
import json
from django.core.management.base import BaseCommand
from ietf.extresource.models import ExtResource
from ietf.doc.models import DocExtResource
from ietf.group.models import GroupExtResource
from ietf.person.models import PersonExtResource
class Command(BaseCommand):
help = ('Locate information about gihub repositories to backup')
def handle(self, *args, **options):
info_dict = {}
for repo in ExtResource.objects.filter(name__slug='github_repo'):
if repo not in info_dict:
info_dict[repo.value] = []
for username in DocExtResource.objects.filter(extresource__name__slug='github_username', doc__name__in=repo.docextresource_set.values_list('doc__name',flat=True).distinct()):
info_dict[repo.value].push(username.value)
for username in GroupExtResource.objects.filter(extresource__name__slug='github_username', group__acronym__in=repo.groupextresource_set.values_list('group__acronym',flat=True).distinct()):
info_dict[repo.value].push(username.value)
for username in PersonExtResource.objects.filter(extresource__name__slug='github_username', person_id__in=repo.personextresource_set.values_list('person__id',flat=True).distinct()):
info_dict[repo.value].push(username.value)
print (json.dumps(info_dict))

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-04-15 10:20
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('name', '0012_extres'),
('doc', '0031_set_state_for_charters_of_replaced_groups'),
]
operations = [
migrations.CreateModel(
name='DocExtResource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('display_name', models.CharField(blank=True, default='', max_length=255)),
('value', models.CharField(max_length=2083)),
('doc', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document')),
('name', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ExtResourceName')),
],
),
]

View file

@ -0,0 +1,116 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-03-19 13:06
from __future__ import unicode_literals
import re
import debug
from collections import OrderedDict, Counter
from django.db import migrations
from ietf.utils.validators import validate_external_resource_value
from django.core.exceptions import ValidationError
name_map = {
"Issue.*": "tracker",
".*FAQ.*": "faq",
".*Area Web Page": "webpage",
".*Wiki": "wiki",
"Home Page": "webpage",
"Slack.*": "slack",
"Additional .* Web Page": "webpage",
"Additional .* Page": "webpage",
"Yang catalog entry.*": "yc_entry",
"Yang impact analysis.*": "yc_impact",
"GitHub": "github_repo",
"Github page": "github_repo",
"GitHub repo.*": "github_repo",
"Github repository.*": "github_repo",
"GitHub notifications": "github_notify",
"GitHub org.*": "github_org",
"GitHub User.*": "github_username",
"GitLab User": "gitlab_username",
"GitLab User Name": "gitlab_username",
}
# TODO: Review all the None values below and make sure ignoring the URLs they match is really the right thing to do.
url_map = OrderedDict({
"https?://github\\.com": "github_repo",
"https?://trac\\.ietf\\.org/.*/wiki": "wiki",
"ietf\\.org.*/trac/wiki": "wiki",
"trac.*wiki": "wiki",
"www\\.ietf\\.org/mailman" : None,
"www\\.ietf\\.org/mail-archive" : None,
"mailarchive\\.ietf\\.org" : None,
"ietf\\.org/logs": "jabber_log",
"ietf\\.org/jabber/logs": "jabber_log",
"xmpp:.*?join": "jabber_room",
"bell-labs\\.com": None,
"html\\.charters": None,
"datatracker\\.ietf\\.org": None,
})
def forward(apps, schema_editor):
DocExtResource = apps.get_model('doc', 'DocExtResource')
ExtResourceName = apps.get_model('name', 'ExtResourceName')
DocumentUrl = apps.get_model('doc', 'DocumentUrl')
stats = Counter()
for doc_url in DocumentUrl.objects.all():
match_found = False
for regext,slug in name_map.items():
if re.match(regext, doc_url.desc):
match_found = True
stats['mapped'] += 1
name = ExtResourceName.objects.get(slug=slug)
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
break
if not match_found:
for regext, slug in url_map.items():
doc_url.url = doc_url.url.strip()
if re.search(regext, doc_url.url):
match_found = True
if slug:
stats['mapped'] +=1
name = ExtResourceName.objects.get(slug=slug)
# Munge the URL if it's the first github repo match
# Remove "/tree/master" substring if it exists
# Remove trailing "/issues" substring if it exists
# Remove "/blob/master/.*" pattern if present
if regext == "https?://github\\.com":
doc_url.url = doc_url.url.replace("/tree/master","")
doc_url.url = re.sub('/issues$', '', doc_url.url)
doc_url.url = re.sub('/blob/master.*$', '', doc_url.url)
try:
validate_external_resource_value(name, doc_url.url)
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
except ValidationError as e: # pyflakes:ignore
debug.show('("Failed validation:", doc_url.url, e)')
stats['failed_validation'] +=1
else:
stats['ignored'] +=1
break
if not match_found:
debug.show('("Not Mapped:",doc_url.desc, doc_url.tag.slug, doc_url.doc.name, doc_url.url)')
stats['not_mapped'] += 1
print (stats)
def reverse(apps, schema_editor):
DocExtResource = apps.get_model('doc', 'DocExtResource')
DocExtResource.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('doc', '0032_extres'),
('name', '0013_populate_extres'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -24,7 +24,7 @@ import debug # pyflakes:ignore
from ietf.group.models import Group
from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName,
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, ReviewAssignmentStateName, FormalLanguageName,
DocUrlTagName)
DocUrlTagName, ExtResourceName)
from ietf.person.models import Email, Person
from ietf.person.utils import get_active_balloters
from ietf.utils import log
@ -105,6 +105,7 @@ class DocumentInfo(models.Model):
note = models.TextField(blank=True)
internal_comments = models.TextField(blank=True)
def file_extension(self):
if not hasattr(self, '_cached_extension'):
if self.uploaded_filename:
@ -861,6 +862,12 @@ class DocumentURL(models.Model):
desc = models.CharField(max_length=255, default='', blank=True)
url = models.URLField(max_length=2083) # 2083 is the legal max for URLs
class DocExtResource(models.Model):
doc = ForeignKey(Document) # Should this really be to DocumentInfo rather than Document?
name = models.ForeignKey(ExtResourceName, on_delete=models.CASCADE)
display_name = models.CharField(max_length=255, default='', blank=True)
value = models.CharField(max_length=2083) # 2083 is the maximum legal URL length
class RelatedDocHistory(models.Model):
source = ForeignKey('DocHistory')
target = ForeignKey('DocAlias', related_name="reversely_related_document_history_set")

View file

@ -17,7 +17,7 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL,
IanaExpertDocEvent, IRSGBallotDocEvent )
IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource )
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
class BallotTypeResource(ModelResource):
@ -767,3 +767,23 @@ class IRSGBallotDocEventResource(ModelResource):
"ballotdocevent_ptr": ALL_WITH_RELATIONS,
}
api.doc.register(IRSGBallotDocEventResource())
from ietf.name.resources import ExtResourceNameResource
class DocExtResourceResource(ModelResource):
doc = ToOneField(DocumentResource, 'doc')
name = ToOneField(ExtResourceNameResource, 'name')
class Meta:
queryset = DocExtResource.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'docextresource'
ordering = ['id', ]
filtering = {
"id": ALL,
"display_name": ALL,
"value": ALL,
"doc": ALL_WITH_RELATIONS,
"name": ALL_WITH_RELATIONS,
}
api.doc.register(DocExtResourceResource())

View file

@ -1123,6 +1123,45 @@ class IndividualInfoFormsTests(TestCase):
self.assertIn('wiki https://wiki.org/', doc.latest_event(DocEvent,type="changed_document").desc)
self.assertIn('https://wiki.org/', [ u.url for u in doc.documenturl_set.all() ])
def test_edit_doc_extresources(self):
url = urlreverse('ietf.doc.views_draft.edit_doc_extresources', kwargs=dict(name=self.docname))
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('form textarea[id=id_resources]')),1)
badlines = (
'github_repo https://github3.com/some/repo',
'github_notify badaddr',
'website /not/a/good/url'
'notavalidtag blahblahblah'
)
for line in badlines:
r = self.client.post(url, dict(resources=line, submit="1"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('.alert-danger'))
goodlines = """
github_repo https://github.com/some/repo Some display text
github_notify notify@example.com
github_username githubuser
website http://example.com/http/is/fine
"""
r = self.client.post(url, dict(resources=goodlines, submit="1"))
self.assertEqual(r.status_code,302)
doc = Document.objects.get(name=self.docname)
self.assertEqual(doc.latest_event(DocEvent,type="changed_document").desc[:35], 'Changed document external resources')
self.assertIn('github_username githubuser', doc.latest_event(DocEvent,type="changed_document").desc)
self.assertEqual(doc.docextresource_set.count(), 4)
self.assertEqual(doc.docextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
class SubmitToIesgTests(TestCase):
def setUp(self):

View file

@ -129,6 +129,7 @@ urlpatterns = [
url(r'^%(name)s/edit/approvedownrefs/$' % settings.URL_REGEXPS, views_ballot.approve_downrefs),
url(r'^%(name)s/edit/makelastcall/$' % settings.URL_REGEXPS, views_ballot.make_last_call),
url(r'^%(name)s/edit/urls/$' % settings.URL_REGEXPS, views_draft.edit_document_urls),
url(r'^%(name)s/edit/resources/$' % settings.URL_REGEXPS, views_draft.edit_doc_extresources),
url(r'^%(name)s/edit/issueballot/irsg/$' % settings.URL_REGEXPS, views_ballot.issue_irsg_ballot),
url(r'^%(name)s/edit/closeballot/irsg/$' % settings.URL_REGEXPS, views_ballot.close_irsg_ballot),

View file

@ -43,11 +43,12 @@ from ietf.iesg.models import TelechatDate
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, is_individual_draft_author
from ietf.ietfauth.utils import role_required
from ietf.message.models import Message
from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName, DocUrlTagName
from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName, DocUrlTagName, ExtResourceName
from ietf.person.fields import SearchableEmailField
from ietf.person.models import Person, Email
from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.utils.validators import validate_external_resource_value
from ietf.utils import log
from ietf.mailtrigger.utils import gather_address_lists
@ -1253,6 +1254,88 @@ def edit_document_urls(request, name):
title = "Additional document URLs"
return render(request, 'doc/edit_field.html',dict(doc=doc, form=form, title=title, info=info) )
def edit_doc_extresources(request, name):
class DocExtResourceForm(forms.Form):
resources = forms.CharField(widget=forms.Textarea, label="Additional Resources", required=False,
help_text=("Format: 'tag value (Optional description)'."
" Separate multiple entries with newline. When the value is a URL, use https:// where possible.") )
def clean_resources(self):
lines = [x.strip() for x in self.cleaned_data["resources"].splitlines() if x.strip()]
errors = []
for l in lines:
parts = l.split()
if len(parts) == 1:
errors.append("Too few fields: Expected at least tag and value: '%s'" % l)
elif len(parts) >= 2:
name_slug = parts[0]
try:
name = ExtResourceName.objects.get(slug=name_slug)
except ObjectDoesNotExist:
errors.append("Bad tag in '%s': Expected one of %s" % (l, ', '.join([ o.slug for o in ExtResourceName.objects.all() ])))
continue
value = parts[1]
try:
validate_external_resource_value(name, value)
except ValidationError as e:
e.message += " : " + value
errors.append(e)
if errors:
raise ValidationError(errors)
return lines
def format_resources(resources, fs="\n"):
res = []
for r in resources:
if r.display_name:
res.append("%s %s (%s)" % (r.name.slug, r.value, r.display_name.strip('()')))
else:
res.append("%s %s" % (r.name.slug, r.value))
# 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.
# Might be better to shift to a formset instead of parsing these lines.
return fs.join(res)
doc = get_object_or_404(Document, name=name)
if not (has_role(request.user, ("Secretariat", "Area Director"))
or is_authorized_in_doc_stream(request.user, doc)
or is_individual_draft_author(request.user, doc)):
return HttpResponseForbidden("You do not have the necessary permissions to view this page")
old_resources = format_resources(doc.docextresource_set.all())
if request.method == 'POST':
form = DocExtResourceForm(request.POST)
if form.is_valid():
old_resources = sorted(old_resources.splitlines())
new_resources = sorted(form.cleaned_data['resources'])
if old_resources != new_resources:
doc.docextresource_set.all().delete()
for u in new_resources:
parts = u.split(None, 2)
name = parts[0]
value = parts[1]
display_name = ' '.join(parts[2:]).strip('()')
doc.docextresource_set.create(value=value, name_id=name, display_name=display_name)
new_resources = format_resources(doc.docextresource_set.all())
e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='changed_document')
e.desc = "Changed document external resources from:\n\n%s\n\nto:\n\n%s" % (old_resources, new_resources)
e.save()
doc.save_with_history([e])
messages.success(request,"Document resources updated.")
else:
messages.info(request,"No change in Document resources.")
return redirect('ietf.doc.views_doc.document_main', name=doc.name)
else:
form = DocExtResourceForm(initial={'resources': old_resources, })
info = "Valid tags:<br><br> %s" % ', '.join([ o.slug for o in ExtResourceName.objects.all().order_by('slug') ])
# May need to explain the tags more - probably more reason to move to a formset.
title = "Additional document resources"
return render(request, 'doc/edit_field.html',dict(doc=doc, form=form, title=title, info=info) )
def request_publication(request, name):
"""Request publication by RFC Editor for a document which hasn't
been through the IESG ballot process."""

View file

@ -11,10 +11,11 @@ import debug # pyflakes:ignore
# Django imports
from django import forms
from django.utils.html import mark_safe # type:ignore
from django.core.exceptions import ValidationError, ObjectDoesNotExist
# IETF imports
from ietf.group.models import Group, GroupHistory, GroupStateName
from ietf.name.models import ReviewTypeName
from ietf.name.models import ReviewTypeName, ExtResourceName
from ietf.person.fields import SearchableEmailsField, PersonEmailChoiceField
from ietf.person.models import Person
from ietf.review.models import ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
@ -24,6 +25,7 @@ from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.utils.text import strip_suffix
#from ietf.utils.ordereddict import insert_after_in_ordered_dict
from ietf.utils.fields import DatepickerDateField, MultiEmailField
from ietf.utils.validators import validate_external_resource_value
# --- Constants --------------------------------------------------------
@ -80,6 +82,7 @@ class GroupForm(forms.Form):
list_subscribe = forms.CharField(max_length=255, required=False)
list_archive = forms.CharField(max_length=255, required=False)
urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", help_text="Format: https://site/path (Optional description). Separate multiple entries with newline. Prefer HTTPS URLs where possible.", required=False)
resources = forms.CharField(widget=forms.Textarea, label="Additional Resources", help_text="UPDATEME: Format: https://site/path (Optional description). Separate multiple entries with newline. Prefer HTTPS URLs where possible.", required=False)
closing_note = forms.CharField(widget=forms.Textarea, label="Closing note", required=False)
def __init__(self, *args, **kwargs):
@ -125,6 +128,12 @@ class GroupForm(forms.Form):
for f in keys:
if f != field and not (f == 'closing_note' and field == 'state'):
del self.fields[f]
if 'resources' in self.fields:
info = "Format: 'tag value (Optional description)'. " \
+ "Separate multiple entries with newline. When the value is a URL, use https:// where possible.<br>" \
+ "Valid tags: %s" % ', '.join([ o.slug for o in ExtResourceName.objects.all().order_by('slug') ])
self.fields['resources'].help_text = mark_safe('<div>'+info+'</div>')
def clean_acronym(self):
# Changing the acronym of an already existing group will cause 404s all
@ -184,6 +193,30 @@ class GroupForm(forms.Form):
def clean_urls(self):
return [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
def clean_resources(self):
lines = [x.strip() for x in self.cleaned_data["resources"].splitlines() if x.strip()]
errors = []
for l in lines:
parts = l.split()
if len(parts) == 1:
errors.append("Too few fields: Expected at least tag and value: '%s'" % l)
elif len(parts) >= 2:
name_slug = parts[0]
try:
name = ExtResourceName.objects.get(slug=name_slug)
except ObjectDoesNotExist:
errors.append("Bad tag in '%s': Expected one of %s" % (l, ', '.join([ o.slug for o in ExtResourceName.objects.all() ])))
continue
value = parts[1]
try:
validate_external_resource_value(name, value)
except ValidationError as e:
e.message += " : " + value
errors.append(e)
if errors:
raise ValidationError(errors)
return lines
def clean_delegates(self):
if len(self.cleaned_data["delegates"]) > MAX_GROUP_DELEGATES:
raise forms.ValidationError("At most %s delegates can be appointed at the same time, please remove %s delegates." % (

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-04-15 10:20
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('name', '0012_extres'),
('group', '0027_programs_have_parents'),
]
operations = [
migrations.CreateModel(
name='GroupExtResource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('display_name', models.CharField(blank=True, default='', max_length=255)),
('value', models.CharField(max_length=2083)),
('group', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='group.Group')),
('name', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ExtResourceName')),
],
),
]

View file

@ -0,0 +1,124 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-03-19 13:06
from __future__ import unicode_literals
import re
import debug
from collections import OrderedDict, Counter
from django.db import migrations
from django.core.exceptions import ValidationError
from ietf.utils.validators import validate_external_resource_value
name_map = {
"Issue.*": "tracker",
".*FAQ.*": "faq",
".*Area Web Page": "webpage",
".*Wiki": "wiki",
"Home Page": "webpage",
"Slack.*": "slack",
"Additional .* Web Page": "webpage",
"Additional .* Page": "webpage",
"Yang catalog entry.*": "yc_entry",
"Yang impact analysis.*": "yc_impact",
"GitHub": "github_repo",
"Github page": "github_repo",
"GitHub repo.*": "github_repo",
"Github repository.*": "github_repo",
"GitHub notifications": "github_notify",
"GitHub org.*": "github_org",
"GitHub User.*": "github_username",
"GitLab User": "gitlab_username",
"GitLab User Name": "gitlab_username",
}
# TODO: Consider dropping known bad links at this point
# " *https?://www.ietf.org/html.charters/*": None, # all these links are dead
# " *http://www.bell-labs.com/mailing-lists/pint": None, # dead link
# "http://www.ietf.org/wg/videos/mile-overview.html": None, # dead link
# " http://domen.uninett.no/~hta/ietf/notary-status.h": None, # dead link
# " http://www.ERC.MsState.Edu/packetway": None, # dead link
# "mailarchive\\.ietf\\.org" : None,
# "bell-labs\\.com": None,
# "html\\.charters": None,
# "datatracker\\.ietf\\.org": None,
# etc.
url_map = OrderedDict({
"https?://github\\.com": "github_repo",
"https?://trac\\.ietf\\.org/.*/wiki": "wiki",
"ietf\\.org.*/trac/wiki": "wiki",
"trac.*wiki": "wiki",
"www\\.ietf\\.org/mailman" : "mailing_list",
"www\\.ietf\\.org/mail-archive" : "mailing_list_archive",
"ietf\\.org/logs": "jabber_log",
"ietf\\.org/jabber/logs": "jabber_log",
"xmpp:.*?join": "jabber_room",
"https?://.*": "webpage"
})
def forward(apps, schema_editor):
GroupExtResource = apps.get_model('group', 'GroupExtResource')
ExtResourceName = apps.get_model('name', 'ExtResourceName')
GroupUrl = apps.get_model('group', 'GroupUrl')
stats = Counter()
for group_url in GroupUrl.objects.all():
match_found = False
for regext,slug in name_map.items():
if re.match(regext, group_url.name):
match_found = True
stats['mapped'] += 1
name = ExtResourceName.objects.get(slug=slug)
GroupExtResource.objects.create(group=group_url.group, name_id=slug, value=group_url.url, display_name=group_url.name) # TODO: validate this value against name.type
break
if not match_found:
for regext, slug in url_map.items():
group_url.url = group_url.url.strip()
if re.search(regext, group_url.url):
match_found = True
if slug:
stats['mapped'] +=1
name = ExtResourceName.objects.get(slug=slug)
# Munge the URL if it's the first github repo match
# Remove "/tree/master" substring if it exists
# Remove trailing "/issues" substring if it exists
# Remove "/blob/master/.*" pattern if present
if regext == "https?://github\\.com":
group_url.url = group_url.url.replace("/tree/master","")
group_url.url = re.sub('/issues$', '', group_url.url)
group_url.url = re.sub('/blob/master.*$', '', group_url.url)
try:
validate_external_resource_value(name, group_url.url)
GroupExtResource.objects.create(group=group_url.group, name=name, value=group_url.url, display_name=group_url.name) # TODO: validate this value against name.type
except ValidationError as e: # pyflakes:ignore
debug.show('("Failed validation:", group_url.url, e)')
stats['failed_validation'] +=1
else:
stats['ignored'] +=1
break
if not match_found:
debug.show('("Not Mapped:",group_url.group.acronym, group_url.name, group_url.url)')
stats['not_mapped'] += 1
print(stats)
def reverse(apps, schema_editor):
GroupExtResource = apps.get_model('group', 'GroupExtResource')
GroupExtResource.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('group', '0028_extres'),
('name', '0013_populate_extres'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -21,7 +21,7 @@ from simple_history.models import HistoricalRecords
import debug # pyflakes:ignore
from ietf.group.colors import fg_group_colors, bg_group_colors
from ietf.name.models import GroupStateName, GroupTypeName, DocTagName, GroupMilestoneStateName, RoleName, AgendaTypeName
from ietf.name.models import GroupStateName, GroupTypeName, DocTagName, GroupMilestoneStateName, RoleName, AgendaTypeName, ExtResourceName
from ietf.person.models import Email, Person
from ietf.utils.mail import formataddr, send_mail_text
from ietf.utils import log
@ -40,6 +40,7 @@ class GroupInfo(models.Model):
list_archive = models.CharField(max_length=255, blank=True)
comments = models.TextField(blank=True)
unused_states = models.ManyToManyField('doc.State', help_text="Document states that have been disabled for the group.", blank=True)
unused_tags = models.ManyToManyField(DocTagName, help_text="Document tags that have been disabled for the group.", blank=True)
@ -256,6 +257,12 @@ class GroupURL(models.Model):
def __str__(self):
return u"%s (%s)" % (self.url, self.name)
class GroupExtResource(models.Model):
group = ForeignKey(Group) # Should this really be to GroupInfo?
name = models.ForeignKey(ExtResourceName, on_delete=models.CASCADE)
display_name = models.CharField(max_length=255, default='', blank=True)
value = models.CharField(max_length=2083) # 2083 is the maximum legal URL length
class GroupMilestoneInfo(models.Model):
group = ForeignKey(Group)
# a group has two sets of milestones, current milestones

View file

@ -13,7 +13,7 @@ from ietf import api
from ietf.group.models import (Group, GroupStateTransitions, GroupMilestone, GroupHistory, # type: ignore
GroupURL, Role, GroupEvent, RoleHistory, GroupMilestoneHistory, MilestoneGroupEvent,
ChangeStateGroupEvent, GroupFeatures, HistoricalGroupFeatures)
ChangeStateGroupEvent, GroupFeatures, HistoricalGroupFeatures, GroupExtResource)
from ietf.person.resources import PersonResource
@ -348,3 +348,23 @@ class HistoricalGroupFeaturesResource(ModelResource):
"history_user": ALL_WITH_RELATIONS,
}
api.group.register(HistoricalGroupFeaturesResource())
from ietf.name.resources import ExtResourceNameResource
class GroupExtResourceResource(ModelResource):
group = ToOneField(GroupResource, 'group')
name = ToOneField(ExtResourceNameResource, 'name')
class Meta:
queryset = GroupExtResource.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'groupextresource'
ordering = ['id', ]
filtering = {
"id": ALL,
"display_name": ALL,
"value": ALL,
"group": ALL_WITH_RELATIONS,
"name": ALL_WITH_RELATIONS,
}
api.group.register(GroupExtResourceResource())

View file

@ -631,6 +631,46 @@ class GroupEditTests(TestCase):
self.assertTrue(prefix+'@' in outbox[0]['To'])
self.assertTrue(get_payload_text(outbox[0]).startswith('Sec Retary'))
def test_edit_extresources(self):
group = GroupFactory(acronym='mars',parent=GroupFactory(type_id='area'))
CharterFactory(group=group)
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit", field="resources"))
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('form textarea[id=id_resources]')),1)
badlines = (
'github_repo https://github3.com/some/repo',
'github_notify badaddr',
'website /not/a/good/url'
'notavalidtag blahblahblah'
)
for line in badlines:
r = self.client.post(url, dict(resources=line, submit="1"))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('.alert-danger'))
goodlines = """
github_repo https://github.com/some/repo Some display text
github_notify notify@example.com
github_username githubuser
website http://example.com/http/is/fine
"""
r = self.client.post(url, dict(resources=goodlines, submit="1"))
self.assertEqual(r.status_code,302)
group = Group.objects.get(acronym=group.acronym)
self.assertEqual(group.latest_event(GroupEvent,type="info_changed").desc[:20], 'Resources changed to')
self.assertIn('github_username githubuser', group.latest_event(GroupEvent,type="info_changed").desc)
self.assertEqual(group.groupextresource_set.count(), 4)
self.assertEqual(group.groupextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
def test_edit_field(self):
group = GroupFactory(acronym="mars")

View file

@ -877,6 +877,17 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
res.append(u.url)
return fs.join(res)
def format_resources(resources, fs="\n"):
res = []
for r in resources:
if r.display_name:
res.append("%s %s (%s)" % (r.name.slug, r.value, r.display_name.strip('()')))
else:
res.append("%s %s" % (r.name.slug, r.value))
# 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.
# Might be better to shift to a formset instead of parsing these lines.
return fs.join(res)
def diff(attr, name):
if field and attr != field:
return
@ -930,11 +941,6 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
else:
save_group_in_history(group)
## XXX Remove after testing
# if action == "charter" and not group.charter: # make sure we have a charter
# group.charter = get_or_create_initial_charter(group, group_type)
changes = []
# update the attributes, keeping track of what we're doing
@ -1020,6 +1026,19 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
url = GroupURL(url=m.group('url'), name='', group=group)
url.save()
if 'resources' in clean:
old_resources = sorted(format_resources(group.groupextresource_set.all()).splitlines())
new_resources = sorted(clean['resources'])
if old_resources != new_resources:
group.groupextresource_set.all().delete()
for u in new_resources:
parts = u.split(None, 2)
name = parts[0]
value = parts[1]
display_name = ' '.join(parts[2:]).strip('()')
group.groupextresource_set.create(value=value, name_id=name, display_name=display_name)
changes.append(('resources', new_resources, desc('Resources', ", ".join(new_resources), ", ".join(old_resources))))
group.time = datetime.datetime.now()
if changes and not new_group:
@ -1072,6 +1091,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
list_subscribe=group.list_subscribe if group.list_subscribe else None,
list_archive=group.list_archive if group.list_archive else None,
urls=format_urls(group.groupurl_set.all()),
resources=format_resources(group.groupextresource_set.all()),
closing_note = closing_note,
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,51 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-03-19 13:56
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('name', '0011_constraintname_editor_label'),
]
operations = [
migrations.CreateModel(
name='ExtResourceName',
fields=[
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order', 'name'],
'abstract': False,
},
),
migrations.CreateModel(
name='ExtResourceTypeName',
fields=[
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order', 'name'],
'abstract': False,
},
),
migrations.AddField(
model_name='extresourcename',
name='type',
field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ExtResourceTypeName'),
),
]

View file

@ -0,0 +1,65 @@
# Copyright The IETF Trust 2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-03-19 11:42
from __future__ import unicode_literals
from collections import namedtuple
from django.db import migrations
def forward(apps, schema_editor):
ExtResourceName = apps.get_model('name','ExtResourceName')
ExtResourceTypeName = apps.get_model('name','ExtResourceTypeName')
ExtResourceTypeName.objects.create(slug='email', name="Email address", desc="Email address", used=True, order=0)
ExtResourceTypeName.objects.create(slug='url', name="URL", desc="URL", used=True, order=0)
ExtResourceTypeName.objects.create(slug='string', name="string", desc="string", used=True, order=0)
# TODO: It might be better to reuse DocumentUrl.tag values for these slugs
resourcename = namedtuple('resourcename', ['slug', 'name', 'type'])
resourcenames= [
resourcename("webpage", "Additional Web Page", "url"),
resourcename("faq", "Frequently Asked Questions", "url"),
resourcename("github_username","GitHub Username", "string"),
resourcename("github_org","GitHub Organization", "url"),
resourcename("github_repo","GitHub Repository", "url"),
resourcename("gitlab_username","GitLab Username", "string"),
resourcename("tracker","Issuer Tracker", "url"),
resourcename("github_notify","GitHub Notifications Email", "email"),
resourcename("slack","Slack Channel", "url"),
resourcename("website","Website", "url"),
resourcename("wiki","Wiki", "url"),
resourcename("yc_entry","Yang Catalog Entry", "url"),
resourcename("yc_impact","Yang Impact Analysis", "url"),
resourcename("jabber_room","Jabber Room", "url"),
resourcename("jabber_log","Jabber Log", "url"),
resourcename("mailing_list","Mailing List", "url"),
resourcename("mailing_list_archive","Mailing List Archive","url"),
]
for name in resourcenames:
ExtResourceName.objects.create(slug=name.slug, name=name.name, desc=name.name, used=True, order=0, type_id=name.type)
def reverse(apps, schema_editor):
ExtResourceName = apps.get_model('name','ExtResourceName')
ExtResourceTypeName = apps.get_model('name','ExtResourceTypeName')
ExtResourceName.objects.all().delete()
ExtResourceTypeName.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('name', '0012_extres'),
('group', '0028_extres'),
('doc', '0032_extres'),
('person', '0011_extres'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -125,4 +125,8 @@ class ImportantDateName(NameModel):
default_offset_days = models.SmallIntegerField()
class DocUrlTagName(NameModel):
"Repository, Wiki, Issue Tracker, ..."
class ExtResourceTypeName(NameModel):
"""Url, Email, String"""
class ExtResourceName(NameModel):
"""GitHub Repository URL, GitHub Username, ..."""
type = ForeignKey(ExtResourceTypeName)

View file

@ -17,7 +17,7 @@ from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintNam
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName)
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName)
class TimeSlotTypeNameResource(ModelResource):
class Meta:
@ -615,3 +615,38 @@ class TimerangeNameResource(ModelResource):
"order": ALL,
}
api.name.register(TimerangeNameResource())
class ExtResourceTypeNameResource(ModelResource):
class Meta:
queryset = ExtResourceTypeName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'extresourcetypename'
ordering = ['slug', ]
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(ExtResourceTypeNameResource())
class ExtResourceNameResource(ModelResource):
type = ToOneField(ExtResourceTypeNameResource, 'type')
class Meta:
queryset = ExtResourceName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'extresourcename'
ordering = ['slug', ]
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
"type": ALL_WITH_RELATIONS,
}
api.name.register(ExtResourceNameResource())

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-04-15 10:20
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import ietf.utils.models
class Migration(migrations.Migration):
dependencies = [
('name', '0012_extres'),
('person', '0010_auto_20200415_1133'),
]
operations = [
migrations.CreateModel(
name='PersonExtResource',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('display_name', models.CharField(blank=True, default='', max_length=255)),
('value', models.CharField(max_length=2083)),
('name', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ExtResourceName')),
('person', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')),
],
),
]

View file

@ -24,6 +24,7 @@ from simple_history.models import HistoricalRecords
import debug # pyflakes:ignore
from ietf.name.models import ExtResourceName
from ietf.person.name import name_parts, initials, plain_name
from ietf.utils.mail import send_mail_preformatted
from ietf.utils.storage import NoLocationMigrationFileSystemStorage
@ -238,6 +239,12 @@ class Person(models.Model):
return [ (v, n) for (v, n, r) in PERSON_API_KEY_VALUES if r==None or has_role(self.user, r) ]
class PersonExtResource(models.Model):
person = ForeignKey(Person)
name = models.ForeignKey(ExtResourceName, on_delete=models.CASCADE)
display_name = models.CharField(max_length=255, default='', blank=True)
value = models.CharField(max_length=2083) # 2083 is the maximum legal URL length
class Alias(models.Model):
"""This is used for alternative forms of a name. This is the
primary lookup point for names, and should always contain the

View file

@ -10,7 +10,7 @@ from tastypie.cache import SimpleCache
from ietf import api
from ietf.person.models import (Person, Email, Alias, PersonalApiKey, PersonEvent, PersonApiKeyEvent, HistoricalPerson, HistoricalEmail) # type: ignore
from ietf.person.models import (Person, Email, Alias, PersonalApiKey, PersonEvent, PersonApiKeyEvent, HistoricalPerson, HistoricalEmail, PersonExtResource) # type: ignore
from ietf.utils.resources import UserResource
@ -182,3 +182,23 @@ class HistoricalEmailResource(ModelResource):
"history_user": ALL_WITH_RELATIONS,
}
api.person.register(HistoricalEmailResource())
from ietf.name.resources import ExtResourceNameResource
class PersonExtResourceResource(ModelResource):
person = ToOneField(PersonResource, 'person')
name = ToOneField(ExtResourceNameResource, 'name')
class Meta:
queryset = PersonExtResource.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'personextresource'
ordering = ['id', ]
filtering = {
"id": ALL,
"display_name": ALL,
"value": ALL,
"person": ALL_WITH_RELATIONS,
"name": ALL_WITH_RELATIONS,
}
api.person.register(PersonExtResourceResource())

View file

@ -272,6 +272,38 @@
</tr>
{% endif %}
{% endwith %}
{% with doc.docextresource_set.all as resources %}
{% if resources or can_edit_stream_info or can_edit_individual %}
<tr>
<td></td>
<th>Additional Resources</th>
<td class="edit">
{% if can_edit_stream_info or can_edit_individual %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_draft.edit_doc_extresources' name=doc.name %}">Edit</a>
{% endif %}
</td>
<td>
{% if resources or doc.group and doc.group.list_archive %}
<table class="col-md-12 col-sm-12 col-xs-12">
<tbody>
{% for resource in resources|dictsort:"display_name" %}
{% if resource.name.type.slug == 'url' or resource.name.type.slug == 'email' %}
<tr><td> - <a href="{{ resource.value }}" title="{{resource.name.name}}">{% firstof resource.display_name resource.name.name %}</a></td></tr>
{# Maybe make how a resource displays itself a method on the class so templates aren't doing this switching #}
{% else %}
<tr><td> - <span title="{{resource.name.name}}">{% firstof resource.display_name resource.name.name %}: {{resource.value}}</span></td></tr>
{% endif %}
{% endfor %}
{% if doc.group and doc.group.list_archive %}
<tr><td> - <a href="{{doc.group.list_archive}}?q={{doc.name}}">Mailing list discussion</a><td></tr>
{% endif %}
</tbody>
</table>
{% endif %}
</td>
</tr>
{% endif %}
{% endwith %}
</tbody>

View file

@ -138,6 +138,36 @@
</tr>
{% endif %}
{% endwith %}
{% with group.groupextresource_set.all as resources %}
{% if resources or can_edit_group %}
<tr>
<td></td>
<th>Additional Resources</th>
<td class="edit">
{% if can_edit_group %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='resources' %}">Edit</a>
{% endif %}
</td>
<td>
{% if resources %}
<table class="col-md-12 col-sm-12 col-xs-12">
<tbody>
{% for resource in resources|dictsort:"display_name" %}
{% if resource.name.type.slug == 'url' or resource.name.type.slug == 'email' %}
<tr><td> - <a href="{{ resource.value }}" title="{{resource.name.name}}">{% firstof resource.display_name resource.name.name %}</a></td></tr>
{# Maybe make how a resource displays itself a method on the class so templates aren't doing this switching #}
{% else %}
<tr><td> - <span title="{{resource.name.name}}">{% firstof resource.display_name resource.name.name %}: {{resource.value}}</span></td></tr>
{% endif %}
{% endfor %}
</tbody>
</table>
{% endif %}
</td>
</tr>
{% endif %}
{% endwith %}
</tbody>
<tbody class="meta">

View file

@ -5,10 +5,12 @@
import os
import re
from pyquery import PyQuery
from urllib.parse import urlparse
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.core.validators import RegexValidator, URLValidator, EmailValidator
from django.template.defaultfilters import filesizeformat
from django.utils.deconstruct import deconstructible
@ -83,3 +85,37 @@ def validate_no_html_frame(file):
q = PyQuery(file.read())
if q("frameset") or q("frame") or q("iframe"):
raise ValidationError('Found content with html frames. Please upload a file that does not use frames')
# instantiations of sub-validiators used by the external_resource validator
validate_url = URLValidator()
validate_http_url = URLValidator(schemes=['http','https'])
validate_email = EmailValidator()
def validate_external_resource_value(name, value):
""" validate a resource value using its name's properties """
if name.type.slug == 'url':
if name.slug in ( 'github_org', 'github_repo' ):
validate_http_url(value)
if urlparse(value).netloc.lower() != 'github.com':
raise ValidationError('URL must be a github url')
elif name.slug == 'jabber_room':
pass
# TODO - build a xmpp URL validator. See XEP-0032.
# It should be easy to build one by copyhacking URLValidator,
# but reading source says it would be better to wait to do that
# until after we make the Django 2 transition
else:
validate_url(value)
elif name.type.slug == 'email':
validate_email(value)
elif name.type.slug == 'string':
pass
else:
raise ValidationError('Unknown resource type '+name.type.name)