diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py
index 1124e1ec3..988b4e9f5 100644
--- a/ietf/doc/admin.py
+++ b/ietf/doc/admin.py
@@ -11,8 +11,9 @@ from .models import (StateType, State, RelatedDocument, DocumentAuthor, Document
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL,
- ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent )
+ ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource )
+from ietf.utils.validators import validate_external_resource_value
class StateTypeAdmin(admin.ModelAdmin):
list_display = ["slug", "label"]
@@ -183,3 +184,14 @@ class DocumentUrlAdmin(admin.ModelAdmin):
search_fields = ['doc__name', 'url', ]
raw_id_fields = ['doc', ]
admin.site.register(DocumentURL, DocumentUrlAdmin)
+
+class DocExtResourceAdminForm(forms.ModelForm):
+ def clean(self):
+ validate_external_resource_value(self.cleaned_data['name'],self.cleaned_data['value'])
+
+class DocExtResourceAdmin(admin.ModelAdmin):
+ form = DocExtResourceAdminForm
+ list_display = ['id', 'doc', 'name', 'display_name', 'value',]
+ search_fields = ['doc__name', 'value', 'display_name', 'name__slug',]
+ raw_id_fields = ['doc', ]
+admin.site.register(DocExtResource, DocExtResourceAdmin)
diff --git a/ietf/doc/management/commands/find_github_backup_info.py b/ietf/doc/management/commands/find_github_backup_info.py
new file mode 100644
index 000000000..19d95e75a
--- /dev/null
+++ b/ietf/doc/management/commands/find_github_backup_info.py
@@ -0,0 +1,45 @@
+# Copyright The IETF Trust 2020, All Rights Reserved
+
+from django.core.management.base import BaseCommand
+from django.db.models import F
+
+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 DocExtResource.objects.filter(name__slug='github_repo'):
+ if not repo.value.endswith('/'):
+ repo.value += '/'
+ if repo not in info_dict:
+ info_dict[repo.value] = []
+ for username in DocExtResource.objects.filter(name__slug='github_username', doc=F('doc')):
+ info_dict[repo.value].push(username.value)
+
+ for repo in GroupExtResource.objects.filter(name__slug='github_repo'):
+ if not repo.value.endswith('/'):
+ repo.value += '/'
+ if repo not in info_dict:
+ info_dict[repo.value] = []
+ for username in GroupExtResource.objects.filter(name__slug='github_username', group=F('group')):
+ info_dict[repo.value].push(username.value)
+
+ for repo in PersonExtResource.objects.filter(name__slug='github_repo'):
+ if not repo.value.endswith('/'):
+ repo.value += '/'
+ if repo not in info_dict:
+ info_dict[repo.value] = []
+ for username in PersonExtResource.objects.filter(name__slug='github_username', person=F('person')):
+ info_dict[repo.value].push(username.value)
+
+ #print (json.dumps(info_dict))
+ # For now, all we need are the repo names
+ for name in info_dict.keys():
+ print(name)
diff --git a/ietf/doc/migrations/0034_extres.py b/ietf/doc/migrations/0034_extres.py
new file mode 100644
index 000000000..2157d0e86
--- /dev/null
+++ b/ietf/doc/migrations/0034_extres.py
@@ -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', '0014_extres'),
+ ('doc', '0033_populate_auth48_urls'),
+ ]
+
+ 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')),
+ ],
+ ),
+ ]
diff --git a/ietf/doc/migrations/0035_populate_docextresources.py b/ietf/doc/migrations/0035_populate_docextresources.py
new file mode 100644
index 000000000..04b396a96
--- /dev/null
+++ b/ietf/doc/migrations/0035_populate_docextresources.py
@@ -0,0 +1,125 @@
+# 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 # pyflakes:ignore
+
+from collections import OrderedDict, Counter
+from io import StringIO
+
+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 org.*": "github_org",
+ "GitHub User.*": "github_username",
+ "GitLab User": "gitlab_username",
+ "GitLab User Name": "gitlab_username",
+}
+
+url_map = OrderedDict({
+ "https?://github\\.com": "github_repo",
+ "https://git.sr.ht/": "repo",
+ "https://todo.sr.ht/": "tracker",
+ "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()
+ stats_file = StringIO()
+
+ for doc_url in DocumentUrl.objects.all():
+ doc_url.url = doc_url.url.strip()
+ match_found = False
+ for regext,slug in name_map.items():
+ if re.fullmatch(regext, doc_url.desc):
+ match_found = True
+ stats['mapped'] += 1
+ name = ExtResourceName.objects.get(slug=slug)
+ try:
+ validate_external_resource_value(name, doc_url.url)
+ DocExtResource.objects.create(doc=doc_url.doc, name_id=slug, value=doc_url.url, display_name=doc_url.desc)
+ except ValidationError as e: # pyflakes:ignore
+ print("Failed validation:", doc_url.url, e, file=stats_file)
+ stats['failed_validation'] +=1
+ break
+ if not match_found:
+ for regext, slug in url_map.items():
+ 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)
+ except ValidationError as e: # pyflakes:ignore
+ print("Failed validation:", doc_url.url, e, file=stats_file)
+ stats['failed_validation'] +=1
+ else:
+ stats['ignored'] +=1
+ break
+ if not match_found:
+ print("Not Mapped:", doc_url.desc, doc_url.tag.slug, doc_url.doc.name, doc_url.url, file=stats_file)
+ stats['not_mapped'] += 1
+ print('')
+ print(stats_file.getvalue())
+ print (stats)
+
+def reverse(apps, schema_editor):
+ DocExtResource = apps.get_model('doc', 'DocExtResource')
+ DocExtResource.objects.all().delete()
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('doc', '0034_extres'),
+ ('name', '0015_populate_extres'),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse)
+ ]
diff --git a/ietf/doc/models.py b/ietf/doc/models.py
index 617c6a6d5..3c88f2104 100644
--- a/ietf/doc/models.py
+++ b/ietf/doc/models.py
@@ -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
@@ -862,6 +862,15 @@ 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
+ def __str__(self):
+ priority = self.display_name or self.name.name
+ return u"%s (%s) %s" % (priority, self.name.slug, self.value)
+
class RelatedDocHistory(models.Model):
source = ForeignKey('DocHistory')
target = ForeignKey('DocAlias', related_name="reversely_related_document_history_set")
diff --git a/ietf/doc/resources.py b/ietf/doc/resources.py
index 623634103..946667b97 100644
--- a/ietf/doc/resources.py
+++ b/ietf/doc/resources.py
@@ -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())
diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py
index 3752cfd62..0b2d8327e 100644
--- a/ietf/doc/tests_draft.py
+++ b/ietf/doc/tests_draft.py
@@ -1104,24 +1104,44 @@ class IndividualInfoFormsTests(TestCase):
q = PyQuery(r.content)
self.assertTrue(q('textarea')[0].text.strip().startswith("As required by RFC 4858"))
- def test_doc_change_document_urls(self):
- url = urlreverse('ietf.doc.views_draft.edit_document_urls', kwargs=dict(name=self.docname))
-
- # get
+ 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_urls]')),1)
+ self.assertEqual(len(q('form textarea[id=id_resources]')),1)
- # direct edit
- r = self.client.post(url, dict(urls='wiki https://wiki.org/ Wiki\nrepository https://repository.org/ Repo\n', submit="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_username githubuser
+ webpage 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.assertTrue(doc.latest_event(DocEvent,type="changed_document").desc.startswith('Changed document URLs'))
- 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() ])
+ 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(), 3)
+ self.assertEqual(doc.docextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
+ self.assertIn(doc.docextresource_set.first().name.slug,str(doc.docextresource_set.first()))
+
class SubmitToIesgTests(TestCase):
diff --git a/ietf/doc/urls.py b/ietf/doc/urls.py
index 1bef6f9ed..b9aa9b2f8 100644
--- a/ietf/doc/urls.py
+++ b/ietf/doc/urls.py
@@ -128,7 +128,7 @@ urlpatterns = [
url(r'^%(name)s/edit/approveballot/$' % settings.URL_REGEXPS, views_ballot.approve_ballot),
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),
diff --git a/ietf/doc/views_draft.py b/ietf/doc/views_draft.py
index 9b025956e..f780bfc5e 100644
--- a/ietf/doc/views_draft.py
+++ b/ietf/doc/views_draft.py
@@ -14,7 +14,6 @@ from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError, ObjectDoesNotExist
-from django.core.validators import URLValidator
from django.db.models import Q
from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404
from django.shortcuts import render, get_object_or_404, redirect
@@ -43,11 +42,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, 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
@@ -1177,42 +1177,46 @@ def edit_consensus(request, name):
},
)
-def edit_document_urls(request, name):
- class DocumentUrlForm(forms.Form):
- urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", required=False,
- help_text=("Format: 'tag https://site/path (Optional description)'."
- " Separate multiple entries with newline. Prefer HTTPS URLs where possible.") )
- def clean_urls(self):
- lines = [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
- url_validator = URLValidator()
+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 url and tag: '%s'" % l)
+ errors.append("Too few fields: Expected at least tag and value: '%s'" % l)
elif len(parts) >= 2:
- tag = parts[0]
- url = parts[1]
+ name_slug = parts[0]
try:
- url_validator(url)
- except ValidationError as e:
- errors.append(e)
- try:
- DocUrlTagName.objects.get(slug=tag)
+ 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 DocUrlTagName.objects.all() ])))
+ 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_urls(urls, fs="\n"):
+ def format_resources(resources, fs="\n"):
res = []
- for u in urls:
- if u.desc:
- res.append("%s %s (%s)" % (u.tag.slug, u.url, u.desc.strip('()')))
+ 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" % (u.tag.slug, u.url))
+ 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)
@@ -1222,37 +1226,39 @@ def edit_document_urls(request, name):
or is_individual_draft_author(request.user, doc)):
return HttpResponseForbidden("You do not have the necessary permissions to view this page")
- old_urls = format_urls(doc.documenturl_set.all())
+ old_resources = format_resources(doc.docextresource_set.all())
if request.method == 'POST':
- form = DocumentUrlForm(request.POST)
+ form = DocExtResourceForm(request.POST)
if form.is_valid():
- old_urls = sorted(old_urls.splitlines())
- new_urls = sorted(form.cleaned_data['urls'])
- if old_urls != new_urls:
- doc.documenturl_set.all().delete()
- for u in new_urls:
+ 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)
- tag = parts[0]
- url = parts[1]
- desc = ' '.join(parts[2:]).strip('()')
- doc.documenturl_set.create(url=url, tag_id=tag, desc=desc)
- new_urls = format_urls(doc.documenturl_set.all())
- e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='changed_document')
- e.desc = "Changed document URLs from:\n\n%s\n\nto:\n\n%s" % (old_urls, new_urls)
- e.save()
- doc.save_with_history([e])
- messages.success(request,"Document URLs updated.")
+ 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 URLs.")
+ messages.info(request,"No change in Document resources.")
return redirect('ietf.doc.views_doc.document_main', name=doc.name)
else:
- form = DocumentUrlForm(initial={'urls': old_urls, })
+ form = DocExtResourceForm(initial={'resources': old_resources, })
- info = "Valid tags:
%s" % ', '.join([ o.slug for o in DocUrlTagName.objects.all() ])
- title = "Additional document URLs"
+ info = "Valid tags:
%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."""
diff --git a/ietf/group/admin.py b/ietf/group/admin.py
index a37741d4f..fda95d968 100644
--- a/ietf/group/admin.py
+++ b/ietf/group/admin.py
@@ -18,7 +18,9 @@ from django.utils.translation import ugettext as _
from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
- MilestoneGroupEvent, )
+ MilestoneGroupEvent, GroupExtResource, )
+
+from ietf.utils.validators import validate_external_resource_value
class RoleInline(admin.TabularInline):
model = Role
@@ -203,3 +205,14 @@ class MilestoneGroupEventAdmin(admin.ModelAdmin):
list_filter = ['time']
raw_id_fields = ['group', 'by', 'milestone']
admin.site.register(MilestoneGroupEvent, MilestoneGroupEventAdmin)
+
+class GroupExtResourceAdminForm(forms.ModelForm):
+ def clean(self):
+ validate_external_resource_value(self.cleaned_data['name'],self.cleaned_data['value'])
+
+class GroupExtResourceAdmin(admin.ModelAdmin):
+ form = GroupExtResourceAdminForm
+ list_display = ['id', 'group', 'name', 'display_name', 'value',]
+ search_fields = ['group__acronym', 'value', 'display_name', 'name__slug',]
+ raw_id_fields = ['group', ]
+admin.site.register(GroupExtResource, GroupExtResourceAdmin)
diff --git a/ietf/group/forms.py b/ietf/group/forms.py
index 1d16f601c..441631724 100644
--- a/ietf/group/forms.py
+++ b/ietf/group/forms.py
@@ -12,10 +12,11 @@ import debug # pyflakes:ignore
from django import forms
from django.utils.html import mark_safe # type:ignore
from django.db.models import F
+from django.core.exceptions import ValidationError, ObjectDoesNotExist
# IETF imports
from ietf.group.models import Group, GroupHistory, GroupStateName, GroupFeatures
-from ietf.name.models import ReviewTypeName, RoleName
+from ietf.name.models import ReviewTypeName, RoleName, ExtResourceName
from ietf.person.fields import SearchableEmailsField, PersonEmailChoiceField
from ietf.person.models import Person, Email
from ietf.review.models import ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
@@ -24,6 +25,7 @@ from ietf.review.utils import close_review_request_states
from ietf.utils.textupload import get_cleaned_text_file_content
#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 --------------------------------------------------------
@@ -65,6 +67,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="Format: tag value (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):
@@ -129,6 +132,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.
" \
+ + "Valid tags: %s" % ', '.join([ o.slug for o in ExtResourceName.objects.all().order_by('slug') ])
+ self.fields['resources'].help_text = mark_safe('
'+info+'
')
+
def clean_acronym(self):
# Changing the acronym of an already existing group will cause 404s all
@@ -188,6 +197,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." % (
diff --git a/ietf/group/migrations/0033_extres.py b/ietf/group/migrations/0033_extres.py
new file mode 100644
index 000000000..2e3d037a2
--- /dev/null
+++ b/ietf/group/migrations/0033_extres.py
@@ -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', '0014_extres'),
+ ('group', '0032_add_meeting_seen_as_area'),
+ ]
+
+ 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')),
+ ],
+ ),
+ ]
diff --git a/ietf/group/migrations/0034_populate_groupextresources.py b/ietf/group/migrations/0034_populate_groupextresources.py
new file mode 100644
index 000000000..a8c0e1b98
--- /dev/null
+++ b/ietf/group/migrations/0034_populate_groupextresources.py
@@ -0,0 +1,120 @@
+# 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 # pyflakes:ignore
+
+from collections import OrderedDict, Counter
+from io import StringIO
+
+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 org.*": "github_org",
+ "GitHub User.*": "github_username",
+ "GitLab User": "gitlab_username",
+ "GitLab User Name": "gitlab_username",
+}
+
+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()
+ stats_file = StringIO()
+
+ for group_url in GroupUrl.objects.all():
+ group_url.url = group_url.url.strip()
+ match_found = False
+ for regext,slug in name_map.items():
+ if re.fullmatch(regext, group_url.name):
+ match_found = True
+ stats['mapped'] += 1
+ name = ExtResourceName.objects.get(slug=slug)
+ try:
+ validate_external_resource_value(name, group_url.url)
+ GroupExtResource.objects.create(group=group_url.group, name_id=slug, value=group_url.url, display_name=group_url.name)
+ except ValidationError as e: # pyflakes:ignore
+ print("Failed validation:", group_url.url, e, file=stats_file)
+ stats['failed_validation'] +=1
+ break
+ if not match_found:
+ for regext, slug in url_map.items():
+ 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)
+ except ValidationError as e: # pyflakes:ignore
+ print("Failed validation:", group_url.url, e, file=stats_file)
+ stats['failed_validation'] +=1
+ else:
+ stats['ignored'] +=1
+ break
+ if not match_found:
+ print("Not Mapped:",group_url.group.acronym, group_url.name, group_url.url, file=stats_file)
+ stats['not_mapped'] += 1
+ print('')
+ print(stats_file.getvalue())
+ print(stats)
+
+def reverse(apps, schema_editor):
+ GroupExtResource = apps.get_model('group', 'GroupExtResource')
+ GroupExtResource.objects.all().delete()
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('group', '0033_extres'),
+ ('name', '0015_populate_extres'),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse)
+ ]
diff --git a/ietf/group/models.py b/ietf/group/models.py
index 093f18be1..7293c3e1a 100644
--- a/ietf/group/models.py
+++ b/ietf/group/models.py
@@ -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
@@ -41,6 +41,7 @@ class GroupInfo(models.Model):
comments = models.TextField(blank=True)
meeting_seen_as_area = models.BooleanField(default=False, help_text='For meeting scheduling, should be considered an area meeting, even if the type is WG')
+
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)
@@ -260,6 +261,15 @@ 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
+ def __str__(self):
+ priority = self.display_name or self.name.name
+ return u"%s (%s) %s" % (priority, self.name.slug, self.value)
+
class GroupMilestoneInfo(models.Model):
group = ForeignKey(Group)
# a group has two sets of milestones, current milestones
diff --git a/ietf/group/resources.py b/ietf/group/resources.py
index 8c119f7b8..509c6758e 100644
--- a/ietf/group/resources.py
+++ b/ietf/group/resources.py
@@ -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())
diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py
index c9281d1e6..304fe9eec 100644
--- a/ietf/group/tests_info.py
+++ b/ietf/group/tests_info.py
@@ -31,7 +31,7 @@ from ietf.group.factories import (GroupFactory, RoleFactory, GroupEventFactory,
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions, Role
from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group
from ietf.meeting.factories import SessionFactory
-from ietf.name.models import DocTagName, GroupStateName, GroupTypeName
+from ietf.name.models import DocTagName, GroupStateName, GroupTypeName, ExtResourceName
from ietf.person.models import Person, Email
from ietf.person.factories import PersonFactory
from ietf.review.factories import ReviewRequestFactory, ReviewAssignmentFactory
@@ -610,7 +610,6 @@ class GroupEditTests(TestCase):
list_email="mars@mail",
list_subscribe="subscribe.mars",
list_archive="archive.mars",
- urls="http://mars.mars (MARS site)"
))
self.assertEqual(r.status_code, 302)
@@ -624,8 +623,7 @@ class GroupEditTests(TestCase):
self.assertEqual(group.list_email, "mars@mail")
self.assertEqual(group.list_subscribe, "subscribe.mars")
self.assertEqual(group.list_archive, "archive.mars")
- self.assertEqual(group.groupurl_set.all()[0].url, "http://mars.mars")
- self.assertEqual(group.groupurl_set.all()[0].name, "MARS site")
+
self.assertTrue(os.path.exists(os.path.join(self.charter_dir, "%s-%s.txt" % (group.charter.canonical_name(), group.charter.rev))))
self.assertEqual(len(outbox), 2)
self.assertTrue('Personnel change' in outbox[0]['Subject'])
@@ -633,6 +631,54 @@ 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)
+ ExtResourceName.objects.create(slug='keymaster', name='Keymaster', type_id='email')
+
+ 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',
+ 'github_repo',
+ )
+
+ 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_username githubuser
+ webpage http://example.com/http/is/fine
+ jabber_room xmpp:mars@jabber.example.com
+ keymaster keymaster@example.org Group Rooter
+ """
+
+ 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(), 5)
+ self.assertEqual(group.groupextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
+ self.assertIn(group.groupextresource_set.first().name.slug, str(group.groupextresource_set.first()))
+
+ # exercise format_resources
+ r = self.client.get(url)
+ self.assertIn('Group Rooter', unicontent(r))
+
def test_edit_field(self):
group = GroupFactory(acronym="mars")
diff --git a/ietf/group/views.py b/ietf/group/views.py
index d81be7bea..4f7b4d90f 100644
--- a/ietf/group/views.py
+++ b/ietf/group/views.py
@@ -73,7 +73,7 @@ from ietf.group.forms import (GroupForm, StatusUpdateForm, ConcludeGroupForm, St
ManageReviewRequestForm, EmailOpenAssignmentsForm, ReviewerSettingsForm,
AddUnavailablePeriodForm, EndUnavailablePeriodForm, ReviewSecretarySettingsForm, )
from ietf.group.mails import email_admin_re_charter, email_personnel_change, email_comment
-from ietf.group.models import ( Group, Role, GroupEvent, GroupStateTransitions, GroupURL,
+from ietf.group.models import ( Group, Role, GroupEvent, GroupStateTransitions,
ChangeStateGroupEvent, GroupFeatures )
from ietf.group.utils import (get_charter_text, can_manage_group_type,
milestone_reviewer_for_group_type, can_provide_status_update,
@@ -346,7 +346,7 @@ def active_wgs(request):
+ list(sorted(roles(area, "pre-ad"), key=extract_last_name)))
area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym")
- area.urls = area.groupurl_set.all().order_by("name")
+ area.urls = area.groupextresource_set.all().order_by("name")
for group in area.groups:
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
group.ad_out_of_area = group.ad_role() and group.ad_role().person not in [role.person for role in area.ads]
@@ -860,13 +860,15 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
return entry % dict(attr=attr, new=new, old=old)
- def format_urls(urls, fs="\n"):
+ def format_resources(resources, fs="\n"):
res = []
- for u in urls:
- if u.name:
- res.append("%s (%s)" % (u.url, u.name))
+ 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(u.url)
+ 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):
@@ -922,11 +924,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
@@ -996,22 +993,18 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
personnel_change_text = "%s has updated %s personnel:\n\n" % (request.user.person.plain_name(), group.acronym.upper() ) + personnel_change_text
email_personnel_change(request, group, personnel_change_text, changed_personnel)
- # update urls
- if 'urls' in clean:
- new_urls = clean['urls']
- old_urls = format_urls(group.groupurl_set.order_by('url'), ", ")
- if ", ".join(sorted(new_urls)) != old_urls:
- changes.append(('urls', new_urls, desc('Urls', ", ".join(sorted(new_urls)), old_urls)))
- group.groupurl_set.all().delete()
- # Add new ones
- for u in new_urls:
- m = re.search(r'(?P[\w\d:#@%/;$()~_?\+-=\\\.&]+)( \((?P.+)\))?', u)
- if m:
- if m.group('name'):
- url = GroupURL(url=m.group('url'), name=m.group('name'), group=group)
- else:
- 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()
@@ -1064,7 +1057,7 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
list_email=group.list_email if group.list_email else 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,
)
diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py
index 72ef6729d..45719c7b1 100644
--- a/ietf/ietfauth/tests.py
+++ b/ietf/ietfauth/tests.py
@@ -667,6 +667,46 @@ class IetfAuthTests(TestCase):
self.assertIn(" %s times" % count, body)
self.assertIn(date, body)
+ def test_edit_person_extresources(self):
+ url = urlreverse('ietf.ietfauth.views.edit_person_externalresources')
+ person = PersonFactory()
+
+ r = self.client.get(url)
+ self.assertNotEqual(r.status_code, 200)
+
+ self.client.login(username=person.user.username,password=person.user.username+'+password')
+
+ 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',
+ 'website',
+ )
+
+ 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_username githubuser
+ webpage http://example.com/http/is/fine
+ """
+
+ r = self.client.post(url, dict(resources=goodlines, submit="1"))
+ self.assertEqual(r.status_code,302)
+ self.assertEqual(person.personextresource_set.count(), 3)
+ self.assertEqual(person.personextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
+ self.assertIn(person.personextresource_set.first().name.slug, str(person.personextresource_set.first()))
+
class OpenIDConnectTests(TestCase):
def request_matcher(self, request):
@@ -800,3 +840,4 @@ class OpenIDConnectTests(TestCase):
# handler, causing later logging to become visible even if that wasn't intended.
# Fail here if that happens.
self.assertEqual(logging.root.handlers, [])
+
diff --git a/ietf/ietfauth/urls.py b/ietf/ietfauth/urls.py
index f31255542..12c749fbe 100644
--- a/ietf/ietfauth/urls.py
+++ b/ietf/ietfauth/urls.py
@@ -18,6 +18,7 @@ urlpatterns = [
url(r'^logout/$', LogoutView.as_view(), name="django.contrib.auth.views.logout"),
url(r'^password/$', views.change_password),
url(r'^profile/$', views.profile),
+ url(r'^editexternalresources/$', views.edit_person_externalresources),
url(r'^reset/$', views.password_reset),
url(r'^reset/confirm/(?P[^/]+)/$', views.confirm_password_reset),
url(r'^review/$', views.review_overview),
diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py
index 915b977c4..234a7e200 100644
--- a/ietf/ietfauth/views.py
+++ b/ietf/ietfauth/views.py
@@ -67,12 +67,15 @@ from ietf.ietfauth.forms import ( RegistrationForm, PasswordForm, ResetPasswordF
from ietf.ietfauth.htpasswd import update_htpasswd_file
from ietf.ietfauth.utils import role_required, has_role
from ietf.mailinglists.models import Subscribed, Whitelisted
+from ietf.name.models import ExtResourceName
from ietf.person.models import Person, Email, Alias, PersonalApiKey, PERSON_API_KEY_VALUES
from ietf.review.models import ReviewerSettings, ReviewWish, ReviewAssignment
from ietf.review.utils import unavailable_periods_to_list, get_default_filter_re
from ietf.doc.fields import SearchableDocumentField
from ietf.utils.decorators import person_required
from ietf.utils.mail import send_mail
+from ietf.utils.validators import validate_external_resource_value
+
def index(request):
return render(request, 'registration/index.html')
@@ -288,6 +291,79 @@ def profile(request):
'settings':settings,
})
+@login_required
+@person_required
+def edit_person_externalresources(request):
+ class PersonExtResourceForm(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)
+
+ person = request.user.person
+
+ old_resources = format_resources(person.personextresource_set.all())
+
+ if request.method == 'POST':
+ form = PersonExtResourceForm(request.POST)
+ if form.is_valid():
+ old_resources = sorted(old_resources.splitlines())
+ new_resources = sorted(form.cleaned_data['resources'])
+ if old_resources != new_resources:
+ person.personextresource_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('()')
+ person.personextresource_set.create(value=value, name_id=name, display_name=display_name)
+ new_resources = format_resources(person.personextresource_set.all())
+ messages.success(request,"Person resources updated.")
+ else:
+ messages.info(request,"No change in Person resources.")
+ return redirect('ietf.ietfauth.views.profile')
+ else:
+ form = PersonExtResourceForm(initial={'resources': old_resources, })
+
+ info = "Valid tags:
%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 person resources"
+ return render(request, 'ietfauth/edit_field.html',dict(person=person, form=form, title=title, info=info) )
+
def confirm_new_email(request, auth):
try:
username, email = django.core.signing.loads(auth, salt="add_email", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
diff --git a/ietf/name/admin.py b/ietf/name/admin.py
index 8c7660132..8462bddeb 100644
--- a/ietf/name/admin.py
+++ b/ietf/name/admin.py
@@ -10,7 +10,9 @@ from ietf.name.models import (
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
- DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName)
+ DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName,
+ ExtResourceName, ExtResourceTypeName, )
+
from ietf.stats.models import CountryAlias
@@ -46,6 +48,10 @@ class ImportantDateNameAdmin(NameAdmin):
ordering = ('-used','default_offset_days',)
admin.site.register(ImportantDateName,ImportantDateNameAdmin)
+class ExtResourceNameAdmin(NameAdmin):
+ list_display = ["slug", "name", "type", "desc", "used",]
+admin.site.register(ExtResourceName,ExtResourceNameAdmin)
+
admin.site.register(AgendaTypeName, NameAdmin)
admin.site.register(BallotPositionName, NameAdmin)
admin.site.register(ConstraintName, NameAdmin)
@@ -82,3 +88,4 @@ admin.site.register(TimeSlotTypeName, NameAdmin)
admin.site.register(TimerangeName, NameAdmin)
admin.site.register(TopicAudienceName, NameAdmin)
admin.site.register(DocUrlTagName, NameAdmin)
+admin.site.register(ExtResourceTypeName, NameAdmin)
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index edd8d0de5..3822cecc8 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -9748,6 +9748,212 @@
"model": "name.draftsubmissionstatename",
"pk": "waiting-for-draft"
},
+ {
+ "fields": {
+ "desc": "Frequently Asked Questions",
+ "name": "Frequently Asked Questions",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "faq"
+ },
+ {
+ "fields": {
+ "desc": "GitHub Organization",
+ "name": "GitHub Organization",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "github_org"
+ },
+ {
+ "fields": {
+ "desc": "GitHub Repository",
+ "name": "GitHub Repository",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "github_repo"
+ },
+ {
+ "fields": {
+ "desc": "GitHub Username",
+ "name": "GitHub Username",
+ "order": 0,
+ "type": "string",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "github_username"
+ },
+ {
+ "fields": {
+ "desc": "GitLab Username",
+ "name": "GitLab Username",
+ "order": 0,
+ "type": "string",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "gitlab_username"
+ },
+ {
+ "fields": {
+ "desc": "Jabber Log",
+ "name": "Jabber Log",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "jabber_log"
+ },
+ {
+ "fields": {
+ "desc": "Jabber Room",
+ "name": "Jabber Room",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "jabber_room"
+ },
+ {
+ "fields": {
+ "desc": "Mailing List",
+ "name": "Mailing List",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "mailing_list"
+ },
+ {
+ "fields": {
+ "desc": "Mailing List Archive",
+ "name": "Mailing List Archive",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "mailing_list_archive"
+ },
+ {
+ "fields": {
+ "desc": "Other Repository",
+ "name": "Other Repository",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "repo"
+ },
+ {
+ "fields": {
+ "desc": "Slack Channel",
+ "name": "Slack Channel",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "slack"
+ },
+ {
+ "fields": {
+ "desc": "Issuer Tracker",
+ "name": "Issuer Tracker",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "tracker"
+ },
+ {
+ "fields": {
+ "desc": "Additional Web Page",
+ "name": "Additional Web Page",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "webpage"
+ },
+ {
+ "fields": {
+ "desc": "Wiki",
+ "name": "Wiki",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "wiki"
+ },
+ {
+ "fields": {
+ "desc": "Yang Catalog Entry",
+ "name": "Yang Catalog Entry",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "yc_entry"
+ },
+ {
+ "fields": {
+ "desc": "Yang Impact Analysis",
+ "name": "Yang Impact Analysis",
+ "order": 0,
+ "type": "url",
+ "used": true
+ },
+ "model": "name.extresourcename",
+ "pk": "yc_impact"
+ },
+ {
+ "fields": {
+ "desc": "Email address",
+ "name": "Email address",
+ "order": 0,
+ "used": true
+ },
+ "model": "name.extresourcetypename",
+ "pk": "email"
+ },
+ {
+ "fields": {
+ "desc": "string",
+ "name": "string",
+ "order": 0,
+ "used": true
+ },
+ "model": "name.extresourcetypename",
+ "pk": "string"
+ },
+ {
+ "fields": {
+ "desc": "URL",
+ "name": "URL",
+ "order": 0,
+ "used": true
+ },
+ "model": "name.extresourcetypename",
+ "pk": "url"
+ },
{
"fields": {
"desc": "",
@@ -14637,7 +14843,7 @@
"fields": {
"command": "xym",
"switch": "--version",
- "time": "2020-05-29T00:13:35.959",
+ "time": "2020-07-09T00:12:56.528",
"used": true,
"version": "xym 0.4.8"
},
@@ -14648,7 +14854,7 @@
"fields": {
"command": "pyang",
"switch": "--version",
- "time": "2020-05-29T00:13:38.724",
+ "time": "2020-07-09T00:12:58.135",
"used": true,
"version": "pyang 2.2.1"
},
@@ -14659,7 +14865,7 @@
"fields": {
"command": "yanglint",
"switch": "--version",
- "time": "2020-05-29T00:13:39.026",
+ "time": "2020-07-09T00:12:58.398",
"used": true,
"version": "yanglint SO 1.6.7"
},
@@ -14670,7 +14876,7 @@
"fields": {
"command": "xml2rfc",
"switch": "--version",
- "time": "2020-05-29T00:13:40.790",
+ "time": "2020-07-09T00:13:00.193",
"used": true,
"version": "xml2rfc 2.46.0"
},
diff --git a/ietf/name/migrations/0014_extres.py b/ietf/name/migrations/0014_extres.py
new file mode 100644
index 000000000..576a98ea3
--- /dev/null
+++ b/ietf/name/migrations/0014_extres.py
@@ -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', '0013_add_auth48_docurltagname'),
+ ]
+
+ 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'),
+ ),
+ ]
diff --git a/ietf/name/migrations/0015_populate_extres.py b/ietf/name/migrations/0015_populate_extres.py
new file mode 100644
index 000000000..64a6bf08a
--- /dev/null
+++ b/ietf/name/migrations/0015_populate_extres.py
@@ -0,0 +1,63 @@
+# 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)
+
+ 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("slack","Slack Channel", "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"),
+ resourcename("repo","Other Repository", "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', '0014_extres'),
+ ('group', '0033_extres'),
+ ('doc', '0034_extres'),
+ ('person', '0015_extres'),
+ ]
+
+ operations = [
+ migrations.RunPython(forward, reverse)
+ ]
diff --git a/ietf/name/models.py b/ietf/name/models.py
index 19e53d28a..69c96c29a 100644
--- a/ietf/name/models.py
+++ b/ietf/name/models.py
@@ -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)
diff --git a/ietf/name/resources.py b/ietf/name/resources.py
index 1f0d15549..80c78842a 100644
--- a/ietf/name/resources.py
+++ b/ietf/name/resources.py
@@ -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())
diff --git a/ietf/person/admin.py b/ietf/person/admin.py
index 0c8d24f0b..0bb17b0b6 100644
--- a/ietf/person/admin.py
+++ b/ietf/person/admin.py
@@ -1,9 +1,14 @@
from django.contrib import admin
import simple_history
-from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, PersonApiKeyEvent
+from django import forms
+
+from ietf.person.models import Email, Alias, Person, PersonalApiKey, PersonEvent, PersonApiKeyEvent, PersonExtResource
from ietf.person.name import name_parts
+from ietf.utils.validators import validate_external_resource_value
+
+
class EmailAdmin(simple_history.admin.SimpleHistoryAdmin):
list_display = ["address", "person", "time", "active", "origin"]
raw_id_fields = ["person", ]
@@ -58,3 +63,14 @@ class PersonApiKeyEventAdmin(admin.ModelAdmin):
admin.site.register(PersonApiKeyEvent, PersonApiKeyEventAdmin)
+
+class PersonExtResourceAdminForm(forms.ModelForm):
+ def clean(self):
+ validate_external_resource_value(self.cleaned_data['name'],self.cleaned_data['value'])
+
+class PersonExtResourceAdmin(admin.ModelAdmin):
+ form = PersonExtResourceAdminForm
+ list_display = ['id', 'person', 'name', 'display_name', 'value',]
+ search_fields = ['person__name', 'value', 'display_name', 'name__slug',]
+ raw_id_fields = ['person', ]
+admin.site.register(PersonExtResource, PersonExtResourceAdmin)
diff --git a/ietf/person/migrations/0015_extres.py b/ietf/person/migrations/0015_extres.py
new file mode 100644
index 000000000..1b9828585
--- /dev/null
+++ b/ietf/person/migrations/0015_extres.py
@@ -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', '0014_extres'),
+ ('person', '0014_auto_20200717_0743'),
+ ]
+
+ 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')),
+ ],
+ ),
+ ]
diff --git a/ietf/person/models.py b/ietf/person/models.py
index 71d4bb34c..c7aad0ed2 100644
--- a/ietf/person/models.py
+++ b/ietf/person/models.py
@@ -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
@@ -244,6 +245,15 @@ 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
+ def __str__(self):
+ priority = self.display_name or self.name.name
+ return u"%s (%s) %s" % (priority, self.name.slug, self.value)
+
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
diff --git a/ietf/person/resources.py b/ietf/person/resources.py
index cf54dede5..7db321ac6 100644
--- a/ietf/person/resources.py
+++ b/ietf/person/resources.py
@@ -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())
diff --git a/ietf/secr/areas/forms.py b/ietf/secr/areas/forms.py
index edd399379..2a34408ad 100644
--- a/ietf/secr/areas/forms.py
+++ b/ietf/secr/areas/forms.py
@@ -1,10 +1,7 @@
from django import forms
from ietf.person.models import Person, Email
-from ietf.group.models import Group, GroupURL
-from ietf.name.models import GroupTypeName, GroupStateName
-import datetime
import re
STATE_CHOICES = (
@@ -13,132 +10,6 @@ STATE_CHOICES = (
)
-class AWPForm(forms.ModelForm):
- class Meta:
- model = GroupURL
- fields = '__all__'
-
- def __init__(self, *args, **kwargs):
- super(AWPForm, self).__init__(*args,**kwargs)
- self.fields['url'].widget.attrs['width'] = 40
- self.fields['name'].widget.attrs['width'] = 40
- self.fields['url'].required = False
- self.fields['name'].required = False
-
- # Validation: url without description and vice-versa
- def clean(self):
- super(AWPForm, self).clean()
- cleaned_data = self.cleaned_data
- url = cleaned_data.get('url')
- name = cleaned_data.get('name')
-
- if (url and not name) or (name and not url):
- raise forms.ValidationError('You must fill out URL and Name')
-
- # Always return the full collection of cleaned data.
- return cleaned_data
-
-
-class AreaForm(forms.ModelForm):
- class Meta:
- model = Group
- fields = ('acronym','name','state','comments')
-
- # use this method to set attrs which keeps other meta info from model.
- def __init__(self, *args, **kwargs):
- super(AreaForm, self).__init__(*args, **kwargs)
- self.fields['state'].queryset = GroupStateName.objects.filter(slug__in=('active','conclude'))
- self.fields['state'].empty_label = None
- self.fields['comments'].widget.attrs['rows'] = 2
-
-"""
- # Validation: status and conclude_date must agree
- def clean(self):
- super(AreaForm, self).clean()
- cleaned_data = self.cleaned_data
- concluded_date = cleaned_data.get('concluded_date')
- state = cleaned_data.get('state')
- concluded_status_object = AreaStatus.objects.get(status_id=2)
-
- if concluded_date and status != concluded_status_object:
- raise forms.ValidationError('Concluded Date set but status is %s' % (status.status_value))
-
- if status == concluded_status_object and not concluded_date:
- raise forms.ValidationError('Status is Concluded but Concluded Date not set.')
-
- # Always return the full collection of cleaned data.
- return cleaned_data
-
-"""
-class AWPAddModelForm(forms.ModelForm):
- class Meta:
- model = GroupURL
- fields = ('url', 'name')
-
-# for use with Add view, ModelForm doesn't work because the parent type hasn't been created yet
-# when initial screen is displayed
-class AWPAddForm(forms.Form):
- url = forms.CharField(
- max_length=50,
- required=False,
- widget=forms.TextInput(attrs={'size':'40'}))
- description = forms.CharField(
- max_length=50,
- required=False,
- widget=forms.TextInput(attrs={'size':'40'}))
-
- # Validation: url without description and vice-versa
- def clean(self):
- super(AWPAddForm, self).clean()
- cleaned_data = self.cleaned_data
- url = cleaned_data.get('url')
- description = cleaned_data.get('description')
-
- if (url and not description) or (description and not url):
- raise forms.ValidationError('You must fill out URL and Description')
-
- # Always return the full collection of cleaned data.
- return cleaned_data
-
-class AddAreaModelForm(forms.ModelForm):
- start_date = forms.DateField()
-
- class Meta:
- model = Group
- fields = ('acronym','name','state','start_date','comments')
-
- def __init__(self, *args, **kwargs):
- super(AddAreaModelForm, self).__init__(*args, **kwargs)
- self.fields['acronym'].required = True
- self.fields['name'].required = True
- self.fields['start_date'].required = True
- self.fields['start_date'].initial = datetime.date.today
-
- def clean_acronym(self):
- acronym = self.cleaned_data['acronym']
- if Group.objects.filter(acronym=acronym):
- raise forms.ValidationError("This acronym already exists. Enter a unique one.")
- r1 = re.compile(r'[a-zA-Z\-\. ]+$')
- if not r1.match(acronym):
- raise forms.ValidationError("Enter a valid acronym (only letters,period,hyphen allowed)")
- return acronym
-
- def clean_name(self):
- name = self.cleaned_data['name']
- if Group.objects.filter(name=name):
- raise forms.ValidationError("This name already exists. Enter a unique one.")
- r1 = re.compile(r'[a-zA-Z\-\. ]+$')
- if name and not r1.match(name):
- raise forms.ValidationError("Enter a valid name (only letters,period,hyphen allowed)")
- return name
-
- def save(self, force_insert=False, force_update=False, commit=True):
- area = super(AddAreaModelForm, self).save(commit=False)
- area.type = GroupTypeName.objects.get(slug='area')
- area.parent = Group.objects.get(acronym='iesg')
- if commit:
- area.save()
- return area
class AreaDirectorForm(forms.Form):
ad_name = forms.CharField(max_length=100,label='Name',help_text="To see a list of people type the first name, or last name, or both.")
diff --git a/ietf/secr/areas/tests.py b/ietf/secr/areas/tests.py
index 956277dfa..f7341dfa9 100644
--- a/ietf/secr/areas/tests.py
+++ b/ietf/secr/areas/tests.py
@@ -32,19 +32,3 @@ class SecrAreasTestCase(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- def test_add(self):
- "Add Test"
- url = reverse('ietf.secr.areas.views.add')
- self.client.login(username="secretary", password="secretary+password")
- data = {'acronym':'ta',
- 'name':'Test Area',
- 'state':'active',
- 'start_date':'2017-01-01',
- 'awp-TOTAL_FORMS':'2',
- 'awp-INITIAL_FORMS':'0',
- 'submit':'Save'}
- response = self.client.post(url,data)
- self.assertRedirects(response, reverse('ietf.secr.areas.views.list_areas'))
- area = Group.objects.get(acronym='ta')
- iesg = Group.objects.get(acronym='iesg')
- self.assertTrue(area.parent == iesg)
\ No newline at end of file
diff --git a/ietf/secr/areas/urls.py b/ietf/secr/areas/urls.py
index 7603403d2..cbb9a4a2f 100644
--- a/ietf/secr/areas/urls.py
+++ b/ietf/secr/areas/urls.py
@@ -4,11 +4,9 @@ from ietf.utils.urls import url
urlpatterns = [
url(r'^$', views.list_areas),
- url(r'^add/$', views.add),
url(r'^getemails', views.getemails),
url(r'^getpeople', views.getpeople),
url(r'^(?P[A-Za-z0-9.-]+)/$', views.view),
- url(r'^(?P[A-Za-z0-9.-]+)/edit/$', views.edit),
url(r'^(?P[A-Za-z0-9.-]+)/people/$', views.people),
url(r'^(?P[A-Za-z0-9.-]+)/people/modify/$', views.modify),
]
diff --git a/ietf/secr/areas/views.py b/ietf/secr/areas/views.py
index ca0bcb0a8..93e438cf8 100644
--- a/ietf/secr/areas/views.py
+++ b/ietf/secr/areas/views.py
@@ -1,17 +1,14 @@
-import datetime
import json
from django.contrib import messages
-from django.forms.formsets import formset_factory
-from django.forms.models import inlineformset_factory
from django.http import HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
-from ietf.group.models import Group, GroupEvent, GroupURL, Role, ChangeStateGroupEvent
+from ietf.group.models import Group, GroupEvent, Role
from ietf.group.utils import save_group_in_history
from ietf.ietfauth.utils import role_required
from ietf.person.models import Person
-from ietf.secr.areas.forms import AWPAddModelForm, AWPForm, AddAreaModelForm, AreaDirectorForm, AreaForm
+from ietf.secr.areas.forms import AreaDirectorForm
# --------------------------------------------------
# AJAX FUNCTIONS
@@ -49,114 +46,7 @@ def getemails(request):
# --------------------------------------------------
# STANDARD VIEW FUNCTIONS
# --------------------------------------------------
-@role_required('Secretariat')
-def add(request):
- """
- Add a new IETF Area
- **Templates:**
-
- * ``areas/add.html``
-
- **Template Variables:**
-
- * area_form
-
- """
- AWPFormSet = formset_factory(AWPAddModelForm, extra=2)
- if request.method == 'POST':
- area_form = AddAreaModelForm(request.POST)
- awp_formset = AWPFormSet(request.POST, prefix='awp')
- if area_form.is_valid() and awp_formset.is_valid():
- area = area_form.save()
-
- #save groupevent 'started' record
- start_date = area_form.cleaned_data.get('start_date')
- login = request.user.person
- group_event = GroupEvent(group=area,time=start_date,type='started',by=login)
- group_event.save()
-
- # save AWPs
- for item in awp_formset.cleaned_data:
- if item.get('url', 0):
- group_url = GroupURL(group=area,name=item['name'],url=item['url'])
- group_url.save()
-
- messages.success(request, 'The Area was created successfully!')
- return redirect('ietf.secr.areas.views.list_areas')
- else:
- # display initial forms
- area_form = AddAreaModelForm()
- awp_formset = AWPFormSet(prefix='awp')
-
- return render(request, 'areas/add.html', {
- 'area_form': area_form,
- 'awp_formset': awp_formset},
- )
-
-@role_required('Secretariat')
-def edit(request, name):
- """
- Edit IETF Areas
-
- **Templates:**
-
- * ``areas/edit.html``
-
- **Template Variables:**
-
- * acronym, area_formset, awp_formset, acronym_form
-
- """
- area = get_object_or_404(Group, acronym=name, type='area')
-
- AWPFormSet = inlineformset_factory(Group, GroupURL, form=AWPForm, max_num=2)
- if request.method == 'POST':
- button_text = request.POST.get('submit', '')
- if button_text == 'Save':
- form = AreaForm(request.POST, instance=area)
- awp_formset = AWPFormSet(request.POST, instance=area)
- if form.is_valid() and awp_formset.is_valid():
- state = form.cleaned_data['state']
-
- # save group
- save_group_in_history(area)
-
- new_area = form.save()
- new_area.time = datetime.datetime.now()
- new_area.save()
- awp_formset.save()
-
- # create appropriate GroupEvent
- if 'state' in form.changed_data:
- ChangeStateGroupEvent.objects.create(group=new_area,
- type='changed_state',
- by=request.user.person,
- state=state,
- time=new_area.time)
- form.changed_data.remove('state')
-
- # if anything else was changed
- if form.changed_data:
- GroupEvent.objects.create(group=new_area,
- type='info_changed',
- by=request.user.person,
- time=new_area.time)
-
- messages.success(request, 'The Area entry was changed successfully')
- return redirect('ietf.secr.areas.views.view', name=name)
- else:
- return redirect('ietf.secr.areas.views.view', name=name)
- else:
- form = AreaForm(instance=area)
- awp_formset = AWPFormSet(instance=area)
-
- return render(request, 'areas/edit.html', {
- 'area': area,
- 'form': form,
- 'awp_formset': awp_formset,
- },
- )
@role_required('Secretariat')
def list_areas(request):
diff --git a/ietf/secr/groups/forms.py b/ietf/secr/groups/forms.py
index a21d57243..7b9611515 100644
--- a/ietf/secr/groups/forms.py
+++ b/ietf/secr/groups/forms.py
@@ -6,7 +6,6 @@ from django.db.models import Count
from ietf.group.models import Group, Role
from ietf.name.models import GroupStateName, GroupTypeName, RoleName
from ietf.person.models import Person, Email
-from ietf.liaisons.models import LiaisonStatementGroupContacts
# ---------------------------------------------
@@ -48,83 +47,6 @@ class DescriptionForm (forms.Form):
description = forms.CharField(widget=forms.Textarea(attrs={'rows':'20'}),required=True, strip=False)
-class GroupModelForm(forms.ModelForm):
- type = forms.ModelChoiceField(queryset=GroupTypeName.objects.all(),empty_label=None)
- parent = forms.ModelChoiceField(queryset=Group.objects.all(),required=False)
- ad = forms.ModelChoiceField(queryset=Person.objects.filter(role__name='ad',role__group__state='active',role__group__type='area'),required=False)
- state = forms.ModelChoiceField(queryset=GroupStateName.objects.exclude(slug__in=('dormant','unknown')),empty_label=None)
- liaison_contacts = forms.CharField(max_length=255,required=False,label='Default Liaison Contacts')
-
- class Meta:
- model = Group
- fields = ('acronym','name','type','state','parent','ad','list_email','list_subscribe','list_archive','description','comments')
-
- def __init__(self, *args, **kwargs):
- super(GroupModelForm, self).__init__(*args, **kwargs)
- self.fields['list_email'].label = 'List Email'
- self.fields['list_subscribe'].label = 'List Subscribe'
- self.fields['list_archive'].label = 'List Archive'
- self.fields['ad'].label = 'Area Director'
- self.fields['comments'].widget.attrs['rows'] = 3
- self.fields['parent'].label = 'Area / Parent'
- self.fields['parent'].choices = get_parent_group_choices()
-
- if self.instance.pk:
- lsgc = self.instance.liaisonstatementgroupcontacts_set.first() # there can only be one
- if lsgc:
- self.fields['liaison_contacts'].initial = lsgc.contacts
-
- def clean_acronym(self):
- acronym = self.cleaned_data['acronym']
- if any(x.isupper() for x in acronym):
- raise forms.ValidationError('Capital letters not allowed in group acronym')
- return acronym
-
- def clean_parent(self):
- parent = self.cleaned_data['parent']
- type = self.cleaned_data['type']
-
- if type.features.acts_like_wg and not parent:
- raise forms.ValidationError("This field is required.")
-
- return parent
-
- def clean(self):
- if any(self.errors):
- return self.cleaned_data
- super(GroupModelForm, self).clean()
-
- type = self.cleaned_data['type']
- parent = self.cleaned_data['parent']
- state = self.cleaned_data['state']
- irtf_area = Group.objects.get(acronym='irtf')
-
- # ensure proper parent for group type
- if type.slug == 'rg' and parent != irtf_area:
- raise forms.ValidationError('The Area for a research group must be %s' % irtf_area)
-
- # an RG can't be proposed
- if type.slug == 'rg' and state.slug not in ('active','conclude'):
- raise forms.ValidationError('You must choose "active" or "concluded" for research group state')
-
- return self.cleaned_data
-
- def save(self, force_insert=False, force_update=False, commit=True):
- obj = super(GroupModelForm, self).save(commit=False)
- if commit:
- obj.save()
- contacts = self.cleaned_data.get('liaison_contacts')
- if contacts:
- try:
- lsgc = LiaisonStatementGroupContacts.objects.get(group=self.instance)
- lsgc.contacts = contacts
- lsgc.save()
- except LiaisonStatementGroupContacts.DoesNotExist:
- LiaisonStatementGroupContacts.objects.create(group=self.instance,contacts=contacts)
- elif LiaisonStatementGroupContacts.objects.filter(group=self.instance):
- LiaisonStatementGroupContacts.objects.filter(group=self.instance).delete()
-
- return obj
class RoleForm(forms.Form):
name = forms.ModelChoiceField(RoleName.objects.filter(slug__in=('chair','editor','secr','techadv')),empty_label=None)
diff --git a/ietf/secr/groups/tests.py b/ietf/secr/groups/tests.py
index 35bd245eb..f4772abcf 100644
--- a/ietf/secr/groups/tests.py
+++ b/ietf/secr/groups/tests.py
@@ -9,7 +9,6 @@ from ietf.secr.groups.forms import get_parent_group_choices
from ietf.group.factories import GroupFactory, RoleFactory
from ietf.meeting.factories import MeetingFactory
from ietf.person.factories import PersonFactory
-from ietf.person.models import Person
import debug # pyflakes:ignore
class GroupsTest(TestCase):
@@ -31,73 +30,6 @@ class GroupsTest(TestCase):
response = self.client.post(url,post_data,follow=True)
self.assertContains(response, group.acronym)
- # ------- Test Add -------- #
- def test_add_button(self):
- url = reverse('ietf.secr.groups.views.search')
- target = reverse('ietf.secr.groups.views.add')
- post_data = {'submit':'Add'}
- self.client.login(username="secretary", password="secretary+password")
- response = self.client.post(url,post_data,follow=True)
- self.assertRedirects(response, target)
-
- def test_add_group_invalid(self):
- url = reverse('ietf.secr.groups.views.add')
- post_data = {'acronym':'test',
- 'type':'wg',
- 'awp-TOTAL_FORMS':'2',
- 'awp-INITIAL_FORMS':'0',
- 'submit':'Save'}
- self.client.login(username="secretary", password="secretary+password")
- response = self.client.post(url,post_data)
- self.assertContains(response, 'This field is required')
-
- def test_add_group_dupe(self):
- group = GroupFactory()
- area = GroupFactory(type_id='area')
- url = reverse('ietf.secr.groups.views.add')
- post_data = {'acronym':group.acronym,
- 'name':'Test Group',
- 'state':'active',
- 'type':'wg',
- 'parent':area.id,
- 'awp-TOTAL_FORMS':'2',
- 'awp-INITIAL_FORMS':'0',
- 'submit':'Save'}
- self.client.login(username="secretary", password="secretary+password")
- response = self.client.post(url,post_data)
- self.assertContains(response, 'Group with this Acronym already exists')
-
- def test_add_group_success(self):
- area = GroupFactory(type_id='area')
- url = reverse('ietf.secr.groups.views.add')
- post_data = {'acronym':'test',
- 'name':'Test Group',
- 'type':'wg',
- 'status':'active',
- 'parent':area.id,
- 'awp-TOTAL_FORMS':'2',
- 'awp-INITIAL_FORMS':'0',
- 'submit':'Save'}
- self.client.login(username="secretary", password="secretary+password")
- response = self.client.post(url,post_data)
- self.assertEqual(response.status_code, 200)
-
- def test_add_group_capital_acronym(self):
- area = GroupFactory(type_id='area')
- url = reverse('ietf.secr.groups.views.add')
- post_data = {'acronym':'TEST',
- 'name':'Test Group',
- 'type':'wg',
- 'status':'active',
- 'parent':area.id,
- 'awp-TOTAL_FORMS':'2',
- 'awp-INITIAL_FORMS':'0',
- 'submit':'Save'}
- self.client.login(username="secretary", password="secretary+password")
- response = self.client.post(url,post_data)
- self.assertEqual(response.status_code, 200)
- self.assertContains(response, 'Capital letters not allowed in group acronym')
-
# ------- Test View -------- #
def test_view(self):
MeetingFactory(type_id='ietf')
@@ -107,47 +39,6 @@ class GroupsTest(TestCase):
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
- # ------- Test Edit -------- #
- def test_edit_valid(self):
- group = GroupFactory()
- area = GroupFactory(type_id='area')
- ad = Person.objects.get(name='AreaĆ° Irector')
- MeetingFactory(type_id='ietf')
- url = reverse('ietf.secr.groups.views.edit', kwargs={'acronym':group.acronym})
- target = reverse('ietf.secr.groups.views.view', kwargs={'acronym':group.acronym})
- post_data = {'acronym':group.acronym,
- 'name':group.name,
- 'type':'wg',
- 'state':group.state_id,
- 'parent':area.id,
- 'ad':ad.id,
- 'groupurl_set-TOTAL_FORMS':'2',
- 'groupurl_set-INITIAL_FORMS':'0',
- 'submit':'Save'}
- self.client.login(username="secretary", password="secretary+password")
- response = self.client.post(url,post_data,follow=True)
- self.assertRedirects(response, target)
- self.assertContains(response, 'changed successfully')
-
- def test_edit_non_wg_group(self):
- parent_sdo = GroupFactory.create(type_id='sdo',state_id='active')
- child_sdo = GroupFactory.create(type_id='sdo',state_id='active',parent=parent_sdo)
- MeetingFactory(type_id='ietf')
- url = reverse('ietf.secr.groups.views.edit', kwargs={'acronym':child_sdo.acronym})
- target = reverse('ietf.secr.groups.views.view', kwargs={'acronym':child_sdo.acronym})
- post_data = {'acronym':child_sdo.acronym,
- 'name':'New Name',
- 'type':'sdo',
- 'state':child_sdo.state_id,
- 'parent':parent_sdo.id,
- 'ad':'',
- 'groupurl_set-TOTAL_FORMS':'2',
- 'groupurl_set-INITIAL_FORMS':'0',
- 'submit':'Save'}
- self.client.login(username="secretary", password="secretary+password")
- response = self.client.post(url,post_data,follow=True)
- self.assertRedirects(response, target)
- self.assertContains(response, 'changed successfully')
# ------- Test People -------- #
def test_people_delete(self):
diff --git a/ietf/secr/groups/urls.py b/ietf/secr/groups/urls.py
index 32659e323..60d3566ca 100644
--- a/ietf/secr/groups/urls.py
+++ b/ietf/secr/groups/urls.py
@@ -5,12 +5,10 @@ from ietf.utils.urls import url
urlpatterns = [
url(r'^$', views.search),
- url(r'^add/$', views.add),
url(r'^blue-dot-report/$', views.blue_dot),
#(r'^ajax/get_ads/$', views.get_ads),
url(r'^%(acronym)s/$' % settings.URL_REGEXPS, views.view),
url(r'^%(acronym)s/delete/(?P\d{1,6})/$' % settings.URL_REGEXPS, views.delete_role),
url(r'^%(acronym)s/charter/$' % settings.URL_REGEXPS, views.charter),
- url(r'^%(acronym)s/edit/$' % settings.URL_REGEXPS, views.edit),
url(r'^%(acronym)s/people/$' % settings.URL_REGEXPS, views.people),
]
diff --git a/ietf/secr/groups/views.py b/ietf/secr/groups/views.py
index ad3ac8ff7..7f72dce43 100644
--- a/ietf/secr/groups/views.py
+++ b/ietf/secr/groups/views.py
@@ -1,14 +1,12 @@
from django.contrib import messages
from django.conf import settings
-from django.forms.models import inlineformset_factory
from django.shortcuts import render, get_object_or_404, redirect
-from ietf.group.models import Group, ChangeStateGroupEvent, GroupEvent, GroupURL, Role
-from ietf.group.utils import save_group_in_history, get_charter_text, setup_default_community_list_for_group
+from ietf.group.models import Group, GroupEvent, Role
+from ietf.group.utils import save_group_in_history, get_charter_text
from ietf.ietfauth.utils import role_required
from ietf.person.models import Person
-from ietf.secr.groups.forms import GroupModelForm, RoleForm, SearchForm
-from ietf.secr.areas.forms import AWPForm
+from ietf.secr.groups.forms import RoleForm, SearchForm
from ietf.secr.utils.meeting import get_current_meeting
# -------------------------------------------------
@@ -71,58 +69,7 @@ def get_ads(request):
# Standard View Functions
# -------------------------------------------------
-@role_required('Secretariat')
-def add(request):
- '''
- Add a new IETF or IRTF Group
- **Templates:**
-
- * ``groups/add.html``
-
- **Template Variables:**
-
- * form, awp_formset
-
- '''
- AWPFormSet = inlineformset_factory(Group, GroupURL, form=AWPForm, max_num=2, can_delete=False)
- if request.method == 'POST':
- button_text = request.POST.get('submit', '')
- if button_text == 'Cancel':
- return redirect('ietf.secr.groups.views.search')
-
- form = GroupModelForm(request.POST)
- awp_formset = AWPFormSet(request.POST, prefix='awp')
- if form.is_valid() and awp_formset.is_valid():
- group = form.save()
- for form in awp_formset.forms:
- if form.has_changed():
- awp = form.save(commit=False)
- awp.group = group
- awp.save()
-
- if group.features.has_documents:
- setup_default_community_list_for_group(group)
-
- # create GroupEvent(s)
- # always create started event
- ChangeStateGroupEvent.objects.create(group=group,
- type='changed_state',
- by=request.user.person,
- state=group.state,
- desc='Started group')
-
- messages.success(request, 'The Group was created successfully!')
- return redirect('ietf.secr.groups.views.view', acronym=group.acronym)
-
- else:
- form = GroupModelForm(initial={'state':'active','type':'wg'})
- awp_formset = AWPFormSet(prefix='awp')
-
- return render(request, 'groups/add.html', {
- 'form': form,
- 'awp_formset': awp_formset},
- )
@role_required('Secretariat')
def blue_dot(request):
@@ -202,83 +149,6 @@ def delete_role(request, acronym, id):
return render(request, 'confirm_delete.html', {'object': role})
-@role_required('Secretariat')
-def edit(request, acronym):
- """
- Edit Group details
-
- **Templates:**
-
- * ``groups/edit.html``
-
- **Template Variables:**
-
- * group, form, awp_formset
-
- """
-
- group = get_object_or_404(Group, acronym=acronym)
- AWPFormSet = inlineformset_factory(Group, GroupURL, form=AWPForm, max_num=2)
-
- if request.method == 'POST':
- button_text = request.POST.get('submit', '')
- if button_text == 'Cancel':
- return redirect('ietf.secr.groups.views.view', acronym=acronym)
-
- form = GroupModelForm(request.POST, instance=group)
- awp_formset = AWPFormSet(request.POST, instance=group)
- if form.is_valid() and awp_formset.is_valid():
-
- awp_formset.save()
- if form.changed_data:
- state = form.cleaned_data['state']
-
- # save group
- save_group_in_history(group)
-
- form.save()
-
- # create appropriate GroupEvent
- if 'state' in form.changed_data:
- if state.name == 'Active':
- desc = 'Started group'
- else:
- desc = state.name + ' group'
- ChangeStateGroupEvent.objects.create(group=group,
- type='changed_state',
- by=request.user.person,
- state=state,
- desc=desc)
- form.changed_data.remove('state')
-
- # if anything else was changed
- if form.changed_data:
- GroupEvent.objects.create(group=group,
- type='info_changed',
- by=request.user.person,
- desc='Info Changed')
-
- # if the acronym was changed we'll want to redirect using the new acronym below
- if 'acronym' in form.changed_data:
- acronym = form.cleaned_data['acronym']
-
- messages.success(request, 'The Group was changed successfully')
-
- return redirect('ietf.secr.groups.views.view', acronym=acronym)
-
- else:
- form = GroupModelForm(instance=group)
- awp_formset = AWPFormSet(instance=group)
-
- messages.warning(request, "WARNING: don't use this tool to change group names. Use Datatracker when possible.")
-
- return render(request, 'groups/edit.html', {
- 'group': group,
- 'awp_formset': awp_formset,
- 'form': form},
- )
-
-
@role_required('Secretariat')
def people(request, acronym):
"""
@@ -343,8 +213,6 @@ def search(request):
results = []
if request.method == 'POST':
form = SearchForm(request.POST)
- if request.POST['submit'] == 'Add':
- return redirect('ietf.secr.groups.views.add')
if form.is_valid():
kwargs = {}
diff --git a/ietf/secr/templates/areas/add.html b/ietf/secr/templates/areas/add.html
deleted file mode 100644
index 19edd797c..000000000
--- a/ietf/secr/templates/areas/add.html
+++ /dev/null
@@ -1,31 +0,0 @@
-{% extends "base_site.html" %}
-{% load staticfiles %}
-{% block title %}Areas - Add{% endblock %}
-
-{% block extrahead %}{{ block.super }}
-
-{% endblock %}
-
-{% block breadcrumbs %}{{ block.super }}
- » Areas
- » Add
-{% endblock %}
-
-{% block content %}
-
-
-
-{% endblock %}
diff --git a/ietf/secr/templates/areas/edit.html b/ietf/secr/templates/areas/edit.html
deleted file mode 100644
index 238c295dc..000000000
--- a/ietf/secr/templates/areas/edit.html
+++ /dev/null
@@ -1,32 +0,0 @@
-{% extends "base_site.html" %}
-{% load staticfiles %}
-{% block title %}Areas - Edit{% endblock %}
-
-{% block extrahead %}{{ block.super }}
-
-{% endblock %}
-
-{% block breadcrumbs %}{{ block.super }}
- » Areas
- » {{ area.acronym }}
- » Edit
-{% endblock %}
-
-{% block content %}
-
-
-
-{% endblock %}
diff --git a/ietf/secr/templates/areas/list.html b/ietf/secr/templates/areas/list.html
index ed5ec4511..7cefc114e 100644
--- a/ietf/secr/templates/areas/list.html
+++ b/ietf/secr/templates/areas/list.html
@@ -13,7 +13,7 @@
{% block content %}
-
+
Areas
diff --git a/ietf/secr/templates/areas/view.html b/ietf/secr/templates/areas/view.html
index f3f428ff1..716e15a23 100644
--- a/ietf/secr/templates/areas/view.html
+++ b/ietf/secr/templates/areas/view.html
@@ -35,10 +35,6 @@
- {% with area.groupurl_set.all as awps %}
- {% include "includes/awp_view.html" %}
- {% endwith %}
-