merged forward ^/personal/rjs/explore-extref
- Legacy-Id: 17840
This commit is contained in:
commit
7e57be2bd3
30
ietf/doc/management/commands/find_github_backup_info.py
Normal file
30
ietf/doc/management/commands/find_github_backup_info.py
Normal 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))
|
28
ietf/doc/migrations/0032_extres.py
Normal file
28
ietf/doc/migrations/0032_extres.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
116
ietf/doc/migrations/0033_populate_docextresources.py
Normal file
116
ietf/doc/migrations/0033_populate_docextresources.py
Normal 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)
|
||||
]
|
|
@ -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")
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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),
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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." % (
|
||||
|
|
28
ietf/group/migrations/0028_extres.py
Normal file
28
ietf/group/migrations/0028_extres.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
124
ietf/group/migrations/0029_populate_groupextresources.py
Normal file
124
ietf/group/migrations/0029_populate_groupextresources.py
Normal 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)
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
51
ietf/name/migrations/0012_extres.py
Normal file
51
ietf/name/migrations/0012_extres.py
Normal 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'),
|
||||
),
|
||||
]
|
65
ietf/name/migrations/0013_populate_extres.py
Normal file
65
ietf/name/migrations/0013_populate_extres.py
Normal 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)
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
|
|
28
ietf/person/migrations/0011_extres.py
Normal file
28
ietf/person/migrations/0011_extres.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue