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,
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)

View file

@ -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
@ -777,9 +778,14 @@ class Document(DocumentInfo):
stream=self.stream, group=self.group)
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')
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,
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())

View file

@ -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'
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):

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/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),
url(r'^help/relationships/(?P<subset>\w+)/$', views_help.relationship_help),

View file

@ -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):

View file

@ -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

View file

@ -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, ..."

View file

@ -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())

View file

@ -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>

View file

@ -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>
{% for url in urls %}
<a href="{{ url.url }}">{% firstof url.name url.url %}</a>{% if not forloop.last %}<br>{% endif %}
{% endfor %}
{% if urls %}
<table>
<tbody>
{% for url in urls %}
<tr><td> - <a href="{{ url.url }}">{% firstof url.name url.url %}</a></td></tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</td>
</tr>
{% endif %}