migrated forward
- Legacy-Id: 18144
This commit is contained in:
commit
53f7bc3ce6
|
@ -11,8 +11,9 @@ from .models import (StateType, State, RelatedDocument, DocumentAuthor, Document
|
||||||
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
|
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
|
||||||
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
|
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
|
||||||
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL,
|
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):
|
class StateTypeAdmin(admin.ModelAdmin):
|
||||||
list_display = ["slug", "label"]
|
list_display = ["slug", "label"]
|
||||||
|
@ -183,3 +184,14 @@ class DocumentUrlAdmin(admin.ModelAdmin):
|
||||||
search_fields = ['doc__name', 'url', ]
|
search_fields = ['doc__name', 'url', ]
|
||||||
raw_id_fields = ['doc', ]
|
raw_id_fields = ['doc', ]
|
||||||
admin.site.register(DocumentURL, DocumentUrlAdmin)
|
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)
|
||||||
|
|
30
ietf/doc/management/commands/find_github_backup_info.py
Normal file
30
ietf/doc/management/commands/find_github_backup_info.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||||
|
import json
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
|
from ietf.extresource.models import ExtResource
|
||||||
|
from ietf.doc.models import DocExtResource
|
||||||
|
from ietf.group.models import GroupExtResource
|
||||||
|
from ietf.person.models import PersonExtResource
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = ('Locate information about gihub repositories to backup')
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
|
||||||
|
info_dict = {}
|
||||||
|
for repo in ExtResource.objects.filter(name__slug='github_repo'):
|
||||||
|
if repo not in info_dict:
|
||||||
|
info_dict[repo.value] = []
|
||||||
|
|
||||||
|
for username in DocExtResource.objects.filter(extresource__name__slug='github_username', doc__name__in=repo.docextresource_set.values_list('doc__name',flat=True).distinct()):
|
||||||
|
info_dict[repo.value].push(username.value)
|
||||||
|
|
||||||
|
for username in GroupExtResource.objects.filter(extresource__name__slug='github_username', group__acronym__in=repo.groupextresource_set.values_list('group__acronym',flat=True).distinct()):
|
||||||
|
info_dict[repo.value].push(username.value)
|
||||||
|
|
||||||
|
for username in PersonExtResource.objects.filter(extresource__name__slug='github_username', person_id__in=repo.personextresource_set.values_list('person__id',flat=True).distinct()):
|
||||||
|
info_dict[repo.value].push(username.value)
|
||||||
|
|
||||||
|
print (json.dumps(info_dict))
|
28
ietf/doc/migrations/0033_extres.py
Normal file
28
ietf/doc/migrations/0033_extres.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.29 on 2020-04-15 10:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ietf.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0013_extres'),
|
||||||
|
('doc', '0032_auto_20200624_1332'),
|
||||||
|
]
|
||||||
|
|
||||||
|
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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
118
ietf/doc/migrations/0034_populate_docextresources.py
Normal file
118
ietf/doc/migrations/0034_populate_docextresources.py
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.29 on 2020-03-19 13:06
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
import debug
|
||||||
|
|
||||||
|
from collections import OrderedDict, Counter
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
from ietf.utils.validators import validate_external_resource_value
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
name_map = {
|
||||||
|
"Issue.*": "tracker",
|
||||||
|
".*FAQ.*": "faq",
|
||||||
|
".*Area Web Page": "webpage",
|
||||||
|
".*Wiki": "wiki",
|
||||||
|
"Home Page": "webpage",
|
||||||
|
"Slack.*": "slack",
|
||||||
|
"Additional .* Web Page": "webpage",
|
||||||
|
"Additional .* Page": "webpage",
|
||||||
|
"Yang catalog entry.*": "yc_entry",
|
||||||
|
"Yang impact analysis.*": "yc_impact",
|
||||||
|
"GitHub": "github_repo",
|
||||||
|
"Github page": "github_repo",
|
||||||
|
"GitHub repo.*": "github_repo",
|
||||||
|
"Github repository.*": "github_repo",
|
||||||
|
"GitHub notifications": "github_notify",
|
||||||
|
"GitHub org.*": "github_org",
|
||||||
|
"GitHub User.*": "github_username",
|
||||||
|
"GitLab User": "gitlab_username",
|
||||||
|
"GitLab User Name": "gitlab_username",
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: Review all the None values below and make sure ignoring the URLs they match is really the right thing to do.
|
||||||
|
url_map = OrderedDict({
|
||||||
|
"https?://github\\.com": "github_repo",
|
||||||
|
"https://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()
|
||||||
|
|
||||||
|
for doc_url in DocumentUrl.objects.all():
|
||||||
|
match_found = False
|
||||||
|
for regext,slug in name_map.items():
|
||||||
|
if re.match(regext, doc_url.desc):
|
||||||
|
match_found = True
|
||||||
|
stats['mapped'] += 1
|
||||||
|
name = ExtResourceName.objects.get(slug=slug)
|
||||||
|
DocExtResource.objects.create(doc=doc_url.doc, name_id=slug, value=doc_url.url, display_name=doc_url.desc) # TODO: validate this value against name.type
|
||||||
|
break
|
||||||
|
if not match_found:
|
||||||
|
for regext, slug in url_map.items():
|
||||||
|
doc_url.url = doc_url.url.strip()
|
||||||
|
if re.search(regext, doc_url.url):
|
||||||
|
match_found = True
|
||||||
|
if slug:
|
||||||
|
stats['mapped'] +=1
|
||||||
|
name = ExtResourceName.objects.get(slug=slug)
|
||||||
|
# Munge the URL if it's the first github repo match
|
||||||
|
# Remove "/tree/master" substring if it exists
|
||||||
|
# Remove trailing "/issues" substring if it exists
|
||||||
|
# Remove "/blob/master/.*" pattern if present
|
||||||
|
if regext == "https?://github\\.com":
|
||||||
|
doc_url.url = doc_url.url.replace("/tree/master","")
|
||||||
|
doc_url.url = re.sub('/issues$', '', doc_url.url)
|
||||||
|
doc_url.url = re.sub('/blob/master.*$', '', doc_url.url)
|
||||||
|
try:
|
||||||
|
validate_external_resource_value(name, doc_url.url)
|
||||||
|
DocExtResource.objects.create(doc=doc_url.doc, name=name, value=doc_url.url, display_name=doc_url.desc) # TODO: validate this value against name.type
|
||||||
|
except ValidationError as e: # pyflakes:ignore
|
||||||
|
debug.show('("Failed validation:", doc_url.url, e)')
|
||||||
|
stats['failed_validation'] +=1
|
||||||
|
else:
|
||||||
|
stats['ignored'] +=1
|
||||||
|
break
|
||||||
|
if not match_found:
|
||||||
|
debug.show('("Not Mapped:",doc_url.desc, doc_url.tag.slug, doc_url.doc.name, doc_url.url)')
|
||||||
|
stats['not_mapped'] += 1
|
||||||
|
print (stats)
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
DocExtResource = apps.get_model('doc', 'DocExtResource')
|
||||||
|
DocExtResource.objects.all().delete()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('doc', '0033_extres'),
|
||||||
|
('name', '0014_populate_extres'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse)
|
||||||
|
]
|
|
@ -24,7 +24,7 @@ import debug # pyflakes:ignore
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName,
|
from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName,
|
||||||
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, ReviewAssignmentStateName, FormalLanguageName,
|
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, ReviewAssignmentStateName, FormalLanguageName,
|
||||||
DocUrlTagName)
|
DocUrlTagName, ExtResourceName)
|
||||||
from ietf.person.models import Email, Person
|
from ietf.person.models import Email, Person
|
||||||
from ietf.person.utils import get_active_balloters
|
from ietf.person.utils import get_active_balloters
|
||||||
from ietf.utils import log
|
from ietf.utils import log
|
||||||
|
@ -105,6 +105,7 @@ class DocumentInfo(models.Model):
|
||||||
note = models.TextField(blank=True)
|
note = models.TextField(blank=True)
|
||||||
internal_comments = models.TextField(blank=True)
|
internal_comments = models.TextField(blank=True)
|
||||||
|
|
||||||
|
|
||||||
def file_extension(self):
|
def file_extension(self):
|
||||||
if not hasattr(self, '_cached_extension'):
|
if not hasattr(self, '_cached_extension'):
|
||||||
if self.uploaded_filename:
|
if self.uploaded_filename:
|
||||||
|
@ -861,6 +862,15 @@ class DocumentURL(models.Model):
|
||||||
desc = models.CharField(max_length=255, default='', blank=True)
|
desc = models.CharField(max_length=255, default='', blank=True)
|
||||||
url = models.URLField(max_length=2083) # 2083 is the legal max for URLs
|
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):
|
class RelatedDocHistory(models.Model):
|
||||||
source = ForeignKey('DocHistory')
|
source = ForeignKey('DocHistory')
|
||||||
target = ForeignKey('DocAlias', related_name="reversely_related_document_history_set")
|
target = ForeignKey('DocAlias', related_name="reversely_related_document_history_set")
|
||||||
|
|
|
@ -17,7 +17,7 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
|
||||||
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
|
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
|
||||||
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
|
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
|
||||||
ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL,
|
ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL,
|
||||||
IanaExpertDocEvent, IRSGBallotDocEvent )
|
IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource )
|
||||||
|
|
||||||
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
|
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
|
||||||
class BallotTypeResource(ModelResource):
|
class BallotTypeResource(ModelResource):
|
||||||
|
@ -767,3 +767,23 @@ class IRSGBallotDocEventResource(ModelResource):
|
||||||
"ballotdocevent_ptr": ALL_WITH_RELATIONS,
|
"ballotdocevent_ptr": ALL_WITH_RELATIONS,
|
||||||
}
|
}
|
||||||
api.doc.register(IRSGBallotDocEventResource())
|
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())
|
||||||
|
|
|
@ -1104,24 +1104,44 @@ class IndividualInfoFormsTests(TestCase):
|
||||||
q = PyQuery(r.content)
|
q = PyQuery(r.content)
|
||||||
self.assertTrue(q('textarea')[0].text.strip().startswith("As required by RFC 4858"))
|
self.assertTrue(q('textarea')[0].text.strip().startswith("As required by RFC 4858"))
|
||||||
|
|
||||||
def test_doc_change_document_urls(self):
|
def test_edit_doc_extresources(self):
|
||||||
url = urlreverse('ietf.doc.views_draft.edit_document_urls', kwargs=dict(name=self.docname))
|
url = urlreverse('ietf.doc.views_draft.edit_doc_extresources', kwargs=dict(name=self.docname))
|
||||||
|
|
||||||
# get
|
|
||||||
login_testing_unauthorized(self, "secretary", url)
|
login_testing_unauthorized(self, "secretary", url)
|
||||||
|
|
||||||
r = self.client.get(url)
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code,200)
|
self.assertEqual(r.status_code,200)
|
||||||
q = PyQuery(r.content)
|
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
|
badlines = (
|
||||||
r = self.client.post(url, dict(urls='wiki https://wiki.org/ Wiki\nrepository https://repository.org/ Repo\n', submit="1"))
|
'github_repo https://github3.com/some/repo',
|
||||||
|
'github_notify badaddr',
|
||||||
|
'website /not/a/good/url'
|
||||||
|
'notavalidtag blahblahblah'
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in badlines:
|
||||||
|
r = self.client.post(url, dict(resources=line, submit="1"))
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
self.assertTrue(q('.alert-danger'))
|
||||||
|
|
||||||
|
goodlines = """
|
||||||
|
github_repo https://github.com/some/repo Some display text
|
||||||
|
github_notify notify@example.com
|
||||||
|
github_username githubuser
|
||||||
|
website http://example.com/http/is/fine
|
||||||
|
"""
|
||||||
|
|
||||||
|
r = self.client.post(url, dict(resources=goodlines, submit="1"))
|
||||||
self.assertEqual(r.status_code,302)
|
self.assertEqual(r.status_code,302)
|
||||||
doc = Document.objects.get(name=self.docname)
|
doc = Document.objects.get(name=self.docname)
|
||||||
self.assertTrue(doc.latest_event(DocEvent,type="changed_document").desc.startswith('Changed document URLs'))
|
self.assertEqual(doc.latest_event(DocEvent,type="changed_document").desc[:35], 'Changed document external resources')
|
||||||
self.assertIn('wiki https://wiki.org/', doc.latest_event(DocEvent,type="changed_document").desc)
|
self.assertIn('github_username githubuser', 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.docextresource_set.count(), 4)
|
||||||
|
self.assertEqual(doc.docextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
|
||||||
|
|
||||||
|
|
||||||
class SubmitToIesgTests(TestCase):
|
class SubmitToIesgTests(TestCase):
|
||||||
|
|
||||||
|
|
|
@ -128,7 +128,7 @@ urlpatterns = [
|
||||||
url(r'^%(name)s/edit/approveballot/$' % settings.URL_REGEXPS, views_ballot.approve_ballot),
|
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/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/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/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),
|
url(r'^%(name)s/edit/closeballot/irsg/$' % settings.URL_REGEXPS, views_ballot.close_irsg_ballot),
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,6 @@ from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
from django.core.validators import URLValidator
|
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404
|
from django.http import HttpResponseRedirect, HttpResponseForbidden, Http404
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
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 has_role, is_authorized_in_doc_stream, user_is_person, is_individual_draft_author
|
||||||
from ietf.ietfauth.utils import role_required
|
from ietf.ietfauth.utils import role_required
|
||||||
from ietf.message.models import Message
|
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.fields import SearchableEmailField
|
||||||
from ietf.person.models import Person, Email
|
from ietf.person.models import Person, Email
|
||||||
from ietf.utils.mail import send_mail, send_mail_message, on_behalf_of
|
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.textupload import get_cleaned_text_file_content
|
||||||
|
from ietf.utils.validators import validate_external_resource_value
|
||||||
from ietf.utils import log
|
from ietf.utils import log
|
||||||
from ietf.mailtrigger.utils import gather_address_lists
|
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):
|
def edit_doc_extresources(request, name):
|
||||||
lines = [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
|
class DocExtResourceForm(forms.Form):
|
||||||
url_validator = URLValidator()
|
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 = []
|
errors = []
|
||||||
for l in lines:
|
for l in lines:
|
||||||
parts = l.split()
|
parts = l.split()
|
||||||
if len(parts) == 1:
|
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:
|
elif len(parts) >= 2:
|
||||||
tag = parts[0]
|
name_slug = parts[0]
|
||||||
url = parts[1]
|
|
||||||
try:
|
try:
|
||||||
url_validator(url)
|
name = ExtResourceName.objects.get(slug=name_slug)
|
||||||
except ValidationError as e:
|
|
||||||
errors.append(e)
|
|
||||||
try:
|
|
||||||
DocUrlTagName.objects.get(slug=tag)
|
|
||||||
except ObjectDoesNotExist:
|
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:
|
if errors:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
def format_urls(urls, fs="\n"):
|
def format_resources(resources, fs="\n"):
|
||||||
res = []
|
res = []
|
||||||
for u in urls:
|
for r in resources:
|
||||||
if u.desc:
|
if r.display_name:
|
||||||
res.append("%s %s (%s)" % (u.tag.slug, u.url, u.desc.strip('()')))
|
res.append("%s %s (%s)" % (r.name.slug, r.value, r.display_name.strip('()')))
|
||||||
else:
|
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)
|
return fs.join(res)
|
||||||
|
|
||||||
doc = get_object_or_404(Document, name=name)
|
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)):
|
or is_individual_draft_author(request.user, doc)):
|
||||||
return HttpResponseForbidden("You do not have the necessary permissions to view this page")
|
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':
|
if request.method == 'POST':
|
||||||
form = DocumentUrlForm(request.POST)
|
form = DocExtResourceForm(request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
old_urls = sorted(old_urls.splitlines())
|
old_resources = sorted(old_resources.splitlines())
|
||||||
new_urls = sorted(form.cleaned_data['urls'])
|
new_resources = sorted(form.cleaned_data['resources'])
|
||||||
if old_urls != new_urls:
|
if old_resources != new_resources:
|
||||||
doc.documenturl_set.all().delete()
|
doc.docextresource_set.all().delete()
|
||||||
for u in new_urls:
|
for u in new_resources:
|
||||||
parts = u.split(None, 2)
|
parts = u.split(None, 2)
|
||||||
tag = parts[0]
|
name = parts[0]
|
||||||
url = parts[1]
|
value = parts[1]
|
||||||
desc = ' '.join(parts[2:]).strip('()')
|
display_name = ' '.join(parts[2:]).strip('()')
|
||||||
doc.documenturl_set.create(url=url, tag_id=tag, desc=desc)
|
doc.docextresource_set.create(value=value, name_id=name, display_name=display_name)
|
||||||
new_urls = format_urls(doc.documenturl_set.all())
|
new_resources = format_resources(doc.docextresource_set.all())
|
||||||
e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='changed_document')
|
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.desc = "Changed document external resources from:\n\n%s\n\nto:\n\n%s" % (old_resources, new_resources)
|
||||||
e.save()
|
e.save()
|
||||||
doc.save_with_history([e])
|
doc.save_with_history([e])
|
||||||
messages.success(request,"Document URLs updated.")
|
messages.success(request,"Document resources updated.")
|
||||||
else:
|
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)
|
return redirect('ietf.doc.views_doc.document_main', name=doc.name)
|
||||||
else:
|
else:
|
||||||
form = DocumentUrlForm(initial={'urls': old_urls, })
|
form = DocExtResourceForm(initial={'resources': old_resources, })
|
||||||
|
|
||||||
info = "Valid tags:<br><br> %s" % ', '.join([ o.slug for o in DocUrlTagName.objects.all() ])
|
info = "Valid tags:<br><br> %s" % ', '.join([ o.slug for o in ExtResourceName.objects.all().order_by('slug') ])
|
||||||
title = "Additional document URLs"
|
# 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) )
|
return render(request, 'doc/edit_field.html',dict(doc=doc, form=form, title=title, info=info) )
|
||||||
|
|
||||||
|
|
||||||
def request_publication(request, name):
|
def request_publication(request, name):
|
||||||
"""Request publication by RFC Editor for a document which hasn't
|
"""Request publication by RFC Editor for a document which hasn't
|
||||||
been through the IESG ballot process."""
|
been through the IESG ballot process."""
|
||||||
|
|
|
@ -18,7 +18,9 @@ from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
|
from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
|
||||||
GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
|
GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
|
||||||
MilestoneGroupEvent, )
|
MilestoneGroupEvent, GroupExtResource, )
|
||||||
|
|
||||||
|
from ietf.utils.validators import validate_external_resource_value
|
||||||
|
|
||||||
class RoleInline(admin.TabularInline):
|
class RoleInline(admin.TabularInline):
|
||||||
model = Role
|
model = Role
|
||||||
|
@ -203,3 +205,14 @@ class MilestoneGroupEventAdmin(admin.ModelAdmin):
|
||||||
list_filter = ['time']
|
list_filter = ['time']
|
||||||
raw_id_fields = ['group', 'by', 'milestone']
|
raw_id_fields = ['group', 'by', 'milestone']
|
||||||
admin.site.register(MilestoneGroupEvent, MilestoneGroupEventAdmin)
|
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)
|
||||||
|
|
|
@ -12,10 +12,11 @@ import debug # pyflakes:ignore
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.html import mark_safe # type:ignore
|
from django.utils.html import mark_safe # type:ignore
|
||||||
from django.db.models import F
|
from django.db.models import F
|
||||||
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
|
|
||||||
# IETF imports
|
# IETF imports
|
||||||
from ietf.group.models import Group, GroupHistory, GroupStateName, GroupFeatures
|
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.fields import SearchableEmailsField, PersonEmailChoiceField
|
||||||
from ietf.person.models import Person, Email
|
from ietf.person.models import Person, Email
|
||||||
from ietf.review.models import ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
|
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.textupload import get_cleaned_text_file_content
|
||||||
#from ietf.utils.ordereddict import insert_after_in_ordered_dict
|
#from ietf.utils.ordereddict import insert_after_in_ordered_dict
|
||||||
from ietf.utils.fields import DatepickerDateField, MultiEmailField
|
from ietf.utils.fields import DatepickerDateField, MultiEmailField
|
||||||
|
from ietf.utils.validators import validate_external_resource_value
|
||||||
|
|
||||||
# --- Constants --------------------------------------------------------
|
# --- Constants --------------------------------------------------------
|
||||||
|
|
||||||
|
@ -65,6 +67,7 @@ class GroupForm(forms.Form):
|
||||||
list_subscribe = forms.CharField(max_length=255, required=False)
|
list_subscribe = forms.CharField(max_length=255, required=False)
|
||||||
list_archive = 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)
|
urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", help_text="Format: https://site/path (Optional description). Separate multiple entries with newline. Prefer HTTPS URLs where possible.", required=False)
|
||||||
|
resources = forms.CharField(widget=forms.Textarea, label="Additional Resources", help_text="UPDATEME: Format: https://site/path (Optional description). Separate multiple entries with newline. Prefer HTTPS URLs where possible.", required=False)
|
||||||
closing_note = forms.CharField(widget=forms.Textarea, label="Closing note", required=False)
|
closing_note = forms.CharField(widget=forms.Textarea, label="Closing note", required=False)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -129,6 +132,12 @@ class GroupForm(forms.Form):
|
||||||
for f in keys:
|
for f in keys:
|
||||||
if f != field and not (f == 'closing_note' and field == 'state'):
|
if f != field and not (f == 'closing_note' and field == 'state'):
|
||||||
del self.fields[f]
|
del self.fields[f]
|
||||||
|
if 'resources' in self.fields:
|
||||||
|
info = "Format: 'tag value (Optional description)'. " \
|
||||||
|
+ "Separate multiple entries with newline. When the value is a URL, use https:// where possible.<br>" \
|
||||||
|
+ "Valid tags: %s" % ', '.join([ o.slug for o in ExtResourceName.objects.all().order_by('slug') ])
|
||||||
|
self.fields['resources'].help_text = mark_safe('<div>'+info+'</div>')
|
||||||
|
|
||||||
|
|
||||||
def clean_acronym(self):
|
def clean_acronym(self):
|
||||||
# Changing the acronym of an already existing group will cause 404s all
|
# Changing the acronym of an already existing group will cause 404s all
|
||||||
|
@ -188,6 +197,30 @@ class GroupForm(forms.Form):
|
||||||
def clean_urls(self):
|
def clean_urls(self):
|
||||||
return [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
|
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):
|
def clean_delegates(self):
|
||||||
if len(self.cleaned_data["delegates"]) > MAX_GROUP_DELEGATES:
|
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." % (
|
raise forms.ValidationError("At most %s delegates can be appointed at the same time, please remove %s delegates." % (
|
||||||
|
|
28
ietf/group/migrations/0033_extres.py
Normal file
28
ietf/group/migrations/0033_extres.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.29 on 2020-04-15 10:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ietf.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0013_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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
124
ietf/group/migrations/0034_populate_groupextresources.py
Normal file
124
ietf/group/migrations/0034_populate_groupextresources.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.29 on 2020-03-19 13:06
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
import debug
|
||||||
|
|
||||||
|
from collections import OrderedDict, Counter
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
from ietf.utils.validators import validate_external_resource_value
|
||||||
|
|
||||||
|
name_map = {
|
||||||
|
"Issue.*": "tracker",
|
||||||
|
".*FAQ.*": "faq",
|
||||||
|
".*Area Web Page": "webpage",
|
||||||
|
".*Wiki": "wiki",
|
||||||
|
"Home Page": "webpage",
|
||||||
|
"Slack.*": "slack",
|
||||||
|
"Additional .* Web Page": "webpage",
|
||||||
|
"Additional .* Page": "webpage",
|
||||||
|
"Yang catalog entry.*": "yc_entry",
|
||||||
|
"Yang impact analysis.*": "yc_impact",
|
||||||
|
"GitHub": "github_repo",
|
||||||
|
"Github page": "github_repo",
|
||||||
|
"GitHub repo.*": "github_repo",
|
||||||
|
"Github repository.*": "github_repo",
|
||||||
|
"GitHub notifications": "github_notify",
|
||||||
|
"GitHub org.*": "github_org",
|
||||||
|
"GitHub User.*": "github_username",
|
||||||
|
"GitLab User": "gitlab_username",
|
||||||
|
"GitLab User Name": "gitlab_username",
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: Consider dropping known bad links at this point
|
||||||
|
# " *https?://www.ietf.org/html.charters/*": None, # all these links are dead
|
||||||
|
# " *http://www.bell-labs.com/mailing-lists/pint": None, # dead link
|
||||||
|
# "http://www.ietf.org/wg/videos/mile-overview.html": None, # dead link
|
||||||
|
# " http://domen.uninett.no/~hta/ietf/notary-status.h": None, # dead link
|
||||||
|
# " http://www.ERC.MsState.Edu/packetway": None, # dead link
|
||||||
|
# "mailarchive\\.ietf\\.org" : None,
|
||||||
|
# "bell-labs\\.com": None,
|
||||||
|
# "html\\.charters": None,
|
||||||
|
# "datatracker\\.ietf\\.org": None,
|
||||||
|
# etc.
|
||||||
|
|
||||||
|
url_map = OrderedDict({
|
||||||
|
"https?://github\\.com": "github_repo",
|
||||||
|
"https?://trac\\.ietf\\.org/.*/wiki": "wiki",
|
||||||
|
"ietf\\.org.*/trac/wiki": "wiki",
|
||||||
|
"trac.*wiki": "wiki",
|
||||||
|
"www\\.ietf\\.org/mailman" : "mailing_list",
|
||||||
|
"www\\.ietf\\.org/mail-archive" : "mailing_list_archive",
|
||||||
|
"ietf\\.org/logs": "jabber_log",
|
||||||
|
"ietf\\.org/jabber/logs": "jabber_log",
|
||||||
|
"xmpp:.*?join": "jabber_room",
|
||||||
|
"https?://.*": "webpage"
|
||||||
|
})
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
GroupExtResource = apps.get_model('group', 'GroupExtResource')
|
||||||
|
ExtResourceName = apps.get_model('name', 'ExtResourceName')
|
||||||
|
GroupUrl = apps.get_model('group', 'GroupUrl')
|
||||||
|
|
||||||
|
stats = Counter()
|
||||||
|
|
||||||
|
for group_url in GroupUrl.objects.all():
|
||||||
|
match_found = False
|
||||||
|
for regext,slug in name_map.items():
|
||||||
|
if re.match(regext, group_url.name):
|
||||||
|
match_found = True
|
||||||
|
stats['mapped'] += 1
|
||||||
|
name = ExtResourceName.objects.get(slug=slug)
|
||||||
|
GroupExtResource.objects.create(group=group_url.group, name_id=slug, value=group_url.url, display_name=group_url.name) # TODO: validate this value against name.type
|
||||||
|
break
|
||||||
|
if not match_found:
|
||||||
|
for regext, slug in url_map.items():
|
||||||
|
group_url.url = group_url.url.strip()
|
||||||
|
if re.search(regext, group_url.url):
|
||||||
|
match_found = True
|
||||||
|
if slug:
|
||||||
|
stats['mapped'] +=1
|
||||||
|
name = ExtResourceName.objects.get(slug=slug)
|
||||||
|
# Munge the URL if it's the first github repo match
|
||||||
|
# Remove "/tree/master" substring if it exists
|
||||||
|
# Remove trailing "/issues" substring if it exists
|
||||||
|
# Remove "/blob/master/.*" pattern if present
|
||||||
|
if regext == "https?://github\\.com":
|
||||||
|
group_url.url = group_url.url.replace("/tree/master","")
|
||||||
|
group_url.url = re.sub('/issues$', '', group_url.url)
|
||||||
|
group_url.url = re.sub('/blob/master.*$', '', group_url.url)
|
||||||
|
try:
|
||||||
|
validate_external_resource_value(name, group_url.url)
|
||||||
|
GroupExtResource.objects.create(group=group_url.group, name=name, value=group_url.url, display_name=group_url.name) # TODO: validate this value against name.type
|
||||||
|
except ValidationError as e: # pyflakes:ignore
|
||||||
|
debug.show('("Failed validation:", group_url.url, e)')
|
||||||
|
stats['failed_validation'] +=1
|
||||||
|
else:
|
||||||
|
stats['ignored'] +=1
|
||||||
|
break
|
||||||
|
if not match_found:
|
||||||
|
debug.show('("Not Mapped:",group_url.group.acronym, group_url.name, group_url.url)')
|
||||||
|
stats['not_mapped'] += 1
|
||||||
|
print(stats)
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
GroupExtResource = apps.get_model('group', 'GroupExtResource')
|
||||||
|
GroupExtResource.objects.all().delete()
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('group', '0033_extres'),
|
||||||
|
('name', '0014_populate_extres'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse)
|
||||||
|
]
|
|
@ -21,7 +21,7 @@ from simple_history.models import HistoricalRecords
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.group.colors import fg_group_colors, bg_group_colors
|
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.person.models import Email, Person
|
||||||
from ietf.utils.mail import formataddr, send_mail_text
|
from ietf.utils.mail import formataddr, send_mail_text
|
||||||
from ietf.utils import log
|
from ietf.utils import log
|
||||||
|
@ -41,6 +41,7 @@ class GroupInfo(models.Model):
|
||||||
comments = models.TextField(blank=True)
|
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')
|
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_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)
|
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):
|
def __str__(self):
|
||||||
return u"%s (%s)" % (self.url, self.name)
|
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):
|
class GroupMilestoneInfo(models.Model):
|
||||||
group = ForeignKey(Group)
|
group = ForeignKey(Group)
|
||||||
# a group has two sets of milestones, current milestones
|
# a group has two sets of milestones, current milestones
|
||||||
|
|
|
@ -13,7 +13,7 @@ from ietf import api
|
||||||
|
|
||||||
from ietf.group.models import (Group, GroupStateTransitions, GroupMilestone, GroupHistory, # type: ignore
|
from ietf.group.models import (Group, GroupStateTransitions, GroupMilestone, GroupHistory, # type: ignore
|
||||||
GroupURL, Role, GroupEvent, RoleHistory, GroupMilestoneHistory, MilestoneGroupEvent,
|
GroupURL, Role, GroupEvent, RoleHistory, GroupMilestoneHistory, MilestoneGroupEvent,
|
||||||
ChangeStateGroupEvent, GroupFeatures, HistoricalGroupFeatures)
|
ChangeStateGroupEvent, GroupFeatures, HistoricalGroupFeatures, GroupExtResource)
|
||||||
|
|
||||||
|
|
||||||
from ietf.person.resources import PersonResource
|
from ietf.person.resources import PersonResource
|
||||||
|
@ -348,3 +348,23 @@ class HistoricalGroupFeaturesResource(ModelResource):
|
||||||
"history_user": ALL_WITH_RELATIONS,
|
"history_user": ALL_WITH_RELATIONS,
|
||||||
}
|
}
|
||||||
api.group.register(HistoricalGroupFeaturesResource())
|
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())
|
||||||
|
|
|
@ -610,7 +610,6 @@ class GroupEditTests(TestCase):
|
||||||
list_email="mars@mail",
|
list_email="mars@mail",
|
||||||
list_subscribe="subscribe.mars",
|
list_subscribe="subscribe.mars",
|
||||||
list_archive="archive.mars",
|
list_archive="archive.mars",
|
||||||
urls="http://mars.mars (MARS site)"
|
|
||||||
))
|
))
|
||||||
self.assertEqual(r.status_code, 302)
|
self.assertEqual(r.status_code, 302)
|
||||||
|
|
||||||
|
@ -624,8 +623,7 @@ class GroupEditTests(TestCase):
|
||||||
self.assertEqual(group.list_email, "mars@mail")
|
self.assertEqual(group.list_email, "mars@mail")
|
||||||
self.assertEqual(group.list_subscribe, "subscribe.mars")
|
self.assertEqual(group.list_subscribe, "subscribe.mars")
|
||||||
self.assertEqual(group.list_archive, "archive.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.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.assertEqual(len(outbox), 2)
|
||||||
self.assertTrue('Personnel change' in outbox[0]['Subject'])
|
self.assertTrue('Personnel change' in outbox[0]['Subject'])
|
||||||
|
@ -633,6 +631,46 @@ class GroupEditTests(TestCase):
|
||||||
self.assertTrue(prefix+'@' in outbox[0]['To'])
|
self.assertTrue(prefix+'@' in outbox[0]['To'])
|
||||||
self.assertTrue(get_payload_text(outbox[0]).startswith('Sec Retary'))
|
self.assertTrue(get_payload_text(outbox[0]).startswith('Sec Retary'))
|
||||||
|
|
||||||
|
def test_edit_extresources(self):
|
||||||
|
group = GroupFactory(acronym='mars',parent=GroupFactory(type_id='area'))
|
||||||
|
CharterFactory(group=group)
|
||||||
|
|
||||||
|
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit", field="resources"))
|
||||||
|
login_testing_unauthorized(self, "secretary", url)
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code,200)
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
self.assertEqual(len(q('form textarea[id=id_resources]')),1)
|
||||||
|
|
||||||
|
badlines = (
|
||||||
|
'github_repo https://github3.com/some/repo',
|
||||||
|
'github_notify badaddr',
|
||||||
|
'website /not/a/good/url'
|
||||||
|
'notavalidtag blahblahblah'
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in badlines:
|
||||||
|
r = self.client.post(url, dict(resources=line, submit="1"))
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
self.assertTrue(q('.alert-danger'))
|
||||||
|
|
||||||
|
goodlines = """
|
||||||
|
github_repo https://github.com/some/repo Some display text
|
||||||
|
github_notify notify@example.com
|
||||||
|
github_username githubuser
|
||||||
|
website http://example.com/http/is/fine
|
||||||
|
"""
|
||||||
|
|
||||||
|
r = self.client.post(url, dict(resources=goodlines, submit="1"))
|
||||||
|
self.assertEqual(r.status_code,302)
|
||||||
|
group = Group.objects.get(acronym=group.acronym)
|
||||||
|
self.assertEqual(group.latest_event(GroupEvent,type="info_changed").desc[:20], 'Resources changed to')
|
||||||
|
self.assertIn('github_username githubuser', group.latest_event(GroupEvent,type="info_changed").desc)
|
||||||
|
self.assertEqual(group.groupextresource_set.count(), 4)
|
||||||
|
self.assertEqual(group.groupextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
|
||||||
|
|
||||||
|
|
||||||
def test_edit_field(self):
|
def test_edit_field(self):
|
||||||
group = GroupFactory(acronym="mars")
|
group = GroupFactory(acronym="mars")
|
||||||
|
|
|
@ -73,7 +73,7 @@ from ietf.group.forms import (GroupForm, StatusUpdateForm, ConcludeGroupForm, St
|
||||||
ManageReviewRequestForm, EmailOpenAssignmentsForm, ReviewerSettingsForm,
|
ManageReviewRequestForm, EmailOpenAssignmentsForm, ReviewerSettingsForm,
|
||||||
AddUnavailablePeriodForm, EndUnavailablePeriodForm, ReviewSecretarySettingsForm, )
|
AddUnavailablePeriodForm, EndUnavailablePeriodForm, ReviewSecretarySettingsForm, )
|
||||||
from ietf.group.mails import email_admin_re_charter, email_personnel_change, email_comment
|
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 )
|
ChangeStateGroupEvent, GroupFeatures )
|
||||||
from ietf.group.utils import (get_charter_text, can_manage_group_type,
|
from ietf.group.utils import (get_charter_text, can_manage_group_type,
|
||||||
milestone_reviewer_for_group_type, can_provide_status_update,
|
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)))
|
+ 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.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:
|
for group in area.groups:
|
||||||
group.chairs = sorted(roles(group, "chair"), key=extract_last_name)
|
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]
|
group.ad_out_of_area = group.ad_role() and group.ad_role().person not in [role.person for role in area.ads]
|
||||||
|
@ -869,6 +869,17 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
|
||||||
res.append(u.url)
|
res.append(u.url)
|
||||||
return fs.join(res)
|
return fs.join(res)
|
||||||
|
|
||||||
|
def format_resources(resources, fs="\n"):
|
||||||
|
res = []
|
||||||
|
for r in resources:
|
||||||
|
if r.display_name:
|
||||||
|
res.append("%s %s (%s)" % (r.name.slug, r.value, r.display_name.strip('()')))
|
||||||
|
else:
|
||||||
|
res.append("%s %s" % (r.name.slug, r.value))
|
||||||
|
# TODO: This is likely problematic if value has spaces. How then to delineate value and display_name? Perhaps in the short term move to comma or pipe separation.
|
||||||
|
# Might be better to shift to a formset instead of parsing these lines.
|
||||||
|
return fs.join(res)
|
||||||
|
|
||||||
def diff(attr, name):
|
def diff(attr, name):
|
||||||
if field and attr != field:
|
if field and attr != field:
|
||||||
return
|
return
|
||||||
|
@ -922,11 +933,6 @@ def edit(request, group_type=None, acronym=None, action="edit", field=None):
|
||||||
else:
|
else:
|
||||||
save_group_in_history(group)
|
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 = []
|
changes = []
|
||||||
|
|
||||||
# update the attributes, keeping track of what we're doing
|
# update the attributes, keeping track of what we're doing
|
||||||
|
@ -996,22 +1002,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
|
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)
|
email_personnel_change(request, group, personnel_change_text, changed_personnel)
|
||||||
|
|
||||||
# update urls
|
if 'resources' in clean:
|
||||||
if 'urls' in clean:
|
old_resources = sorted(format_resources(group.groupextresource_set.all()).splitlines())
|
||||||
new_urls = clean['urls']
|
new_resources = sorted(clean['resources'])
|
||||||
old_urls = format_urls(group.groupurl_set.order_by('url'), ", ")
|
if old_resources != new_resources:
|
||||||
if ", ".join(sorted(new_urls)) != old_urls:
|
group.groupextresource_set.all().delete()
|
||||||
changes.append(('urls', new_urls, desc('Urls', ", ".join(sorted(new_urls)), old_urls)))
|
for u in new_resources:
|
||||||
group.groupurl_set.all().delete()
|
parts = u.split(None, 2)
|
||||||
# Add new ones
|
name = parts[0]
|
||||||
for u in new_urls:
|
value = parts[1]
|
||||||
m = re.search(r'(?P<url>[\w\d:#@%/;$()~_?\+-=\\\.&]+)( \((?P<name>.+)\))?', u)
|
display_name = ' '.join(parts[2:]).strip('()')
|
||||||
if m:
|
group.groupextresource_set.create(value=value, name_id=name, display_name=display_name)
|
||||||
if m.group('name'):
|
changes.append(('resources', new_resources, desc('Resources', ", ".join(new_resources), ", ".join(old_resources))))
|
||||||
url = GroupURL(url=m.group('url'), name=m.group('name'), group=group)
|
|
||||||
else:
|
|
||||||
url = GroupURL(url=m.group('url'), name='', group=group)
|
|
||||||
url.save()
|
|
||||||
|
|
||||||
group.time = datetime.datetime.now()
|
group.time = datetime.datetime.now()
|
||||||
|
|
||||||
|
@ -1064,7 +1066,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_email=group.list_email if group.list_email else None,
|
||||||
list_subscribe=group.list_subscribe if group.list_subscribe else None,
|
list_subscribe=group.list_subscribe if group.list_subscribe else None,
|
||||||
list_archive=group.list_archive if group.list_archive 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,
|
closing_note = closing_note,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -664,6 +664,45 @@ class IetfAuthTests(TestCase):
|
||||||
self.assertIn(" %s times" % count, body)
|
self.assertIn(" %s times" % count, body)
|
||||||
self.assertIn(date, 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'
|
||||||
|
)
|
||||||
|
|
||||||
|
for line in badlines:
|
||||||
|
r = self.client.post(url, dict(resources=line, submit="1"))
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
self.assertTrue(q('.alert-danger'))
|
||||||
|
|
||||||
|
goodlines = """
|
||||||
|
github_repo https://github.com/some/repo Some display text
|
||||||
|
github_notify notify@example.com
|
||||||
|
github_username githubuser
|
||||||
|
website http://example.com/http/is/fine
|
||||||
|
"""
|
||||||
|
|
||||||
|
r = self.client.post(url, dict(resources=goodlines, submit="1"))
|
||||||
|
self.assertEqual(r.status_code,302)
|
||||||
|
self.assertEqual(person.personextresource_set.count(), 4)
|
||||||
|
self.assertEqual(person.personextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
|
||||||
|
|
||||||
|
|
||||||
class OpenIDConnectTests(TestCase):
|
class OpenIDConnectTests(TestCase):
|
||||||
def request_matcher(self, request):
|
def request_matcher(self, request):
|
||||||
|
@ -797,3 +836,4 @@ class OpenIDConnectTests(TestCase):
|
||||||
# handler, causing later logging to become visible even if that wasn't intended.
|
# handler, causing later logging to become visible even if that wasn't intended.
|
||||||
# Fail here if that happens.
|
# Fail here if that happens.
|
||||||
self.assertEqual(logging.root.handlers, [])
|
self.assertEqual(logging.root.handlers, [])
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ urlpatterns = [
|
||||||
url(r'^logout/$', LogoutView.as_view(), name="django.contrib.auth.views.logout"),
|
url(r'^logout/$', LogoutView.as_view(), name="django.contrib.auth.views.logout"),
|
||||||
url(r'^password/$', views.change_password),
|
url(r'^password/$', views.change_password),
|
||||||
url(r'^profile/$', views.profile),
|
url(r'^profile/$', views.profile),
|
||||||
|
url(r'^editexternalresources/$', views.edit_person_externalresources),
|
||||||
url(r'^reset/$', views.password_reset),
|
url(r'^reset/$', views.password_reset),
|
||||||
url(r'^reset/confirm/(?P<auth>[^/]+)/$', views.confirm_password_reset),
|
url(r'^reset/confirm/(?P<auth>[^/]+)/$', views.confirm_password_reset),
|
||||||
url(r'^review/$', views.review_overview),
|
url(r'^review/$', views.review_overview),
|
||||||
|
|
|
@ -67,12 +67,15 @@ from ietf.ietfauth.forms import ( RegistrationForm, PasswordForm, ResetPasswordF
|
||||||
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
from ietf.ietfauth.htpasswd import update_htpasswd_file
|
||||||
from ietf.ietfauth.utils import role_required, has_role
|
from ietf.ietfauth.utils import role_required, has_role
|
||||||
from ietf.mailinglists.models import Subscribed, Whitelisted
|
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.person.models import Person, Email, Alias, PersonalApiKey, PERSON_API_KEY_VALUES
|
||||||
from ietf.review.models import ReviewerSettings, ReviewWish, ReviewAssignment
|
from ietf.review.models import ReviewerSettings, ReviewWish, ReviewAssignment
|
||||||
from ietf.review.utils import unavailable_periods_to_list, get_default_filter_re
|
from ietf.review.utils import unavailable_periods_to_list, get_default_filter_re
|
||||||
from ietf.doc.fields import SearchableDocumentField
|
from ietf.doc.fields import SearchableDocumentField
|
||||||
from ietf.utils.decorators import person_required
|
from ietf.utils.decorators import person_required
|
||||||
from ietf.utils.mail import send_mail
|
from ietf.utils.mail import send_mail
|
||||||
|
from ietf.utils.validators import validate_external_resource_value
|
||||||
|
|
||||||
|
|
||||||
def index(request):
|
def index(request):
|
||||||
return render(request, 'registration/index.html')
|
return render(request, 'registration/index.html')
|
||||||
|
@ -288,6 +291,79 @@ def profile(request):
|
||||||
'settings':settings,
|
'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:<br><br> %s" % ', '.join([ o.slug for o in ExtResourceName.objects.all().order_by('slug') ])
|
||||||
|
# May need to explain the tags more - probably more reason to move to a formset.
|
||||||
|
title = "Additional person resources"
|
||||||
|
return render(request, 'ietfauth/edit_field.html',dict(person=person, form=form, title=title, info=info) )
|
||||||
|
|
||||||
def confirm_new_email(request, auth):
|
def confirm_new_email(request, auth):
|
||||||
try:
|
try:
|
||||||
username, email = django.core.signing.loads(auth, salt="add_email", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
|
username, email = django.core.signing.loads(auth, salt="add_email", max_age=settings.DAYS_TO_EXPIRE_REGISTRATION_LINK * 24 * 60 * 60)
|
||||||
|
|
|
@ -10,7 +10,9 @@ from ietf.name.models import (
|
||||||
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
|
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
|
||||||
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
|
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
|
||||||
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
|
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
|
||||||
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName)
|
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName,
|
||||||
|
ExtResourceName, ExtResourceTypeName, )
|
||||||
|
|
||||||
|
|
||||||
from ietf.stats.models import CountryAlias
|
from ietf.stats.models import CountryAlias
|
||||||
|
|
||||||
|
@ -46,6 +48,10 @@ class ImportantDateNameAdmin(NameAdmin):
|
||||||
ordering = ('-used','default_offset_days',)
|
ordering = ('-used','default_offset_days',)
|
||||||
admin.site.register(ImportantDateName,ImportantDateNameAdmin)
|
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(AgendaTypeName, NameAdmin)
|
||||||
admin.site.register(BallotPositionName, NameAdmin)
|
admin.site.register(BallotPositionName, NameAdmin)
|
||||||
admin.site.register(ConstraintName, NameAdmin)
|
admin.site.register(ConstraintName, NameAdmin)
|
||||||
|
@ -82,3 +88,4 @@ admin.site.register(TimeSlotTypeName, NameAdmin)
|
||||||
admin.site.register(TimerangeName, NameAdmin)
|
admin.site.register(TimerangeName, NameAdmin)
|
||||||
admin.site.register(TopicAudienceName, NameAdmin)
|
admin.site.register(TopicAudienceName, NameAdmin)
|
||||||
admin.site.register(DocUrlTagName, NameAdmin)
|
admin.site.register(DocUrlTagName, NameAdmin)
|
||||||
|
admin.site.register(ExtResourceTypeName, NameAdmin)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
51
ietf/name/migrations/0013_extres.py
Normal file
51
ietf/name/migrations/0013_extres.py
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.29 on 2020-03-19 13:56
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ietf.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0012_role_name_robots'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExtResourceName',
|
||||||
|
fields=[
|
||||||
|
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('desc', models.TextField(blank=True)),
|
||||||
|
('used', models.BooleanField(default=True)),
|
||||||
|
('order', models.IntegerField(default=0)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['order', 'name'],
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExtResourceTypeName',
|
||||||
|
fields=[
|
||||||
|
('slug', models.CharField(max_length=32, primary_key=True, serialize=False)),
|
||||||
|
('name', models.CharField(max_length=255)),
|
||||||
|
('desc', models.TextField(blank=True)),
|
||||||
|
('used', models.BooleanField(default=True)),
|
||||||
|
('order', models.IntegerField(default=0)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'ordering': ['order', 'name'],
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='extresourcename',
|
||||||
|
name='type',
|
||||||
|
field=ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ExtResourceTypeName'),
|
||||||
|
),
|
||||||
|
]
|
65
ietf/name/migrations/0014_populate_extres.py
Normal file
65
ietf/name/migrations/0014_populate_extres.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.29 on 2020-03-19 11:42
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
ExtResourceName = apps.get_model('name','ExtResourceName')
|
||||||
|
ExtResourceTypeName = apps.get_model('name','ExtResourceTypeName')
|
||||||
|
|
||||||
|
ExtResourceTypeName.objects.create(slug='email', name="Email address", desc="Email address", used=True, order=0)
|
||||||
|
ExtResourceTypeName.objects.create(slug='url', name="URL", desc="URL", used=True, order=0)
|
||||||
|
ExtResourceTypeName.objects.create(slug='string', name="string", desc="string", used=True, order=0)
|
||||||
|
|
||||||
|
resourcename = namedtuple('resourcename', ['slug', 'name', 'type'])
|
||||||
|
resourcenames= [
|
||||||
|
resourcename("webpage", "Additional Web Page", "url"),
|
||||||
|
resourcename("faq", "Frequently Asked Questions", "url"),
|
||||||
|
resourcename("github_username","GitHub Username", "string"),
|
||||||
|
resourcename("github_org","GitHub Organization", "url"),
|
||||||
|
resourcename("github_repo","GitHub Repository", "url"),
|
||||||
|
resourcename("gitlab_username","GitLab Username", "string"),
|
||||||
|
resourcename("tracker","Issuer Tracker", "url"),
|
||||||
|
resourcename("github_notify","GitHub Notifications Email", "email"),
|
||||||
|
resourcename("slack","Slack Channel", "url"),
|
||||||
|
resourcename("website","Website", "url"),
|
||||||
|
resourcename("wiki","Wiki", "url"),
|
||||||
|
resourcename("yc_entry","Yang Catalog Entry", "url"),
|
||||||
|
resourcename("yc_impact","Yang Impact Analysis", "url"),
|
||||||
|
resourcename("jabber_room","Jabber Room", "url"),
|
||||||
|
resourcename("jabber_log","Jabber Log", "url"),
|
||||||
|
resourcename("mailing_list","Mailing List", "url"),
|
||||||
|
resourcename("mailing_list_archive","Mailing List Archive","url"),
|
||||||
|
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', '0013_extres'),
|
||||||
|
('group', '0033_extres'),
|
||||||
|
('doc', '0033_extres'),
|
||||||
|
('person', '0013_extres'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse)
|
||||||
|
]
|
|
@ -125,4 +125,8 @@ class ImportantDateName(NameModel):
|
||||||
default_offset_days = models.SmallIntegerField()
|
default_offset_days = models.SmallIntegerField()
|
||||||
class DocUrlTagName(NameModel):
|
class DocUrlTagName(NameModel):
|
||||||
"Repository, Wiki, Issue Tracker, ..."
|
"Repository, Wiki, Issue Tracker, ..."
|
||||||
|
class ExtResourceTypeName(NameModel):
|
||||||
|
"""Url, Email, String"""
|
||||||
|
class ExtResourceName(NameModel):
|
||||||
|
"""GitHub Repository URL, GitHub Username, ..."""
|
||||||
|
type = ForeignKey(ExtResourceTypeName)
|
||||||
|
|
|
@ -17,7 +17,7 @@ from ietf.name.models import ( AgendaTypeName, BallotPositionName, ConstraintNam
|
||||||
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
|
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
|
||||||
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
|
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
|
||||||
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
|
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
|
||||||
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName)
|
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName)
|
||||||
|
|
||||||
class TimeSlotTypeNameResource(ModelResource):
|
class TimeSlotTypeNameResource(ModelResource):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -615,3 +615,38 @@ class TimerangeNameResource(ModelResource):
|
||||||
"order": ALL,
|
"order": ALL,
|
||||||
}
|
}
|
||||||
api.name.register(TimerangeNameResource())
|
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())
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
import simple_history
|
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.person.name import name_parts
|
||||||
|
|
||||||
|
from ietf.utils.validators import validate_external_resource_value
|
||||||
|
|
||||||
|
|
||||||
class EmailAdmin(simple_history.admin.SimpleHistoryAdmin):
|
class EmailAdmin(simple_history.admin.SimpleHistoryAdmin):
|
||||||
list_display = ["address", "person", "time", "active", "origin"]
|
list_display = ["address", "person", "time", "active", "origin"]
|
||||||
raw_id_fields = ["person", ]
|
raw_id_fields = ["person", ]
|
||||||
|
@ -55,3 +60,14 @@ class PersonApiKeyEventAdmin(admin.ModelAdmin):
|
||||||
admin.site.register(PersonApiKeyEvent, PersonApiKeyEventAdmin)
|
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)
|
||||||
|
|
28
ietf/person/migrations/0013_extres.py
Normal file
28
ietf/person/migrations/0013_extres.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.11.29 on 2020-04-15 10:20
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ietf.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('name', '0013_extres'),
|
||||||
|
('person', '0012_auto_20200624_1332'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='PersonExtResource',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('display_name', models.CharField(blank=True, default='', max_length=255)),
|
||||||
|
('value', models.CharField(max_length=2083)),
|
||||||
|
('name', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='name.ExtResourceName')),
|
||||||
|
('person', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -24,6 +24,7 @@ from simple_history.models import HistoricalRecords
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
|
from ietf.name.models import ExtResourceName
|
||||||
from ietf.person.name import name_parts, initials, plain_name
|
from ietf.person.name import name_parts, initials, plain_name
|
||||||
from ietf.utils.mail import send_mail_preformatted
|
from ietf.utils.mail import send_mail_preformatted
|
||||||
from ietf.utils.storage import NoLocationMigrationFileSystemStorage
|
from ietf.utils.storage import NoLocationMigrationFileSystemStorage
|
||||||
|
@ -240,6 +241,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) ]
|
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):
|
class Alias(models.Model):
|
||||||
"""This is used for alternative forms of a name. This is the
|
"""This is used for alternative forms of a name. This is the
|
||||||
primary lookup point for names, and should always contain the
|
primary lookup point for names, and should always contain the
|
||||||
|
|
|
@ -10,7 +10,7 @@ from tastypie.cache import SimpleCache
|
||||||
|
|
||||||
from ietf import api
|
from ietf 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
|
from ietf.utils.resources import UserResource
|
||||||
|
@ -182,3 +182,23 @@ class HistoricalEmailResource(ModelResource):
|
||||||
"history_user": ALL_WITH_RELATIONS,
|
"history_user": ALL_WITH_RELATIONS,
|
||||||
}
|
}
|
||||||
api.person.register(HistoricalEmailResource())
|
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())
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from ietf.person.models import Person, Email
|
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
|
import re
|
||||||
|
|
||||||
STATE_CHOICES = (
|
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):
|
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.")
|
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.")
|
||||||
|
|
|
@ -32,19 +32,3 @@ class SecrAreasTestCase(TestCase):
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
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)
|
|
|
@ -4,11 +4,9 @@ from ietf.utils.urls import url
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.list_areas),
|
url(r'^$', views.list_areas),
|
||||||
url(r'^add/$', views.add),
|
|
||||||
url(r'^getemails', views.getemails),
|
url(r'^getemails', views.getemails),
|
||||||
url(r'^getpeople', views.getpeople),
|
url(r'^getpeople', views.getpeople),
|
||||||
url(r'^(?P<name>[A-Za-z0-9.-]+)/$', views.view),
|
url(r'^(?P<name>[A-Za-z0-9.-]+)/$', views.view),
|
||||||
url(r'^(?P<name>[A-Za-z0-9.-]+)/edit/$', views.edit),
|
|
||||||
url(r'^(?P<name>[A-Za-z0-9.-]+)/people/$', views.people),
|
url(r'^(?P<name>[A-Za-z0-9.-]+)/people/$', views.people),
|
||||||
url(r'^(?P<name>[A-Za-z0-9.-]+)/people/modify/$', views.modify),
|
url(r'^(?P<name>[A-Za-z0-9.-]+)/people/modify/$', views.modify),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,17 +1,14 @@
|
||||||
import datetime
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib import messages
|
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.http import HttpResponse
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
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.group.utils import save_group_in_history
|
||||||
from ietf.ietfauth.utils import role_required
|
from ietf.ietfauth.utils import role_required
|
||||||
from ietf.person.models import Person
|
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
|
# AJAX FUNCTIONS
|
||||||
|
@ -49,114 +46,7 @@ def getemails(request):
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# STANDARD VIEW FUNCTIONS
|
# 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')
|
@role_required('Secretariat')
|
||||||
def list_areas(request):
|
def list_areas(request):
|
||||||
|
|
|
@ -6,7 +6,6 @@ from django.db.models import Count
|
||||||
from ietf.group.models import Group, Role
|
from ietf.group.models import Group, Role
|
||||||
from ietf.name.models import GroupStateName, GroupTypeName, RoleName
|
from ietf.name.models import GroupStateName, GroupTypeName, RoleName
|
||||||
from ietf.person.models import Person, Email
|
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)
|
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):
|
class RoleForm(forms.Form):
|
||||||
name = forms.ModelChoiceField(RoleName.objects.filter(slug__in=('chair','editor','secr','techadv')),empty_label=None)
|
name = forms.ModelChoiceField(RoleName.objects.filter(slug__in=('chair','editor','secr','techadv')),empty_label=None)
|
||||||
|
|
|
@ -9,7 +9,6 @@ from ietf.secr.groups.forms import get_parent_group_choices
|
||||||
from ietf.group.factories import GroupFactory, RoleFactory
|
from ietf.group.factories import GroupFactory, RoleFactory
|
||||||
from ietf.meeting.factories import MeetingFactory
|
from ietf.meeting.factories import MeetingFactory
|
||||||
from ietf.person.factories import PersonFactory
|
from ietf.person.factories import PersonFactory
|
||||||
from ietf.person.models import Person
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
class GroupsTest(TestCase):
|
class GroupsTest(TestCase):
|
||||||
|
@ -31,73 +30,6 @@ class GroupsTest(TestCase):
|
||||||
response = self.client.post(url,post_data,follow=True)
|
response = self.client.post(url,post_data,follow=True)
|
||||||
self.assertContains(response, group.acronym)
|
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 -------- #
|
# ------- Test View -------- #
|
||||||
def test_view(self):
|
def test_view(self):
|
||||||
MeetingFactory(type_id='ietf')
|
MeetingFactory(type_id='ietf')
|
||||||
|
@ -107,47 +39,6 @@ class GroupsTest(TestCase):
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 200)
|
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 -------- #
|
# ------- Test People -------- #
|
||||||
def test_people_delete(self):
|
def test_people_delete(self):
|
||||||
|
|
|
@ -5,12 +5,10 @@ from ietf.utils.urls import url
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^$', views.search),
|
url(r'^$', views.search),
|
||||||
url(r'^add/$', views.add),
|
|
||||||
url(r'^blue-dot-report/$', views.blue_dot),
|
url(r'^blue-dot-report/$', views.blue_dot),
|
||||||
#(r'^ajax/get_ads/$', views.get_ads),
|
#(r'^ajax/get_ads/$', views.get_ads),
|
||||||
url(r'^%(acronym)s/$' % settings.URL_REGEXPS, views.view),
|
url(r'^%(acronym)s/$' % settings.URL_REGEXPS, views.view),
|
||||||
url(r'^%(acronym)s/delete/(?P<id>\d{1,6})/$' % settings.URL_REGEXPS, views.delete_role),
|
url(r'^%(acronym)s/delete/(?P<id>\d{1,6})/$' % settings.URL_REGEXPS, views.delete_role),
|
||||||
url(r'^%(acronym)s/charter/$' % settings.URL_REGEXPS, views.charter),
|
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),
|
url(r'^%(acronym)s/people/$' % settings.URL_REGEXPS, views.people),
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.forms.models import inlineformset_factory
|
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
|
||||||
from ietf.group.models import Group, ChangeStateGroupEvent, GroupEvent, GroupURL, Role
|
from ietf.group.models import Group, GroupEvent, Role
|
||||||
from ietf.group.utils import save_group_in_history, get_charter_text, setup_default_community_list_for_group
|
from ietf.group.utils import save_group_in_history, get_charter_text
|
||||||
from ietf.ietfauth.utils import role_required
|
from ietf.ietfauth.utils import role_required
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.secr.groups.forms import GroupModelForm, RoleForm, SearchForm
|
from ietf.secr.groups.forms import RoleForm, SearchForm
|
||||||
from ietf.secr.areas.forms import AWPForm
|
|
||||||
from ietf.secr.utils.meeting import get_current_meeting
|
from ietf.secr.utils.meeting import get_current_meeting
|
||||||
|
|
||||||
# -------------------------------------------------
|
# -------------------------------------------------
|
||||||
|
@ -71,58 +69,7 @@ def get_ads(request):
|
||||||
# Standard View Functions
|
# 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')
|
@role_required('Secretariat')
|
||||||
def blue_dot(request):
|
def blue_dot(request):
|
||||||
|
@ -202,83 +149,6 @@ def delete_role(request, acronym, id):
|
||||||
return render(request, 'confirm_delete.html', {'object': role})
|
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')
|
@role_required('Secretariat')
|
||||||
def people(request, acronym):
|
def people(request, acronym):
|
||||||
"""
|
"""
|
||||||
|
@ -343,8 +213,6 @@ def search(request):
|
||||||
results = []
|
results = []
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = SearchForm(request.POST)
|
form = SearchForm(request.POST)
|
||||||
if request.POST['submit'] == 'Add':
|
|
||||||
return redirect('ietf.secr.groups.views.add')
|
|
||||||
|
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
{% extends "base_site.html" %}
|
|
||||||
{% load staticfiles %}
|
|
||||||
{% block title %}Areas - Add{% endblock %}
|
|
||||||
|
|
||||||
{% block extrahead %}{{ block.super }}
|
|
||||||
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}{{ block.super }}
|
|
||||||
» <a href="../">Areas</a>
|
|
||||||
» Add
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="module">
|
|
||||||
<h2>Area - Add</h2>
|
|
||||||
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
|
|
||||||
<table id="area-add-table" class="full-width amstable">
|
|
||||||
<col width="150">
|
|
||||||
{{ area_form.as_table }}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% include "includes/awp_add_form.html" %}
|
|
||||||
|
|
||||||
{% include "includes/buttons_submit.html" %}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div> <!-- module -->
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,32 +0,0 @@
|
||||||
{% extends "base_site.html" %}
|
|
||||||
{% load staticfiles %}
|
|
||||||
{% block title %}Areas - Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block extrahead %}{{ block.super }}
|
|
||||||
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}{{ block.super }}
|
|
||||||
» <a href="../../">Areas</a>
|
|
||||||
» <a href="../">{{ area.acronym }}</a>
|
|
||||||
» Edit
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="module">
|
|
||||||
<form action="." method="post">{% csrf_token %}
|
|
||||||
<h2>Area - Edit</h2>
|
|
||||||
<table class="full-width amstable">
|
|
||||||
<col width="150">
|
|
||||||
{{ form.as_table }}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% include "includes/awp_edit_form.html" %}
|
|
||||||
|
|
||||||
{% include "includes/buttons_save_cancel.html" %}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div> <!-- module -->
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -13,7 +13,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="module">
|
<div class="module">
|
||||||
<h2>Areas <span class="unlocked"><a href="add/" class="addlink">Add</a></span></h2>
|
<h2>Areas</h2>
|
||||||
<table id="areas-list-table" class="full-width">
|
<table id="areas-list-table" class="full-width">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
{% extends "base_site.html" %}
|
|
||||||
{% load staticfiles %}
|
|
||||||
|
|
||||||
{% block title %}Groups - Add{% endblock %}
|
|
||||||
|
|
||||||
{% block extrahead %}{{ block.super }}
|
|
||||||
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}{{ block.super }}
|
|
||||||
» <a href="../">Groups</a>
|
|
||||||
» Add
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="module group-container">
|
|
||||||
<h2>Groups - Add</h2>
|
|
||||||
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
|
|
||||||
<table id="groups-table" class="full-width amstable">
|
|
||||||
<col width="150">
|
|
||||||
{{ form.as_table }}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% include "includes/awp_add_form.html" %}
|
|
||||||
|
|
||||||
{% include "includes/buttons_save_cancel.html" %}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div> <!-- module -->
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -1,33 +0,0 @@
|
||||||
{% extends "base_site.html" %}
|
|
||||||
{% load staticfiles %}
|
|
||||||
|
|
||||||
{% block title %}Groups - Edit{% endblock %}
|
|
||||||
|
|
||||||
{% block extrahead %}{{ block.super }}
|
|
||||||
<script type="text/javascript" src="{% static 'secr/js/utils.js' %}"></script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block breadcrumbs %}{{ block.super }}
|
|
||||||
» <a href="../../">Groups</a>
|
|
||||||
» <a href="../">{{ group.acronym }}</a>
|
|
||||||
» Edit
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
|
|
||||||
<div class="module group-container">
|
|
||||||
<h2>Groups - Edit</h2>
|
|
||||||
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
|
|
||||||
<table id="groups-table" class="full-width amstable">
|
|
||||||
<col width="150">
|
|
||||||
{{ form }}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
{% include "includes/awp_edit_form.html" %}
|
|
||||||
|
|
||||||
{% include "includes/buttons_save_cancel.html" %}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div> <!-- module -->
|
|
||||||
|
|
||||||
{% endblock %}
|
|
|
@ -14,7 +14,7 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div class="module group-container">
|
<div class="module group-container">
|
||||||
<h2>Groups - Search <span class="unlocked"><a href="add/" class="addlink">Add</a></span></h2>
|
<h2>Groups - Search</h2>
|
||||||
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
|
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
|
||||||
<table class="full-width amstable">
|
<table class="full-width amstable">
|
||||||
{{ form.as_table }}
|
{{ form.as_table }}
|
||||||
|
|
|
@ -47,7 +47,6 @@
|
||||||
<form id="roles-form".>
|
<form id="roles-form".>
|
||||||
{{ group_form.as_p }}
|
{{ group_form.as_p }}
|
||||||
</form>
|
</form>
|
||||||
<p><a href="{% url 'ietf.secr.groups.views.add' %}">Add a new group...</a></p>
|
|
||||||
</div> <!-- inline-related -->
|
</div> <!-- inline-related -->
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|
|
@ -377,19 +377,19 @@ def post_submission(request, submission, approved_doc_desc, approved_subm_desc):
|
||||||
if 'yang' in code:
|
if 'yang' in code:
|
||||||
modules = code['yang']
|
modules = code['yang']
|
||||||
# Yang impact analysis URL
|
# Yang impact analysis URL
|
||||||
draft.documenturl_set.filter(tag_id='yang-impact-analysis').delete()
|
draft.docextresource_set.filter(name_id='yc_impact').delete()
|
||||||
f = settings.SUBMIT_YANG_CATALOG_MODULEARG
|
f = settings.SUBMIT_YANG_CATALOG_MODULEARG
|
||||||
moduleargs = '&'.join([ f.format(module=m) for m in modules])
|
moduleargs = '&'.join([ f.format(module=m) for m in modules])
|
||||||
url = settings.SUBMIT_YANG_CATALOG_IMPACT_URL.format(moduleargs=moduleargs, draft=draft.name)
|
url = settings.SUBMIT_YANG_CATALOG_IMPACT_URL.format(moduleargs=moduleargs, draft=draft.name)
|
||||||
desc = settings.SUBMIT_YANG_CATALOG_IMPACT_DESC.format(modules=','.join(modules), draft=draft.name)
|
desc = settings.SUBMIT_YANG_CATALOG_IMPACT_DESC.format(modules=','.join(modules), draft=draft.name)
|
||||||
draft.documenturl_set.create(url=url, tag_id='yang-impact-analysis', desc=desc)
|
draft.docextresource_set.create(value=url, name_id='yang-impact-analysis', display_name=desc)
|
||||||
# Yang module metadata URLs
|
# Yang module metadata URLs
|
||||||
old_urls = draft.documenturl_set.filter(tag_id='yang-module-metadata')
|
old_urls = draft.documenturl_set.filter(tag_id='yc_entry')
|
||||||
old_urls.delete()
|
old_urls.delete()
|
||||||
for module in modules:
|
for module in modules:
|
||||||
url = settings.SUBMIT_YANG_CATALOG_MODULE_URL.format(module=module)
|
url = settings.SUBMIT_YANG_CATALOG_MODULE_URL.format(module=module)
|
||||||
desc = settings.SUBMIT_YANG_CATALOG_MODULE_DESC.format(module=module)
|
desc = settings.SUBMIT_YANG_CATALOG_MODULE_DESC.format(module=module)
|
||||||
draft.documenturl_set.create(url=url, tag_id='yang-module-metadata', desc=desc)
|
draft.docextresource_set.create(value=url, name_id='yc_entry', display_name=desc)
|
||||||
|
|
||||||
if not draft.get_state('draft-iesg'):
|
if not draft.get_state('draft-iesg'):
|
||||||
draft.states.add(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
draft.states.add(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
||||||
|
|
|
@ -245,31 +245,36 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% with doc.documenturl_set.all as urls %}
|
{% with doc.docextresource_set.all as resources %}
|
||||||
{% if urls or can_edit_stream_info or can_edit_individual %}
|
{% if resources or can_edit_stream_info or can_edit_individual %}
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<th>Additional URLs</th>
|
<th>Additional Resources</th>
|
||||||
<td class="edit">
|
<td class="edit">
|
||||||
{% if can_edit_stream_info or can_edit_individual %}
|
{% if can_edit_stream_info or can_edit_individual %}
|
||||||
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_draft.edit_document_urls' name=doc.name %}">Edit</a>
|
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_draft.edit_doc_extresources' name=doc.name %}">Edit</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if urls or doc.group and doc.group.list_archive %}
|
{% if resources or doc.group and doc.group.list_archive %}
|
||||||
<table class="col-md-12 col-sm-12 col-xs-12">
|
<table class="col-md-12 col-sm-12 col-xs-12">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for url in urls|dictsort:"desc" %}
|
{% for resource in resources|dictsort:"display_name" %}
|
||||||
<tr><td> - <a href="{{ url.url }}">{% firstof url.desc url.tag.name %}</a></td></tr>
|
{% if resource.name.type.slug == 'url' or resource.name.type.slug == 'email' %}
|
||||||
{% endfor %}
|
<tr><td> - <a href="{{ resource.value }}" title="{{resource.name.name}}">{% firstof resource.display_name resource.name.name %}</a></td></tr>
|
||||||
{% if doc.group and doc.group.list_archive %}
|
{# Maybe make how a resource displays itself a method on the class so templates aren't doing this switching #}
|
||||||
<tr><td> - <a href="{{doc.group.list_archive}}?q={{doc.name}}">Mailing list discussion</a><td></tr>
|
{% else %}
|
||||||
{% endif %}
|
<tr><td> - <span title="{{resource.name.name}}">{% firstof resource.display_name resource.name.name %}: {{resource.value}}</span></td></tr>
|
||||||
</tbody>
|
{% endif %}
|
||||||
</table>
|
{% endfor %}
|
||||||
{% endif %}
|
{% if doc.group and doc.group.list_archive %}
|
||||||
</td>
|
<tr><td> - <a href="{{doc.group.list_archive}}?q={{doc.name}}">Mailing list discussion</a><td></tr>
|
||||||
</tr>
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
|
|
|
@ -114,30 +114,35 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% with group.groupurl_set.all as urls %}
|
{% with group.groupextresource_set.all as resources %}
|
||||||
{% if urls or can_edit_group %}
|
{% if resources or can_edit_group %}
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
<th>Additional URLs</th>
|
<th>Additional Resources</th>
|
||||||
<td class="edit">
|
<td class="edit">
|
||||||
{% if can_edit_group %}
|
{% if can_edit_group %}
|
||||||
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='urls' %}">Edit</a>
|
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='resources' %}">Edit</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if urls %}
|
{% if resources %}
|
||||||
<table class="col-md-12 col-sm-12 col-xs-12">
|
<table class="col-md-12 col-sm-12 col-xs-12">
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for url in urls %}
|
{% for resource in resources|dictsort:"display_name" %}
|
||||||
<tr><td> - <a href="{{ url.url }}">{% firstof url.name url.url %}</a></td></tr>
|
{% if resource.name.type.slug == 'url' or resource.name.type.slug == 'email' %}
|
||||||
{% endfor %}
|
<tr><td> - <a href="{{ resource.value }}" title="{{resource.name.name}}">{% firstof resource.display_name resource.name.name %}</a></td></tr>
|
||||||
</tbody>
|
{# Maybe make how a resource displays itself a method on the class so templates aren't doing this switching #}
|
||||||
</table>
|
{% else %}
|
||||||
{% endif %}
|
<tr><td> - <span title="{{resource.name.name}}">{% firstof resource.display_name resource.name.name %}: {{resource.value}}</span></td></tr>
|
||||||
</td>
|
{% endif %}
|
||||||
</tr>
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
<tbody class="meta">
|
<tbody class="meta">
|
||||||
|
|
33
ietf/templates/ietfauth/edit_field.html
Normal file
33
ietf/templates/ietfauth/edit_field.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||||
|
{% load origin %}
|
||||||
|
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{{ title }} {{ person.plain_name }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% origin %}
|
||||||
|
<h1>{{ title }}<br><small>{{ person.plain_name }}</small></h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<b>
|
||||||
|
{{ info|safe }}
|
||||||
|
</b>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
|
||||||
|
{% buttons %}
|
||||||
|
<!-- Regarding placement of buttons: https://www.lukew.com/ff/entry.asp?571 -->
|
||||||
|
<button type="submit" class="btn btn-primary" name="submit" value="Save">Submit</button>
|
||||||
|
<a class="btn btn-default pull-right" href="{% url "ietf.ietfauth.views.profile" %}">Back</a>
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
|
@ -45,7 +45,7 @@
|
||||||
|
|
||||||
{% for fieldset in form.fieldsets %}
|
{% for fieldset in form.fieldsets %}
|
||||||
{% if forloop.first and user|has_role:"Secretariat" %}
|
{% if forloop.first and user|has_role:"Secretariat" %}
|
||||||
<h2><div class="col-md-2">{{ fieldset.name }}</div><div class="col-md-10"><a class="small" target="_blank" href="{% url 'ietf.secr.groups.views.add' %}">Add new group >></a></div></h2>
|
<h2><div class="col-md-2">{{ fieldset.name }}</div><div class="col-md-10"></div></h2>
|
||||||
{% else %}
|
{% else %}
|
||||||
<h2>{{ fieldset.name }}</h2>
|
<h2>{{ fieldset.name }}</h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -57,7 +57,19 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if person.personextresource_set.exists %}
|
||||||
|
<div class="col-md-12">
|
||||||
|
<h2 id="extresources">External Resources</h2>
|
||||||
|
<table class="table">
|
||||||
|
{% for extres in person.personextresource_set.all %}
|
||||||
|
<tr>
|
||||||
|
<td class="col-md-1"><span title="{{ extres.name.name }}">{% firstof extres.display_name extres.name.name %}</span></td>
|
||||||
|
<td class="col-md-11">{{extres.value}}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h2 id="rfcs">RFCs</h2>
|
<h2 id="rfcs">RFCs</h2>
|
||||||
|
|
|
@ -94,7 +94,7 @@
|
||||||
<label class="col-sm-2 control-label">Nomcom Eligible</label>
|
<label class="col-sm-2 control-label">Nomcom Eligible</label>
|
||||||
<div class="col-sm-1 form-control-static">{{person|is_nomcom_eligible|yesno:'Yes,No,No'}}</div>
|
<div class="col-sm-1 form-control-static">{{person|is_nomcom_eligible|yesno:'Yes,No,No'}}</div>
|
||||||
<div class="col-sm-9">
|
<div class="col-sm-9">
|
||||||
<p class="alert alert-info form-control-static ">
|
<p class="alert alert-info form-control-static">
|
||||||
This calculation is EXPERIMENTAL.<br/>
|
This calculation is EXPERIMENTAL.<br/>
|
||||||
|
|
||||||
If you believe it is incorrect, make sure you've added all the
|
If you believe it is incorrect, make sure you've added all the
|
||||||
|
@ -111,6 +111,22 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="col-sm-2 control-label">External Resources</label>
|
||||||
|
<div class="col-sm-10 form-control-static">
|
||||||
|
{% for extres in person.personextresource_set.all %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-1"><span title="{{ extres.name.name}}">{% firstof extres.display_name extres.name.name %}</span></div>
|
||||||
|
<div class="col-sm-11">{{extres.value}}
|
||||||
|
{% if forloop.first %} <a href="{% url 'ietf.ietfauth.views.edit_person_externalresources' %}"><span class="fa fa-pencil"></span></a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<div class="row"><div class="col-sm-1">None <a href="{% url 'ietf.ietfauth.views.edit_person_externalresources' %}"><span class="fa fa-pencil"></span></a></div></div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-sm-2 control-label">Email addresses</label>
|
<label class="col-sm-2 control-label">Email addresses</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
|
@ -5,12 +5,16 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from pyquery import PyQuery
|
from pyquery import PyQuery
|
||||||
|
from urllib.parse import urlparse, urlsplit, urlunsplit
|
||||||
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator, URLValidator, EmailValidator, _lazy_re_compile
|
||||||
from django.template.defaultfilters import filesizeformat
|
from django.template.defaultfilters import filesizeformat
|
||||||
from django.utils.deconstruct import deconstructible
|
from django.utils.deconstruct import deconstructible
|
||||||
|
from django.utils.ipv6 import is_valid_ipv6_address
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
|
@ -83,3 +87,106 @@ def validate_no_html_frame(file):
|
||||||
q = PyQuery(file.read())
|
q = PyQuery(file.read())
|
||||||
if q("frameset") or q("frame") or q("iframe"):
|
if q("frameset") or q("frame") or q("iframe"):
|
||||||
raise ValidationError('Found content with html frames. Please upload a file that does not use frames')
|
raise ValidationError('Found content with html frames. Please upload a file that does not use frames')
|
||||||
|
|
||||||
|
# instantiations of sub-validiators used by the external_resource validator
|
||||||
|
|
||||||
|
validate_url = URLValidator()
|
||||||
|
validate_http_url = URLValidator(schemes=['http','https'])
|
||||||
|
validate_email = EmailValidator()
|
||||||
|
|
||||||
|
def validate_ipv6_address(value):
|
||||||
|
if not is_valid_ipv6_address(value):
|
||||||
|
raise ValidationError(_('Enter a valid IPv6 address.'), code='invalid')
|
||||||
|
|
||||||
|
@deconstructible
|
||||||
|
class XMPPURLValidator(RegexValidator):
|
||||||
|
ul = '\u00a1-\uffff' # unicode letters range (must not be a raw string)
|
||||||
|
|
||||||
|
# IP patterns
|
||||||
|
ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
|
||||||
|
ipv6_re = r'\[[0-9a-f:\.]+\]' # (simple regex, validated later)
|
||||||
|
|
||||||
|
# Host patterns
|
||||||
|
hostname_re = r'[a-z' + ul + r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
|
||||||
|
# Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
|
||||||
|
domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*'
|
||||||
|
tld_re = (
|
||||||
|
r'\.' # dot
|
||||||
|
r'(?!-)' # can't start with a dash
|
||||||
|
r'(?:[a-z' + ul + '-]{2,63}' # domain label
|
||||||
|
r'|xn--[a-z0-9]{1,59})' # or punycode label
|
||||||
|
r'(?<!-)' # can't end with a dash
|
||||||
|
r'\.?' # may have a trailing dot
|
||||||
|
)
|
||||||
|
host_re = '(' + hostname_re + domain_re + tld_re + '|localhost)'
|
||||||
|
|
||||||
|
regex = _lazy_re_compile(
|
||||||
|
r'^(?:xmpp:)' # Note there is no '//'
|
||||||
|
r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?' # user:pass authentication
|
||||||
|
r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
|
||||||
|
r'(?::\d{2,5})?' # port
|
||||||
|
r'(?:[/?#][^\s]*)?' # resource path
|
||||||
|
r'\Z', re.IGNORECASE)
|
||||||
|
message = _('Enter a valid URL.')
|
||||||
|
schemes = ['http', 'https', 'ftp', 'ftps']
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
try:
|
||||||
|
super().__call__(value)
|
||||||
|
except ValidationError as e:
|
||||||
|
# Trivial case failed. Try for possible IDN domain
|
||||||
|
if value:
|
||||||
|
try:
|
||||||
|
scheme, netloc, path, query, fragment = urlsplit(value)
|
||||||
|
except ValueError: # for example, "Invalid IPv6 URL"
|
||||||
|
raise ValidationError(self.message, code=self.code)
|
||||||
|
try:
|
||||||
|
netloc = netloc.encode('idna').decode('ascii') # IDN -> ACE
|
||||||
|
except UnicodeError: # invalid domain part
|
||||||
|
raise e
|
||||||
|
url = urlunsplit((scheme, netloc, path, query, fragment))
|
||||||
|
super().__call__(url)
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
# Now verify IPv6 in the netloc part
|
||||||
|
host_match = re.search(r'^\[(.+)\](?::\d{2,5})?$', urlsplit(value).netloc)
|
||||||
|
if host_match:
|
||||||
|
potential_ip = host_match.groups()[0]
|
||||||
|
try:
|
||||||
|
validate_ipv6_address(potential_ip)
|
||||||
|
except ValidationError:
|
||||||
|
raise ValidationError(self.message, code=self.code)
|
||||||
|
|
||||||
|
# The maximum length of a full host name is 253 characters per RFC 1034
|
||||||
|
# section 3.1. It's defined to be 255 bytes or less, but this includes
|
||||||
|
# one byte for the length of the name and one byte for the trailing dot
|
||||||
|
# that's used to indicate absolute names in DNS.
|
||||||
|
if len(urlsplit(value).netloc) > 253:
|
||||||
|
raise ValidationError(self.message, code=self.code)
|
||||||
|
|
||||||
|
validate_xmpp = XMPPURLValidator()
|
||||||
|
|
||||||
|
def validate_external_resource_value(name, value):
|
||||||
|
""" validate a resource value using its name's properties """
|
||||||
|
|
||||||
|
if name.type.slug == 'url':
|
||||||
|
|
||||||
|
if name.slug in ( 'github_org', 'github_repo' ):
|
||||||
|
validate_http_url(value)
|
||||||
|
if urlparse(value).netloc.lower() != 'github.com':
|
||||||
|
raise ValidationError('URL must be a github url')
|
||||||
|
elif name.slug == 'jabber_room':
|
||||||
|
validate_xmpp(value)
|
||||||
|
else:
|
||||||
|
validate_url(value)
|
||||||
|
|
||||||
|
elif name.type.slug == 'email':
|
||||||
|
validate_email(value)
|
||||||
|
|
||||||
|
elif name.type.slug == 'string':
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValidationError('Unknown resource type '+name.type.name)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue