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:
parent
34c32e1b71
commit
92d425fd9b
|
@ -7,7 +7,7 @@ from models import (StateType, State, RelatedDocument, DocumentAuthor, Document,
|
|||
DocHistoryAuthor, DocHistory, DocAlias, DocReminder, DocEvent, NewRevisionDocEvent,
|
||||
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
|
||||
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
|
||||
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, )
|
||||
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL)
|
||||
|
||||
|
||||
from ietf.doc.utils import get_state_types
|
||||
|
@ -211,3 +211,7 @@ class BallotPositionDocEventAdmin(DocEventAdmin):
|
|||
raw_id_fields = ["doc", "by", "ad", "ballot"]
|
||||
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)
|
||||
|
|
|
@ -20,7 +20,8 @@ import debug # pyflakes:ignore
|
|||
|
||||
from ietf.group.models import Group
|
||||
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.utils import log
|
||||
from ietf.utils.admin import admin_link
|
||||
|
@ -779,6 +780,11 @@ class Document(DocumentInfo):
|
|||
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):
|
||||
source = models.ForeignKey('DocHistory')
|
||||
|
|
|
@ -12,7 +12,7 @@ from ietf.doc.models import (BallotType, DeletedEvent, StateType, State, Documen
|
|||
TelechatDocEvent, DocReminder, LastCallDocEvent, NewRevisionDocEvent, WriteupDocEvent,
|
||||
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
|
||||
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
|
||||
ReviewRequestDocEvent, EditedAuthorsDocEvent)
|
||||
ReviewRequestDocEvent, EditedAuthorsDocEvent, DocumentURL)
|
||||
|
||||
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
|
||||
class BallotTypeResource(ModelResource):
|
||||
|
@ -629,3 +629,22 @@ class EditedAuthorsDocEventResource(ModelResource):
|
|||
"docevent_ptr": ALL_WITH_RELATIONS,
|
||||
}
|
||||
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())
|
||||
|
|
|
@ -765,6 +765,11 @@ class ExpireLastCallTests(TestCase):
|
|||
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To'])
|
||||
|
||||
class IndividualInfoFormsTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.doc = make_test_data()
|
||||
self.docname = self.doc.name
|
||||
|
||||
def test_doc_change_stream(self):
|
||||
url = urlreverse('ietf.doc.views_draft.change_stream', kwargs=dict(name=self.docname))
|
||||
login_testing_unauthorized(self, "secretary", url)
|
||||
|
@ -1050,11 +1055,24 @@ class IndividualInfoFormsTests(TestCase):
|
|||
q = PyQuery(r.content)
|
||||
self.assertTrue(q('textarea')[0].text.strip().startswith("As required by RFC 4858"))
|
||||
|
||||
def setUp(self):
|
||||
make_test_data()
|
||||
self.docname='draft-ietf-mars-test'
|
||||
self.doc = Document.objects.get(name=self.docname)
|
||||
def test_doc_change_document_urls(self):
|
||||
url = urlreverse('ietf.doc.views_draft.edit_document_urls', kwargs=dict(name=self.docname))
|
||||
|
||||
# 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.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):
|
||||
def test_verify_permissions(self):
|
||||
|
|
|
@ -118,6 +118,7 @@ urlpatterns = [
|
|||
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/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/relationships/$', views_help.relationship_help),
|
||||
|
|
|
@ -3,15 +3,17 @@
|
|||
import datetime
|
||||
|
||||
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.http import HttpResponseRedirect, HttpResponseForbidden, Http404
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.conf import settings
|
||||
from django.forms.utils import ErrorList
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.template.defaultfilters import pluralize
|
||||
from django.contrib import messages
|
||||
|
||||
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 role_required
|
||||
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.models import Person, Email
|
||||
from ietf.utils.mail import send_mail, send_mail_message
|
||||
|
@ -1110,14 +1112,89 @@ def edit_consensus(request, name):
|
|||
},
|
||||
)
|
||||
|
||||
class PublicationForm(forms.Form):
|
||||
subject = forms.CharField(max_length=200, required=True)
|
||||
body = forms.CharField(widget=forms.Textarea, required=True, strip=False)
|
||||
def edit_document_urls(request, name):
|
||||
class DocumentUrlForm(forms.Form):
|
||||
urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", required=False,
|
||||
help_text=("Format: 'tag https://site/path (Optional description)'."
|
||||
" Separate multiple entries with newline. Prefer HTTPS URLs where possible.") )
|
||||
|
||||
def clean_urls(self):
|
||||
lines = [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
|
||||
url_validator = URLValidator()
|
||||
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):
|
||||
"""Request publication by RFC Editor for a document which hasn't
|
||||
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"))
|
||||
|
||||
if not is_authorized_in_doc_stream(request.user, doc):
|
||||
|
|
|
@ -8,7 +8,8 @@ from ietf.name.models import (
|
|||
IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName,
|
||||
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
|
||||
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
|
||||
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName, )
|
||||
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
|
||||
DocUrlTagName)
|
||||
|
||||
from ietf.stats.models import CountryAlias
|
||||
|
||||
|
@ -75,3 +76,4 @@ admin.site.register(StdLevelName, NameAdmin)
|
|||
admin.site.register(StreamName, NameAdmin)
|
||||
admin.site.register(TimeSlotTypeName, NameAdmin)
|
||||
admin.site.register(TopicAudienceName, NameAdmin)
|
||||
admin.site.register(DocUrlTagName, NameAdmin)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -107,6 +107,9 @@ class CountryName(NameModel):
|
|||
"Afghanistan, Aaland Islands, Albania, ..."
|
||||
continent = models.ForeignKey(ContinentName)
|
||||
in_eu = models.BooleanField(verbose_name="In EU", default=False)
|
||||
|
||||
class ImportantDateName(NameModel):
|
||||
"Registration Opens, Scheduling Opens, ID Cutoff, ..."
|
||||
default_offset_days = models.SmallIntegerField()
|
||||
class DocUrlTagName(NameModel):
|
||||
"Repository, Wiki, Issue Tracker, ..."
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@ from ietf.name.models import (TimeSlotTypeName, GroupStateName, DocTagName, Inte
|
|||
IprEventTypeName, GroupMilestoneStateName, SessionStatusName, DocReminderTypeName,
|
||||
ConstraintName, MeetingTypeName, DocRelationshipName, RoomResourceName, IprLicenseTypeName,
|
||||
LiaisonStatementTagName, FeedbackTypeName, LiaisonStatementState, StreamName,
|
||||
BallotPositionName, DBTemplateTypeName, NomineePositionStateName,
|
||||
ReviewRequestStateName, ReviewTypeName, ReviewResultName,
|
||||
TopicAudienceName, FormalLanguageName, ContinentName, CountryName, ImportantDateName)
|
||||
BallotPositionName, DBTemplateTypeName, NomineePositionStateName, ReviewRequestStateName,
|
||||
ReviewTypeName, ReviewResultName, TopicAudienceName, FormalLanguageName, ContinentName,
|
||||
CountryName, ImportantDateName, DocUrlTagName)
|
||||
|
||||
|
||||
class TimeSlotTypeNameResource(ModelResource):
|
||||
|
@ -536,3 +536,19 @@ class ImportantDateNameResource(ModelResource):
|
|||
"default_offset_days": ALL,
|
||||
}
|
||||
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())
|
||||
|
|
|
@ -240,6 +240,32 @@
|
|||
</tr>
|
||||
{% 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 class="meta">
|
||||
<tr>
|
||||
|
|
|
@ -114,7 +114,7 @@
|
|||
{% endif %}
|
||||
|
||||
{% with group.groupurl_set.all as urls %}
|
||||
{% if urls %}
|
||||
{% if urls or can_edit_group %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<th>Additional URLs</th>
|
||||
|
@ -124,9 +124,15 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if urls %}
|
||||
<table>
|
||||
<tbody>
|
||||
{% for url in urls %}
|
||||
<a href="{{ url.url }}">{% firstof url.name url.url %}</a>{% if not forloop.last %}<br>{% endif %}
|
||||
<tr><td> - <a href="{{ url.url }}">{% firstof url.name url.url %}</a></td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
|
Loading…
Reference in a new issue