Added 'Additional URLs' for documents, the same way we have them for groups.

This could be used to point to a document source repository, to extracted
  yang module files, document wikis, and other relevant resources.
 - Legacy-Id: 14166
This commit is contained in:
Henrik Levkowetz 2017-09-27 10:52:32 +00:00
parent 34c32e1b71
commit 92d425fd9b
12 changed files with 10215 additions and 9966 deletions

View file

@ -7,7 +7,7 @@ from models import (StateType, State, RelatedDocument, DocumentAuthor, Document,
DocHistoryAuthor, DocHistory, DocAlias, DocReminder, DocEvent, NewRevisionDocEvent, DocHistoryAuthor, DocHistory, DocAlias, DocReminder, DocEvent, NewRevisionDocEvent,
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent, StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent, TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, ) AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL)
from ietf.doc.utils import get_state_types from ietf.doc.utils import get_state_types
@ -211,3 +211,7 @@ class BallotPositionDocEventAdmin(DocEventAdmin):
raw_id_fields = ["doc", "by", "ad", "ballot"] raw_id_fields = ["doc", "by", "ad", "ballot"]
admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin) admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin)
class DocumentUrlAdmin(admin.ModelAdmin):
list_display = ['id', 'doc', 'tag', 'url', 'desc', ]
raw_id_fields = ['doc', ]
admin.site.register(DocumentURL, DocumentUrlAdmin)

View file

@ -20,7 +20,8 @@ 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, FormalLanguageName ) DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, FormalLanguageName,
DocUrlTagName)
from ietf.person.models import Email, Person from ietf.person.models import Email, Person
from ietf.utils import log from ietf.utils import log
from ietf.utils.admin import admin_link from ietf.utils.admin import admin_link
@ -777,9 +778,14 @@ class Document(DocumentInfo):
stream=self.stream, group=self.group) stream=self.stream, group=self.group)
return dh return dh
class DocumentURL(models.Model):
doc = models.ForeignKey(Document)
tag = models.ForeignKey(DocUrlTagName)
desc = models.CharField(max_length=255, default='', blank=True)
url = models.URLField()
class RelatedDocHistory(models.Model): class RelatedDocHistory(models.Model):
source = models.ForeignKey('DocHistory') source = models.ForeignKey('DocHistory')
target = models.ForeignKey('DocAlias', related_name="reversely_related_document_history_set") target = models.ForeignKey('DocAlias', related_name="reversely_related_document_history_set")

View file

@ -12,7 +12,7 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
TelechatDocEvent, DocReminder, LastCallDocEvent, NewRevisionDocEvent, WriteupDocEvent, TelechatDocEvent, DocReminder, LastCallDocEvent, NewRevisionDocEvent, WriteupDocEvent,
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument, InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent, RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
ReviewRequestDocEvent, EditedAuthorsDocEvent) ReviewRequestDocEvent, EditedAuthorsDocEvent, DocumentURL)
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
class BallotTypeResource(ModelResource): class BallotTypeResource(ModelResource):
@ -629,3 +629,22 @@ class EditedAuthorsDocEventResource(ModelResource):
"docevent_ptr": ALL_WITH_RELATIONS, "docevent_ptr": ALL_WITH_RELATIONS,
} }
api.doc.register(EditedAuthorsDocEventResource()) api.doc.register(EditedAuthorsDocEventResource())
from ietf.name.resources import DocUrlTagNameResource
class DocumentURLResource(ModelResource):
doc = ToOneField(DocumentResource, 'doc')
tag = ToOneField(DocUrlTagNameResource, 'tag')
class Meta:
queryset = DocumentURL.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'documenturl'
filtering = {
"id": ALL,
"desc": ALL,
"url": ALL,
"doc": ALL_WITH_RELATIONS,
"tag": ALL_WITH_RELATIONS,
}
api.doc.register(DocumentURLResource())

View file

@ -765,6 +765,11 @@ class ExpireLastCallTests(TestCase):
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To']) self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To'])
class IndividualInfoFormsTests(TestCase): class IndividualInfoFormsTests(TestCase):
def setUp(self):
self.doc = make_test_data()
self.docname = self.doc.name
def test_doc_change_stream(self): def test_doc_change_stream(self):
url = urlreverse('ietf.doc.views_draft.change_stream', kwargs=dict(name=self.docname)) url = urlreverse('ietf.doc.views_draft.change_stream', kwargs=dict(name=self.docname))
login_testing_unauthorized(self, "secretary", url) login_testing_unauthorized(self, "secretary", url)
@ -1050,11 +1055,24 @@ 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 setUp(self): def test_doc_change_document_urls(self):
make_test_data() url = urlreverse('ietf.doc.views_draft.edit_document_urls', kwargs=dict(name=self.docname))
self.docname='draft-ietf-mars-test'
# get
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
q = PyQuery(r.content)
self.assertEqual(len(q('form textarea[id=id_urls]')),1)
# direct edit
r = self.client.post(url, dict(urls='wiki https://wiki.org/ Wiki\nrepository https://repository.org/ Repo\n', submit="1"))
self.assertEqual(r.status_code,302)
self.doc = Document.objects.get(name=self.docname) self.doc = Document.objects.get(name=self.docname)
self.assertTrue(self.doc.latest_event(DocEvent,type="changed_document").desc.startswith('Changed document URLs'))
self.assertIn('wiki https://wiki.org/', self.doc.latest_event(DocEvent,type="changed_document").desc)
self.assertIn('https://wiki.org/', [ u.url for u in self.doc.documenturl_set.all() ])
class SubmitToIesgTests(TestCase): class SubmitToIesgTests(TestCase):
def test_verify_permissions(self): def test_verify_permissions(self):

View file

@ -118,7 +118,8 @@ urlpatterns = [
url(r'^%(name)s/edit/approvaltext/$' % settings.URL_REGEXPS, views_ballot.ballot_approvaltext), url(r'^%(name)s/edit/approvaltext/$' % settings.URL_REGEXPS, views_ballot.ballot_approvaltext),
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/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'^help/state/(?P<type>[\w-]+)/$', views_help.state_help), url(r'^help/state/(?P<type>[\w-]+)/$', views_help.state_help),
url(r'^help/relationships/$', views_help.relationship_help), url(r'^help/relationships/$', views_help.relationship_help),
url(r'^help/relationships/(?P<subset>\w+)/$', views_help.relationship_help), url(r'^help/relationships/(?P<subset>\w+)/$', views_help.relationship_help),

View file

@ -3,15 +3,17 @@
import datetime import datetime
from django import forms from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError, ObjectDoesNotExist
from django.core.validators import URLValidator
from django.db.models import Q from django.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
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.conf import settings
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.contrib.auth.decorators import login_required
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize
from django.contrib import messages
import debug # pyflakes:ignore import debug # pyflakes:ignore
@ -33,7 +35,7 @@ from ietf.iesg.models import TelechatDate
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person
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 from ietf.name.models import IntendedStdLevelName, DocTagName, StreamName, DocUrlTagName
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 from ietf.utils.mail import send_mail, send_mail_message
@ -1110,14 +1112,89 @@ def edit_consensus(request, name):
}, },
) )
class PublicationForm(forms.Form): def edit_document_urls(request, name):
subject = forms.CharField(max_length=200, required=True) class DocumentUrlForm(forms.Form):
body = forms.CharField(widget=forms.Textarea, required=True, strip=False) urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", required=False,
help_text=("Format: 'tag https://site/path (Optional description)'."
" Separate multiple entries with newline. Prefer HTTPS URLs where possible.") )
def clean_urls(self):
lines = [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
url_validator = URLValidator()
for l in lines:
errors = []
parts = l.split()
if len(parts) == 1:
errors.append("Too few fields: Expected at least url and tag: '%s'" % l)
elif len(parts) >= 2:
tag = parts[0]
url = parts[1]
try:
url_validator(url)
except ValidationError as e:
errors.append(e)
try:
DocUrlTagName.objects.get(slug=tag)
except ObjectDoesNotExist:
errors.append("Bad tag in '%s': Expected one of %s" % (l, ', '.join([ o.slug for o in DocUrlTagName.objects.all() ])))
if errors:
raise ValidationError(errors)
return lines
def format_urls(urls, fs="\n"):
res = []
for u in urls:
if u.desc:
res.append(u"%s %s (%s)" % (u.tag.slug, u.url, u.desc.strip('()')))
else:
res.append(u"%s %s" % (u.tag.slug, u.url))
return fs.join(res)
doc = get_object_or_404(Document, name=name)
if not (has_role(request.user, ("Secretariat", "Area Director"))
or is_authorized_in_doc_stream(request.user, doc)):
return HttpResponseForbidden("You do not have the necessary permissions to view this page")
old_urls = format_urls(doc.documenturl_set.all())
if request.method == 'POST':
form = DocumentUrlForm(request.POST)
if form.is_valid():
old_urls = sorted(old_urls.splitlines())
new_urls = sorted(form.cleaned_data['urls'])
if old_urls != new_urls:
doc.documenturl_set.all().delete()
for u in new_urls:
parts = u.split(None, 2)
tag = parts[0]
url = parts[1]
desc = ' '.join(parts[2:]).strip('()')
doc.documenturl_set.create(url=url, tag_id=tag, desc=desc)
new_urls = format_urls(doc.documenturl_set.all())
e = DocEvent(doc=doc, rev=doc.rev, by=request.user.person, type='changed_document')
e.desc = "Changed document URLs from:\n\n%s\n\nto:\n\n%s" % (old_urls, new_urls)
e.save()
doc.save_with_history([e])
messages.success(request,"Document URLs updated.")
else:
messages.info(request,"No change in Document URLs.")
return redirect('ietf.doc.views_doc.document_main', name=doc.name)
else:
form = DocumentUrlForm(initial={'urls': old_urls, })
info = "Valid tags:<br><br> %s" % ', '.join([ o.slug for o in DocUrlTagName.objects.all() ])
title = "Additional document URLs"
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."""
class PublicationForm(forms.Form):
subject = forms.CharField(max_length=200, required=True)
body = forms.CharField(widget=forms.Textarea, required=True, strip=False)
doc = get_object_or_404(Document, type="draft", name=name, stream__in=("iab", "ise", "irtf")) doc = get_object_or_404(Document, type="draft", name=name, stream__in=("iab", "ise", "irtf"))
if not is_authorized_in_doc_stream(request.user, doc): if not is_authorized_in_doc_stream(request.user, doc):

View file

@ -8,7 +8,8 @@ from ietf.name.models import (
IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName, IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName,
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)
from ietf.stats.models import CountryAlias from ietf.stats.models import CountryAlias
@ -75,3 +76,4 @@ admin.site.register(StdLevelName, NameAdmin)
admin.site.register(StreamName, NameAdmin) admin.site.register(StreamName, NameAdmin)
admin.site.register(TimeSlotTypeName, NameAdmin) admin.site.register(TimeSlotTypeName, NameAdmin)
admin.site.register(TopicAudienceName, NameAdmin) admin.site.register(TopicAudienceName, NameAdmin)
admin.site.register(DocUrlTagName, NameAdmin)

File diff suppressed because it is too large Load diff

View file

@ -107,6 +107,9 @@ class CountryName(NameModel):
"Afghanistan, Aaland Islands, Albania, ..." "Afghanistan, Aaland Islands, Albania, ..."
continent = models.ForeignKey(ContinentName) continent = models.ForeignKey(ContinentName)
in_eu = models.BooleanField(verbose_name="In EU", default=False) in_eu = models.BooleanField(verbose_name="In EU", default=False)
class ImportantDateName(NameModel): class ImportantDateName(NameModel):
"Registration Opens, Scheduling Opens, ID Cutoff, ..."
default_offset_days = models.SmallIntegerField() default_offset_days = models.SmallIntegerField()
class DocUrlTagName(NameModel):
"Repository, Wiki, Issue Tracker, ..."

View file

@ -13,9 +13,9 @@ from ietf.name.models import (TimeSlotTypeName, GroupStateName, DocTagName, Inte
IprEventTypeName, GroupMilestoneStateName, SessionStatusName, DocReminderTypeName, IprEventTypeName, GroupMilestoneStateName, SessionStatusName, DocReminderTypeName,
ConstraintName, MeetingTypeName, DocRelationshipName, RoomResourceName, IprLicenseTypeName, ConstraintName, MeetingTypeName, DocRelationshipName, RoomResourceName, IprLicenseTypeName,
LiaisonStatementTagName, FeedbackTypeName, LiaisonStatementState, StreamName, LiaisonStatementTagName, FeedbackTypeName, LiaisonStatementState, StreamName,
BallotPositionName, DBTemplateTypeName, NomineePositionStateName, BallotPositionName, DBTemplateTypeName, NomineePositionStateName, ReviewRequestStateName,
ReviewRequestStateName, ReviewTypeName, ReviewResultName, ReviewTypeName, ReviewResultName, TopicAudienceName, FormalLanguageName, ContinentName,
TopicAudienceName, FormalLanguageName, ContinentName, CountryName, ImportantDateName) CountryName, ImportantDateName, DocUrlTagName)
class TimeSlotTypeNameResource(ModelResource): class TimeSlotTypeNameResource(ModelResource):
@ -536,3 +536,19 @@ class ImportantDateNameResource(ModelResource):
"default_offset_days": ALL, "default_offset_days": ALL,
} }
api.name.register(ImportantDateNameResource()) api.name.register(ImportantDateNameResource())
class DocUrlTagNameResource(ModelResource):
class Meta:
queryset = DocUrlTagName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'docurltagname'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(DocUrlTagNameResource())

View file

@ -240,6 +240,32 @@
</tr> </tr>
{% endif %} {% endif %}
{% with doc.documenturl_set.all as urls %}
{% if urls or can_edit_stream_info %}
<tr>
<td></td>
<th>Additional URLs</th>
<td class="edit">
{% if can_edit_stream_info %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_draft.edit_document_urls' name=doc.name %}">Edit</a>
{% endif %}
</td>
<td>
{% if urls %}
<table class="col-md-12 col-sm-12 col-xs-12">
<tbody>
{% for url in urls %}
<tr><td> - <a href="{{ url.url }}">{% firstof url.desc url.tag.name %}</a></td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</td>
</tr>
{% endif %}
{% endwith %}
</tbody> </tbody>
<tbody class="meta"> <tbody class="meta">
<tr> <tr>

View file

@ -114,7 +114,7 @@
{% endif %} {% endif %}
{% with group.groupurl_set.all as urls %} {% with group.groupurl_set.all as urls %}
{% if urls %} {% if urls or can_edit_group %}
<tr> <tr>
<td></td> <td></td>
<th>Additional URLs</th> <th>Additional URLs</th>
@ -124,9 +124,15 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% for url in urls %} {% if urls %}
<a href="{{ url.url }}">{% firstof url.name url.url %}</a>{% if not forloop.last %}<br>{% endif %} <table>
{% endfor %} <tbody>
{% for url in urls %}
<tr><td> - <a href="{{ url.url }}">{% firstof url.name url.url %}</a></td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</td> </td>
</tr> </tr>
{% endif %} {% endif %}