Allow assignment of Person as "action holder" for a Doc, plus rudimentary automation of assignment. Fixes #3146. Commit ready for merge.
- Legacy-Id: 18829
This commit is contained in:
parent
df37793e14
commit
e11583a87f
|
@ -11,7 +11,7 @@ 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, DocExtResource )
|
ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder )
|
||||||
|
|
||||||
from ietf.utils.validators import validate_external_resource_value
|
from ietf.utils.validators import validate_external_resource_value
|
||||||
|
|
||||||
|
@ -34,6 +34,11 @@ class DocAuthorInline(admin.TabularInline):
|
||||||
raw_id_fields = ['person', 'email']
|
raw_id_fields = ['person', 'email']
|
||||||
extra = 1
|
extra = 1
|
||||||
|
|
||||||
|
class DocActionHolderInline(admin.TabularInline):
|
||||||
|
model = DocumentActionHolder
|
||||||
|
raw_id_fields = ['person']
|
||||||
|
extra = 1
|
||||||
|
|
||||||
class RelatedDocumentInline(admin.TabularInline):
|
class RelatedDocumentInline(admin.TabularInline):
|
||||||
model = RelatedDocument
|
model = RelatedDocument
|
||||||
def this(self, instance):
|
def this(self, instance):
|
||||||
|
@ -72,7 +77,7 @@ class DocumentAdmin(admin.ModelAdmin):
|
||||||
search_fields = ['name']
|
search_fields = ['name']
|
||||||
list_filter = ['type']
|
list_filter = ['type']
|
||||||
raw_id_fields = ['group', 'shepherd', 'ad']
|
raw_id_fields = ['group', 'shepherd', 'ad']
|
||||||
inlines = [DocAuthorInline, RelatedDocumentInline, AdditionalUrlInLine]
|
inlines = [DocAuthorInline, DocActionHolderInline, RelatedDocumentInline, AdditionalUrlInLine]
|
||||||
form = DocumentForm
|
form = DocumentForm
|
||||||
|
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
|
@ -137,6 +142,13 @@ class BallotTypeAdmin(admin.ModelAdmin):
|
||||||
list_display = ["slug", "doc_type", "name", "question"]
|
list_display = ["slug", "doc_type", "name", "question"]
|
||||||
admin.site.register(BallotType, BallotTypeAdmin)
|
admin.site.register(BallotType, BallotTypeAdmin)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentActionHolderAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['id', 'document', 'person', 'time_added']
|
||||||
|
raw_id_fields = ['document', 'person']
|
||||||
|
admin.site.register(DocumentActionHolder, DocumentActionHolderAdmin)
|
||||||
|
|
||||||
|
|
||||||
# events
|
# events
|
||||||
|
|
||||||
class DocEventAdmin(admin.ModelAdmin):
|
class DocEventAdmin(admin.ModelAdmin):
|
||||||
|
|
|
@ -15,7 +15,7 @@ from ietf.utils.mail import send_mail
|
||||||
from ietf.doc.models import Document, DocEvent, State, IESG_SUBSTATE_TAGS
|
from ietf.doc.models import Document, DocEvent, State, IESG_SUBSTATE_TAGS
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.meeting.models import Meeting
|
from ietf.meeting.models import Meeting
|
||||||
from ietf.doc.utils import add_state_change_event
|
from ietf.doc.utils import add_state_change_event, update_action_holders
|
||||||
from ietf.mailtrigger.utils import gather_address_lists
|
from ietf.mailtrigger.utils import gather_address_lists
|
||||||
|
|
||||||
|
|
||||||
|
@ -171,6 +171,9 @@ def expire_draft(doc):
|
||||||
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
if e:
|
if e:
|
||||||
events.append(e)
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
|
||||||
events.append(DocEvent.objects.create(doc=doc, rev=doc.rev, by=system, type="expired_document", desc="Document has expired"))
|
events.append(DocEvent.objects.create(doc=doc, rev=doc.rev, by=system, type="expired_document", desc="Document has expired"))
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,8 @@ from typing import Optional # pyflakes:ignore
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
from ietf.doc.models import ( Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor,
|
from ietf.doc.models import ( Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor,
|
||||||
StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent)
|
StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent,
|
||||||
|
DocumentActionHolder)
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
|
|
||||||
def draft_name_generator(type_id,group,n):
|
def draft_name_generator(type_id,group,n):
|
||||||
|
@ -358,3 +359,9 @@ class BallotPositionDocEventFactory(DocEventFactory):
|
||||||
balloter = factory.SubFactory('ietf.person.factories.PersonFactory')
|
balloter = factory.SubFactory('ietf.person.factories.PersonFactory')
|
||||||
pos_id = 'discuss'
|
pos_id = 'discuss'
|
||||||
|
|
||||||
|
class DocumentActionHolderFactory(factory.DjangoModelFactory):
|
||||||
|
class Meta:
|
||||||
|
model = DocumentActionHolder
|
||||||
|
|
||||||
|
document = factory.SubFactory(WgDraftFactory)
|
||||||
|
person = factory.SubFactory('ietf.person.factories.PersonFactory')
|
||||||
|
|
|
@ -10,6 +10,8 @@ from ietf.doc.fields import SearchableDocAliasesField, SearchableDocAliasField
|
||||||
from ietf.doc.models import RelatedDocument
|
from ietf.doc.models import RelatedDocument
|
||||||
from ietf.iesg.models import TelechatDate
|
from ietf.iesg.models import TelechatDate
|
||||||
from ietf.iesg.utils import telechat_page_count
|
from ietf.iesg.utils import telechat_page_count
|
||||||
|
from ietf.person.fields import SearchablePersonsField
|
||||||
|
|
||||||
|
|
||||||
class TelechatForm(forms.Form):
|
class TelechatForm(forms.Form):
|
||||||
telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, help_text="Page counts are the current page counts for the telechat, before this telechat date edit is made.")
|
telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, help_text="Page counts are the current page counts for the telechat, before this telechat date edit is made.")
|
||||||
|
@ -54,6 +56,15 @@ class NotifyForm(forms.Form):
|
||||||
addrspecs = [x.strip() for x in self.cleaned_data["notify"].split(',')]
|
addrspecs = [x.strip() for x in self.cleaned_data["notify"].split(',')]
|
||||||
return ', '.join(addrspecs)
|
return ', '.join(addrspecs)
|
||||||
|
|
||||||
|
class ActionHoldersForm(forms.Form):
|
||||||
|
action_holders = SearchablePersonsField(required=False)
|
||||||
|
reason = forms.CharField(
|
||||||
|
label='Reason for change',
|
||||||
|
required=False,
|
||||||
|
max_length=255,
|
||||||
|
strip=True,
|
||||||
|
)
|
||||||
|
|
||||||
IESG_APPROVED_STATE_LIST = ("ann", "rfcqueue", "pub")
|
IESG_APPROVED_STATE_LIST = ("ann", "rfcqueue", "pub")
|
||||||
|
|
||||||
class AddDownrefForm(forms.Form):
|
class AddDownrefForm(forms.Form):
|
||||||
|
|
|
@ -7,7 +7,7 @@ from django.db.models import Q
|
||||||
from ietf.doc.models import Document, State, DocEvent, LastCallDocEvent, WriteupDocEvent
|
from ietf.doc.models import Document, State, DocEvent, LastCallDocEvent, WriteupDocEvent
|
||||||
from ietf.doc.models import IESG_SUBSTATE_TAGS
|
from ietf.doc.models import IESG_SUBSTATE_TAGS
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.doc.utils import add_state_change_event
|
from ietf.doc.utils import add_state_change_event, update_action_holders
|
||||||
from ietf.doc.mails import generate_ballot_writeup, generate_approval_mail, generate_last_call_announcement
|
from ietf.doc.mails import generate_ballot_writeup, generate_approval_mail, generate_last_call_announcement
|
||||||
from ietf.doc.mails import send_last_call_request, email_last_call_expired, email_last_call_expired_with_downref
|
from ietf.doc.mails import send_last_call_request, email_last_call_expired, email_last_call_expired_with_downref
|
||||||
|
|
||||||
|
@ -60,9 +60,14 @@ def expire_last_call(doc):
|
||||||
doc.tags.remove(*prev_tags)
|
doc.tags.remove(*prev_tags)
|
||||||
|
|
||||||
system = Person.objects.get(name="(System)")
|
system = Person.objects.get(name="(System)")
|
||||||
|
events = []
|
||||||
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
if e:
|
if e:
|
||||||
doc.save_with_history([e])
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
doc.save_with_history(events)
|
||||||
|
|
||||||
email_last_call_expired(doc)
|
email_last_call_expired(doc)
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ from django.utils.encoding import force_text
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
from ietf.doc.templatetags.mail_filters import std_level_prompt
|
from ietf.doc.templatetags.mail_filters import std_level_prompt
|
||||||
|
|
||||||
|
from ietf.utils import log
|
||||||
from ietf.utils.mail import send_mail, send_mail_text
|
from ietf.utils.mail import send_mail, send_mail_text
|
||||||
from ietf.ipr.utils import iprs_from_docs, related_docs
|
from ietf.ipr.utils import iprs_from_docs, related_docs
|
||||||
from ietf.doc.models import WriteupDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent
|
from ietf.doc.models import WriteupDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent
|
||||||
|
@ -127,6 +128,22 @@ def email_iesg_processing_document(request, doc, changes):
|
||||||
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
|
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
|
||||||
cc=addrs.cc)
|
cc=addrs.cc)
|
||||||
|
|
||||||
|
def email_remind_action_holders(request, doc, note=None):
|
||||||
|
addrs = gather_address_lists('doc_remind_action_holders', doc=doc)
|
||||||
|
log.assertion(
|
||||||
|
'not doc.action_holders.exclude(email__in=addrs.to).exists()',
|
||||||
|
note='All action holders should receive a reminder email. Failed for %s.' % doc.name,
|
||||||
|
)
|
||||||
|
send_mail(request, addrs.to, None,
|
||||||
|
'Reminder: action needed for %s' % doc.display_name(),
|
||||||
|
'doc/mail/remind_action_holders_mail.txt',
|
||||||
|
dict(
|
||||||
|
doc=doc,
|
||||||
|
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
||||||
|
note=note,
|
||||||
|
),
|
||||||
|
cc=addrs.cc)
|
||||||
|
|
||||||
def html_to_text(html):
|
def html_to_text(html):
|
||||||
return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("<br>", "\n"))
|
return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("<br>", "\n"))
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Generated by Django 2.2.17 on 2021-01-15 12:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('group', '0040_lengthen_used_roles_fields'), # only needed for schema vs data ordering
|
||||||
|
('doc', '0039_auto_20201109_0439'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='docevent',
|
||||||
|
name='type',
|
||||||
|
field=models.CharField(choices=[('new_revision', 'Added new revision'), ('new_submission', 'Uploaded new revision'), ('changed_document', 'Changed document metadata'), ('added_comment', 'Added comment'), ('added_message', 'Added message'), ('edited_authors', 'Edited the documents author list'), ('deleted', 'Deleted document'), ('changed_state', 'Changed state'), ('changed_stream', 'Changed document stream'), ('expired_document', 'Expired document'), ('extended_expiry', 'Extended expiry of document'), ('requested_resurrect', 'Requested resurrect'), ('completed_resurrect', 'Completed resurrect'), ('changed_consensus', 'Changed consensus'), ('published_rfc', 'Published RFC'), ('added_suggested_replaces', 'Added suggested replacement relationships'), ('reviewed_suggested_replaces', 'Reviewed suggested replacement relationships'), ('changed_action_holders', 'Changed action holders for document'), ('changed_group', 'Changed group'), ('changed_protocol_writeup', 'Changed protocol writeup'), ('changed_charter_milestone', 'Changed charter milestone'), ('initial_review', 'Set initial review time'), ('changed_review_announcement', 'Changed WG Review text'), ('changed_action_announcement', 'Changed WG Action text'), ('started_iesg_process', 'Started IESG process on document'), ('created_ballot', 'Created ballot'), ('closed_ballot', 'Closed ballot'), ('sent_ballot_announcement', 'Sent ballot announcement'), ('changed_ballot_position', 'Changed ballot position'), ('changed_ballot_approval_text', 'Changed ballot approval text'), ('changed_ballot_writeup_text', 'Changed ballot writeup text'), ('changed_rfc_editor_note_text', 'Changed RFC Editor Note text'), ('changed_last_call_text', 'Changed last call text'), ('requested_last_call', 'Requested last call'), ('sent_last_call', 'Sent last call'), ('scheduled_for_telechat', 'Scheduled for telechat'), ('iesg_approved', 'IESG approved document (no problem)'), ('iesg_disapproved', 'IESG disapproved document (do not publish)'), ('approved_in_minute', 'Approved in minute'), ('iana_review', 'IANA review comment'), ('rfc_in_iana_registry', 'RFC is in IANA registry'), ('rfc_editor_received_announcement', 'Announcement was received by RFC Editor'), ('requested_publication', 'Publication at RFC Editor requested'), ('sync_from_rfc_editor', 'Received updated information from RFC Editor'), ('requested_review', 'Requested review'), ('assigned_review_request', 'Assigned review request'), ('closed_review_request', 'Closed review request'), ('closed_review_assignment', 'Closed review assignment'), ('downref_approved', 'Downref approved'), ('posted_related_ipr', 'Posted related IPR'), ('removed_related_ipr', 'Removed related IPR')], max_length=50),
|
||||||
|
),
|
||||||
|
]
|
35
ietf/doc/migrations/0041_add_documentactionholder.py
Normal file
35
ietf/doc/migrations/0041_add_documentactionholder.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Generated by Django 2.2.17 on 2021-01-15 12:50
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
import ietf.utils.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('person', '0018_auto_20201109_0439'),
|
||||||
|
('doc', '0040_add_changed_action_holders_docevent_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='DocumentActionHolder',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('time_added', models.DateTimeField(default=datetime.datetime.now)),
|
||||||
|
('document', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document')),
|
||||||
|
('person', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='document',
|
||||||
|
name='action_holders',
|
||||||
|
field=models.ManyToManyField(blank=True, through='doc.DocumentActionHolder', to='person.Person'),
|
||||||
|
),
|
||||||
|
migrations.AddConstraint(
|
||||||
|
model_name='documentactionholder',
|
||||||
|
constraint=models.UniqueConstraint(fields=('document', 'person'), name='unique_action_holder'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -630,6 +630,45 @@ class DocumentAuthor(DocumentAuthorInfo):
|
||||||
return u"%s %s (%s)" % (self.document.name, self.person, self.order)
|
return u"%s %s (%s)" % (self.document.name, self.person, self.order)
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentActionHolder(models.Model):
|
||||||
|
"""Action holder for a document"""
|
||||||
|
document = ForeignKey('Document')
|
||||||
|
person = ForeignKey(Person)
|
||||||
|
time_added = models.DateTimeField(default=datetime.datetime.now)
|
||||||
|
|
||||||
|
CLEAR_ACTION_HOLDERS_STATES = ['approved', 'ann', 'rfcqueue', 'pub', 'dead'] # draft-iesg state slugs
|
||||||
|
GROUP_ROLES_OF_INTEREST = ['chair', 'techadv', 'editor', 'secr']
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.person)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(fields=['document', 'person'], name='unique_action_holder')
|
||||||
|
]
|
||||||
|
|
||||||
|
def role_for_doc(self):
|
||||||
|
"""Brief string description of this person's relationship to the doc"""
|
||||||
|
roles = []
|
||||||
|
if self.person in self.document.authors():
|
||||||
|
roles.append('Author')
|
||||||
|
if self.person == self.document.ad:
|
||||||
|
roles.append('Responsible AD')
|
||||||
|
if self.document.shepherd and self.person == self.document.shepherd.person:
|
||||||
|
roles.append('Shepherd')
|
||||||
|
if self.document.group:
|
||||||
|
roles.extend([
|
||||||
|
'Group %s' % role.name.name
|
||||||
|
for role in self.document.group.role_set.filter(
|
||||||
|
name__in=self.GROUP_ROLES_OF_INTEREST,
|
||||||
|
person=self.person,
|
||||||
|
)
|
||||||
|
])
|
||||||
|
|
||||||
|
if not roles:
|
||||||
|
roles.append('Action Holder')
|
||||||
|
return ', '.join(roles)
|
||||||
|
|
||||||
validate_docname = RegexValidator(
|
validate_docname = RegexValidator(
|
||||||
r'^[-a-z0-9]+$',
|
r'^[-a-z0-9]+$',
|
||||||
"Provide a valid document name consisting of lowercase letters, numbers and hyphens.",
|
"Provide a valid document name consisting of lowercase letters, numbers and hyphens.",
|
||||||
|
@ -638,6 +677,8 @@ validate_docname = RegexValidator(
|
||||||
|
|
||||||
class Document(DocumentInfo):
|
class Document(DocumentInfo):
|
||||||
name = models.CharField(max_length=255, validators=[validate_docname,], unique=True) # immutable
|
name = models.CharField(max_length=255, validators=[validate_docname,], unique=True) # immutable
|
||||||
|
|
||||||
|
action_holders = models.ManyToManyField(Person, through=DocumentActionHolder, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -869,6 +910,11 @@ class Document(DocumentInfo):
|
||||||
|
|
||||||
return dh
|
return dh
|
||||||
|
|
||||||
|
def action_holders_enabled(self):
|
||||||
|
"""Is the action holder list active for this document?"""
|
||||||
|
iesg_state = self.get_state('draft-iesg')
|
||||||
|
return iesg_state and iesg_state.slug != 'idexists'
|
||||||
|
|
||||||
class DocumentURL(models.Model):
|
class DocumentURL(models.Model):
|
||||||
doc = ForeignKey(Document)
|
doc = ForeignKey(Document)
|
||||||
tag = ForeignKey(DocUrlTagName)
|
tag = ForeignKey(DocUrlTagName)
|
||||||
|
@ -1000,6 +1046,7 @@ EVENT_TYPES = [
|
||||||
("published_rfc", "Published RFC"),
|
("published_rfc", "Published RFC"),
|
||||||
("added_suggested_replaces", "Added suggested replacement relationships"),
|
("added_suggested_replaces", "Added suggested replacement relationships"),
|
||||||
("reviewed_suggested_replaces", "Reviewed suggested replacement relationships"),
|
("reviewed_suggested_replaces", "Reviewed suggested replacement relationships"),
|
||||||
|
("changed_action_holders", "Changed action holders for document"),
|
||||||
|
|
||||||
# WG events
|
# WG events
|
||||||
("changed_group", "Changed group"),
|
("changed_group", "Changed group"),
|
||||||
|
|
|
@ -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, DocExtResource )
|
IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder )
|
||||||
|
|
||||||
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
|
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
|
||||||
class BallotTypeResource(ModelResource):
|
class BallotTypeResource(ModelResource):
|
||||||
|
@ -787,3 +787,22 @@ class DocExtResourceResource(ModelResource):
|
||||||
"name": ALL_WITH_RELATIONS,
|
"name": ALL_WITH_RELATIONS,
|
||||||
}
|
}
|
||||||
api.doc.register(DocExtResourceResource())
|
api.doc.register(DocExtResourceResource())
|
||||||
|
|
||||||
|
|
||||||
|
from ietf.person.resources import PersonResource
|
||||||
|
class DocumentActionHolderResource(ModelResource):
|
||||||
|
document = ToOneField(DocumentResource, 'document')
|
||||||
|
person = ToOneField(PersonResource, 'person')
|
||||||
|
class Meta:
|
||||||
|
queryset = DocumentActionHolder.objects.all()
|
||||||
|
serializer = api.Serializer()
|
||||||
|
cache = SimpleCache()
|
||||||
|
#resource_name = 'documentactionholder'
|
||||||
|
ordering = ['id', ]
|
||||||
|
filtering = {
|
||||||
|
"id": ALL,
|
||||||
|
"time_added": ALL,
|
||||||
|
"document": ALL_WITH_RELATIONS,
|
||||||
|
"person": ALL_WITH_RELATIONS,
|
||||||
|
}
|
||||||
|
api.doc.register(DocumentActionHolderResource())
|
||||||
|
|
|
@ -581,3 +581,35 @@ def can_ballot(user,doc):
|
||||||
return has_role(user,'IRSG Member')
|
return has_role(user,'IRSG Member')
|
||||||
else:
|
else:
|
||||||
return user.person.role_set.filter(name="ad", group__type="area", group__state="active")
|
return user.person.role_set.filter(name="ad", group__type="area", group__state="active")
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def action_holder_badge(action_holder):
|
||||||
|
"""Add a warning tag if action holder age exceeds limit
|
||||||
|
|
||||||
|
>>> from ietf.doc.factories import DocumentActionHolderFactory
|
||||||
|
>>> old_limit = settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS
|
||||||
|
>>> settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS = 15
|
||||||
|
>>> action_holder_badge(DocumentActionHolderFactory())
|
||||||
|
''
|
||||||
|
|
||||||
|
>>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=15)))
|
||||||
|
''
|
||||||
|
|
||||||
|
>>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=16)))
|
||||||
|
'<span class="label label-danger" title="Goal is <15 days">for 16 days</span>'
|
||||||
|
|
||||||
|
>>> action_holder_badge(DocumentActionHolderFactory(time_added=datetime.datetime.now() - datetime.timedelta(days=30)))
|
||||||
|
'<span class="label label-danger" title="Goal is <15 days">for 30 days</span>'
|
||||||
|
|
||||||
|
>>> settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS = old_limit
|
||||||
|
"""
|
||||||
|
age_limit = settings.DOC_ACTION_HOLDER_AGE_LIMIT_DAYS
|
||||||
|
age = (datetime.datetime.now() - action_holder.time_added).days
|
||||||
|
if age > age_limit:
|
||||||
|
return mark_safe('<span class="label label-danger" title="Goal is <%d days">for %d day%s</span>' % (
|
||||||
|
age_limit,
|
||||||
|
age,
|
||||||
|
's' if age != 1 else ''))
|
||||||
|
else:
|
||||||
|
return '' # no alert needed
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import io
|
||||||
import lxml
|
import lxml
|
||||||
import sys
|
import sys
|
||||||
import bibtexparser
|
import bibtexparser
|
||||||
|
import mock
|
||||||
|
|
||||||
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
if sys.version_info[0] == 2 and sys.version_info[1] < 7:
|
||||||
import unittest2 as unittest
|
import unittest2 as unittest
|
||||||
|
@ -224,6 +225,7 @@ class SearchTests(TestCase):
|
||||||
def test_docs_for_ad(self):
|
def test_docs_for_ad(self):
|
||||||
ad = RoleFactory(name_id='ad',group__type_id='area',group__state_id='active').person
|
ad = RoleFactory(name_id='ad',group__type_id='area',group__state_id='active').person
|
||||||
draft = IndividualDraftFactory(ad=ad)
|
draft = IndividualDraftFactory(ad=ad)
|
||||||
|
draft.action_holders.set([PersonFactory()])
|
||||||
draft.set_state(State.objects.get(type='draft-iesg', slug='lc'))
|
draft.set_state(State.objects.get(type='draft-iesg', slug='lc'))
|
||||||
rfc = IndividualDraftFactory(ad=ad)
|
rfc = IndividualDraftFactory(ad=ad)
|
||||||
rfc.set_state(State.objects.get(type='draft', slug='rfc'))
|
rfc.set_state(State.objects.get(type='draft', slug='rfc'))
|
||||||
|
@ -243,6 +245,7 @@ class SearchTests(TestCase):
|
||||||
r = self.client.get(urlreverse('ietf.doc.views_search.docs_for_ad', kwargs=dict(name=ad.full_name_as_key())))
|
r = self.client.get(urlreverse('ietf.doc.views_search.docs_for_ad', kwargs=dict(name=ad.full_name_as_key())))
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertContains(r, draft.name)
|
self.assertContains(r, draft.name)
|
||||||
|
self.assertContains(r, draft.action_holders.first().plain_name())
|
||||||
self.assertContains(r, rfc.canonical_name())
|
self.assertContains(r, rfc.canonical_name())
|
||||||
self.assertContains(r, conflrev.name)
|
self.assertContains(r, conflrev.name)
|
||||||
self.assertContains(r, statchg.name)
|
self.assertContains(r, statchg.name)
|
||||||
|
@ -265,18 +268,22 @@ class SearchTests(TestCase):
|
||||||
|
|
||||||
def test_drafts_in_last_call(self):
|
def test_drafts_in_last_call(self):
|
||||||
draft = IndividualDraftFactory(pages=1)
|
draft = IndividualDraftFactory(pages=1)
|
||||||
|
draft.action_holders.set([PersonFactory()])
|
||||||
draft.set_state(State.objects.get(type="draft-iesg", slug="lc"))
|
draft.set_state(State.objects.get(type="draft-iesg", slug="lc"))
|
||||||
r = self.client.get(urlreverse('ietf.doc.views_search.drafts_in_last_call'))
|
r = self.client.get(urlreverse('ietf.doc.views_search.drafts_in_last_call'))
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertContains(r, draft.title)
|
self.assertContains(r, draft.title)
|
||||||
|
self.assertContains(r, draft.action_holders.first().plain_name())
|
||||||
|
|
||||||
def test_in_iesg_process(self):
|
def test_in_iesg_process(self):
|
||||||
doc_in_process = IndividualDraftFactory()
|
doc_in_process = IndividualDraftFactory()
|
||||||
|
doc_in_process.action_holders.set([PersonFactory()])
|
||||||
doc_in_process.set_state(State.objects.get(type='draft-iesg', slug='lc'))
|
doc_in_process.set_state(State.objects.get(type='draft-iesg', slug='lc'))
|
||||||
doc_not_in_process = IndividualDraftFactory()
|
doc_not_in_process = IndividualDraftFactory()
|
||||||
r = self.client.get(urlreverse('ietf.doc.views_search.drafts_in_iesg_process'))
|
r = self.client.get(urlreverse('ietf.doc.views_search.drafts_in_iesg_process'))
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertContains(r, doc_in_process.title)
|
self.assertContains(r, doc_in_process.title)
|
||||||
|
self.assertContains(r, doc_in_process.action_holders.first().plain_name())
|
||||||
self.assertNotContains(r, doc_not_in_process.title)
|
self.assertNotContains(r, doc_not_in_process.title)
|
||||||
|
|
||||||
def test_indexes(self):
|
def test_indexes(self):
|
||||||
|
@ -323,6 +330,7 @@ class SearchTests(TestCase):
|
||||||
drafts = WgDraftFactory.create_batch(3,states=[('draft','active'),('draft-iesg','ad-eval')])
|
drafts = WgDraftFactory.create_batch(3,states=[('draft','active'),('draft-iesg','ad-eval')])
|
||||||
for index, draft in enumerate(drafts):
|
for index, draft in enumerate(drafts):
|
||||||
StateDocEventFactory(doc=draft, state=('draft-iesg','ad-eval'), time=datetime.datetime.now()-datetime.timedelta(days=[1,15,29][index]))
|
StateDocEventFactory(doc=draft, state=('draft-iesg','ad-eval'), time=datetime.datetime.now()-datetime.timedelta(days=[1,15,29][index]))
|
||||||
|
draft.action_holders.set([PersonFactory()])
|
||||||
|
|
||||||
# And one draft that should not show (with the default of 7 days to view)
|
# And one draft that should not show (with the default of 7 days to view)
|
||||||
old = WgDraftFactory()
|
old = WgDraftFactory()
|
||||||
|
@ -335,7 +343,9 @@ class SearchTests(TestCase):
|
||||||
q = PyQuery(r.content)
|
q = PyQuery(r.content)
|
||||||
self.assertEqual(len(q('td.doc')),3)
|
self.assertEqual(len(q('td.doc')),3)
|
||||||
self.assertEqual(q('td.status span.label-warning').text(),"for 15 days")
|
self.assertEqual(q('td.status span.label-warning').text(),"for 15 days")
|
||||||
self.assertEqual(q('td.status span.label-danger').text(),"for 29 days")
|
self.assertEqual(q('td.status span.label-danger').text(),"for 29 days")
|
||||||
|
for ah in [draft.action_holders.first() for draft in drafts]:
|
||||||
|
self.assertContains(r, ah.plain_name())
|
||||||
|
|
||||||
class DocDraftTestCase(TestCase):
|
class DocDraftTestCase(TestCase):
|
||||||
draft_text = """
|
draft_text = """
|
||||||
|
@ -775,6 +785,95 @@ Man Expires September 22, 2015 [Page 3]
|
||||||
msg_prefix='Non-WG-like group %s (%s) should not include group type in link' % (group.acronym, group.type),
|
msg_prefix='Non-WG-like group %s (%s) should not include group type in link' % (group.acronym, group.type),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _pyquery_select_action_holder_string(q, s):
|
||||||
|
"""Helper to use PyQuery to find an action holder in the draft HTML"""
|
||||||
|
# selector grabs the action holders heading and finds siblings with a div containing the search string
|
||||||
|
return q('th:contains("Action Holders") ~ td>div:contains("%s")' % s)
|
||||||
|
|
||||||
|
@mock.patch.object(Document, 'action_holders_enabled', return_value=False, new_callable=mock.PropertyMock)
|
||||||
|
def test_document_draft_hides_action_holders(self, mock_method):
|
||||||
|
"""Draft should not show action holders when appropriate"""
|
||||||
|
draft = WgDraftFactory()
|
||||||
|
url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertNotContains(r, 'Action Holders') # should not show action holders...
|
||||||
|
|
||||||
|
draft.action_holders.set([PersonFactory()])
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertNotContains(r, 'Action Holders') # ...even if they are assigned
|
||||||
|
|
||||||
|
@mock.patch.object(Document, 'action_holders_enabled', return_value=True, new_callable=mock.PropertyMock)
|
||||||
|
def test_document_draft_shows_action_holders(self, mock_method):
|
||||||
|
"""Draft should show action holders when appropriate"""
|
||||||
|
draft = WgDraftFactory()
|
||||||
|
url = urlreverse("ietf.doc.views_doc.document_main", kwargs=dict(name=draft.name))
|
||||||
|
|
||||||
|
# No action holders case should be shown properly
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertContains(r, 'Action Holders') # should show action holders
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
self.assertEqual(len(self._pyquery_select_action_holder_string(q, '(None)')), 1)
|
||||||
|
|
||||||
|
# Action holders should be listed when assigned
|
||||||
|
draft.action_holders.set(PersonFactory.create_batch(3))
|
||||||
|
|
||||||
|
# Make one action holder "old"
|
||||||
|
old_action_holder = draft.documentactionholder_set.first()
|
||||||
|
old_action_holder.time_added -= datetime.timedelta(days=30)
|
||||||
|
old_action_holder.save()
|
||||||
|
|
||||||
|
with self.settings(DOC_ACTION_HOLDER_AGE_LIMIT_DAYS=20):
|
||||||
|
r = self.client.get(url)
|
||||||
|
|
||||||
|
self.assertContains(r, 'Action Holders') # should still be shown
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
self.assertEqual(len(self._pyquery_select_action_holder_string(q, '(None)')), 0)
|
||||||
|
for person in draft.action_holders.all():
|
||||||
|
self.assertEqual(len(self._pyquery_select_action_holder_string(q, person.plain_name())), 1)
|
||||||
|
# check that one action holder was marked as old
|
||||||
|
self.assertEqual(len(self._pyquery_select_action_holder_string(q, 'for 30 days')), 1)
|
||||||
|
|
||||||
|
@mock.patch.object(Document, 'action_holders_enabled', return_value=True, new_callable=mock.PropertyMock)
|
||||||
|
def test_document_draft_action_holders_buttons(self, mock_method):
|
||||||
|
"""Buttons for action holders should be shown when AD or secretary"""
|
||||||
|
draft = WgDraftFactory()
|
||||||
|
draft.action_holders.set([PersonFactory()])
|
||||||
|
|
||||||
|
url = urlreverse('ietf.doc.views_doc.document_main', kwargs=dict(name=draft.name))
|
||||||
|
edit_ah_url = urlreverse('ietf.doc.views_doc.edit_action_holders', kwargs=dict(name=draft.name))
|
||||||
|
remind_ah_url = urlreverse('ietf.doc.views_doc.remind_action_holders', kwargs=dict(name=draft.name))
|
||||||
|
|
||||||
|
def _run_test(username=None, expect_buttons=False):
|
||||||
|
if username:
|
||||||
|
self.client.login(username=username, password=username + '+password')
|
||||||
|
r = self.client.get(url)
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
len(q('th:contains("Action Holders") ~ td a[href="%s"]' % edit_ah_url)),
|
||||||
|
1 if expect_buttons else 0,
|
||||||
|
'%s should%s see the edit action holders button but %s' % (
|
||||||
|
username if username else 'unauthenticated user',
|
||||||
|
'' if expect_buttons else ' not',
|
||||||
|
'did not' if expect_buttons else 'did',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
len(q('th:contains("Action Holders") ~ td a[href="%s"]' % remind_ah_url)),
|
||||||
|
1 if expect_buttons else 0,
|
||||||
|
'%s should%s see the remind action holders button but %s' % (
|
||||||
|
username if username else 'unauthenticated user',
|
||||||
|
'' if expect_buttons else ' not',
|
||||||
|
'did not' if expect_buttons else 'did',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_run_test(None, False)
|
||||||
|
_run_test('plain', False)
|
||||||
|
_run_test('ad', True)
|
||||||
|
_run_test('secretary', True)
|
||||||
|
|
||||||
def test_draft_group_link(self):
|
def test_draft_group_link(self):
|
||||||
"""Link to group 'about' page should have correct format"""
|
"""Link to group 'about' page should have correct format"""
|
||||||
for group_type_id in ['wg', 'rg', 'ag']:
|
for group_type_id in ['wg', 'rg', 'ag']:
|
||||||
|
|
|
@ -339,6 +339,8 @@ class BallotWriteupsTests(TestCase):
|
||||||
send_last_call_request="1"))
|
send_last_call_request="1"))
|
||||||
draft = Document.objects.get(name=draft.name)
|
draft = Document.objects.get(name=draft.name)
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "lc-req")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "lc-req")
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [ad])
|
||||||
|
self.assertIn('Changed action holders', draft.latest_event(type='changed_action_holders').desc)
|
||||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||||
self.assertTrue("Last Call" in outbox[-1]['Subject'])
|
self.assertTrue("Last Call" in outbox[-1]['Subject'])
|
||||||
self.assertTrue(draft.name in outbox[-1]['Subject'])
|
self.assertTrue(draft.name in outbox[-1]['Subject'])
|
||||||
|
@ -501,6 +503,7 @@ class BallotWriteupsTests(TestCase):
|
||||||
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
|
self.assertEqual(len(q('textarea[name=ballot_writeup]')), 1)
|
||||||
self.assertFalse(q('[class=help-block]:contains("not completed IETF Last Call")'))
|
self.assertFalse(q('[class=help-block]:contains("not completed IETF Last Call")'))
|
||||||
self.assertTrue(q('[type=submit]:contains("Save")'))
|
self.assertTrue(q('[type=submit]:contains("Save")'))
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [])
|
||||||
|
|
||||||
# save
|
# save
|
||||||
r = self.client.post(url, dict(
|
r = self.client.post(url, dict(
|
||||||
|
@ -508,7 +511,8 @@ class BallotWriteupsTests(TestCase):
|
||||||
issue_ballot="1"))
|
issue_ballot="1"))
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
d = Document.objects.get(name=draft.name)
|
d = Document.objects.get(name=draft.name)
|
||||||
self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg'))
|
self.assertTrue('iesg-eva' == d.get_state_slug('draft-iesg'))
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [ad])
|
||||||
|
|
||||||
def test_issue_ballot_warn_if_early(self):
|
def test_issue_ballot_warn_if_early(self):
|
||||||
ad = Person.objects.get(user__username="ad")
|
ad = Person.objects.get(user__username="ad")
|
||||||
|
@ -727,13 +731,17 @@ class ApproveBallotTests(TestCase):
|
||||||
self.assertTrue(not outbox[-1]['CC'])
|
self.assertTrue(not outbox[-1]['CC'])
|
||||||
self.assertTrue('drafts-approval@icann.org' in outbox[-1]['To'])
|
self.assertTrue('drafts-approval@icann.org' in outbox[-1]['To'])
|
||||||
self.assertTrue("Protocol Action" in draft.message_set.order_by("-time")[0].subject)
|
self.assertTrue("Protocol Action" in draft.message_set.order_by("-time")[0].subject)
|
||||||
|
# in 'ann' state, action holders should be empty
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [])
|
||||||
|
|
||||||
def test_disapprove_ballot(self):
|
def test_disapprove_ballot(self):
|
||||||
# This tests a codepath that is not used in production
|
# This tests a codepath that is not used in production
|
||||||
# and that has already had some drift from usefulness (it results in a
|
# and that has already had some drift from usefulness (it results in a
|
||||||
# older-style conflict review response).
|
# older-style conflict review response).
|
||||||
draft = IndividualDraftFactory()
|
ad = Person.objects.get(name="Areað Irector")
|
||||||
|
draft = IndividualDraftFactory(ad=ad)
|
||||||
draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="nopubadw"))
|
draft.set_state(State.objects.get(used=True, type="draft-iesg", slug="nopubadw"))
|
||||||
|
draft.action_holders.set([ad])
|
||||||
|
|
||||||
url = urlreverse('ietf.doc.views_ballot.approve_ballot', kwargs=dict(name=draft.name))
|
url = urlreverse('ietf.doc.views_ballot.approve_ballot', kwargs=dict(name=draft.name))
|
||||||
login_testing_unauthorized(self, "secretary", url)
|
login_testing_unauthorized(self, "secretary", url)
|
||||||
|
@ -748,6 +756,8 @@ class ApproveBallotTests(TestCase):
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "dead")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "dead")
|
||||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||||
self.assertTrue("NOT be published" in str(outbox[-1]))
|
self.assertTrue("NOT be published" in str(outbox[-1]))
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [])
|
||||||
|
self.assertIn('Removed all action holders', draft.latest_event(type='changed_action_holders').desc)
|
||||||
|
|
||||||
def test_clear_ballot(self):
|
def test_clear_ballot(self):
|
||||||
draft = IndividualDraftFactory()
|
draft = IndividualDraftFactory()
|
||||||
|
@ -846,6 +856,7 @@ class MakeLastCallTests(TestCase):
|
||||||
draft = Document.objects.get(name=draft.name)
|
draft = Document.objects.get(name=draft.name)
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "lc")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "lc")
|
||||||
self.assertEqual(draft.latest_event(LastCallDocEvent, "sent_last_call").expires.strftime("%Y-%m-%d"), expire_date)
|
self.assertEqual(draft.latest_event(LastCallDocEvent, "sent_last_call").expires.strftime("%Y-%m-%d"), expire_date)
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [ad])
|
||||||
|
|
||||||
self.assertEqual(len(outbox), mailbox_before + 2)
|
self.assertEqual(len(outbox), mailbox_before + 2)
|
||||||
|
|
||||||
|
@ -940,7 +951,12 @@ class DeferUndeferTestCase(TestCase):
|
||||||
if doc.type_id in defer_states:
|
if doc.type_id in defer_states:
|
||||||
self.assertEqual(doc.get_state(defer_states[doc.type_id][0]).slug,defer_states[doc.type_id][1])
|
self.assertEqual(doc.get_state(defer_states[doc.type_id][0]).slug,defer_states[doc.type_id][1])
|
||||||
self.assertTrue(doc.active_defer_event())
|
self.assertTrue(doc.active_defer_event())
|
||||||
|
if doc.type_id == 'draft':
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [doc.ad])
|
||||||
|
self.assertIn('Changed action holders', doc.latest_event(type='changed_action_holders').desc)
|
||||||
|
else:
|
||||||
|
self.assertIsNone(doc.latest_event(type='changed_action_holders'))
|
||||||
|
|
||||||
self.assertEqual(len(outbox), mailbox_before + 2)
|
self.assertEqual(len(outbox), mailbox_before + 2)
|
||||||
|
|
||||||
self.assertTrue('Telechat update' in outbox[-2]['Subject'])
|
self.assertTrue('Telechat update' in outbox[-2]['Subject'])
|
||||||
|
@ -1000,6 +1016,11 @@ class DeferUndeferTestCase(TestCase):
|
||||||
if doc.type_id in undefer_states:
|
if doc.type_id in undefer_states:
|
||||||
self.assertEqual(doc.get_state(undefer_states[doc.type_id][0]).slug,undefer_states[doc.type_id][1])
|
self.assertEqual(doc.get_state(undefer_states[doc.type_id][0]).slug,undefer_states[doc.type_id][1])
|
||||||
self.assertFalse(doc.active_defer_event())
|
self.assertFalse(doc.active_defer_event())
|
||||||
|
if doc.type_id == 'draft':
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [doc.ad])
|
||||||
|
self.assertIn('Changed action holders', doc.latest_event(type='changed_action_holders').desc)
|
||||||
|
else:
|
||||||
|
self.assertIsNone(doc.latest_event(type='changed_action_holders'))
|
||||||
self.assertEqual(len(outbox), mailbox_before + 2)
|
self.assertEqual(len(outbox), mailbox_before + 2)
|
||||||
self.assertTrue("Telechat update" in outbox[-2]['Subject'])
|
self.assertTrue("Telechat update" in outbox[-2]['Subject'])
|
||||||
self.assertTrue('iesg-secretary@' in outbox[-2]['To'])
|
self.assertTrue('iesg-secretary@' in outbox[-2]['To'])
|
||||||
|
@ -1035,7 +1056,8 @@ class DeferUndeferTestCase(TestCase):
|
||||||
# when charters support being deferred, be sure to test them here
|
# when charters support being deferred, be sure to test them here
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
IndividualDraftFactory(name='draft-ietf-mars-test',states=[('draft','active'),('draft-iesg','iesg-eva')])
|
IndividualDraftFactory(name='draft-ietf-mars-test',states=[('draft','active'),('draft-iesg','iesg-eva')],
|
||||||
|
ad=Person.objects.get(user__username='ad'))
|
||||||
DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review',states=[('statchg','iesgeval')])
|
DocumentFactory(type_id='statchg',name='status-change-imaginary-mid-review',states=[('statchg','iesgeval')])
|
||||||
DocumentFactory(type_id='conflrev',name='conflict-review-imaginary-irtf-submission',states=[('conflrev','iesgeval')])
|
DocumentFactory(type_id='conflrev',name='conflict-review-imaginary-irtf-submission',states=[('conflrev','iesgeval')])
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ from ietf.doc.utils import get_tags_for_stream_id, create_ballot_if_not_open
|
||||||
from ietf.name.models import StreamName, DocTagName
|
from ietf.name.models import StreamName, DocTagName
|
||||||
from ietf.group.factories import GroupFactory, RoleFactory
|
from ietf.group.factories import GroupFactory, RoleFactory
|
||||||
from ietf.group.models import Group, Role
|
from ietf.group.models import Group, Role
|
||||||
from ietf.person.factories import PersonFactory
|
from ietf.person.factories import PersonFactory, EmailFactory
|
||||||
from ietf.person.models import Person, Email
|
from ietf.person.models import Person, Email
|
||||||
from ietf.meeting.models import Meeting, MeetingTypeName
|
from ietf.meeting.models import Meeting, MeetingTypeName
|
||||||
from ietf.iesg.models import TelechatDate
|
from ietf.iesg.models import TelechatDate
|
||||||
|
@ -41,6 +41,7 @@ class ChangeStateTests(TestCase):
|
||||||
draft = WgDraftFactory(ad=ad,states=[('draft','active'),('draft-iesg','iesg-eva')])
|
draft = WgDraftFactory(ad=ad,states=[('draft','active'),('draft-iesg','iesg-eva')])
|
||||||
DocEventFactory(type='started_iesg_process',by=ad,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
DocEventFactory(type='started_iesg_process',by=ad,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
||||||
draft.tags.add("ad-f-up")
|
draft.tags.add("ad-f-up")
|
||||||
|
draft.action_holders.add(ad)
|
||||||
|
|
||||||
url = urlreverse('ietf.doc.views_draft.change_state', kwargs=dict(name=draft.name))
|
url = urlreverse('ietf.doc.views_draft.change_state', kwargs=dict(name=draft.name))
|
||||||
login_testing_unauthorized(self, "ad", url)
|
login_testing_unauthorized(self, "ad", url)
|
||||||
|
@ -67,10 +68,12 @@ class ChangeStateTests(TestCase):
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "approved")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "approved")
|
||||||
self.assertTrue(not draft.tags.filter(slug="approved"))
|
self.assertTrue(not draft.tags.filter(slug="approved"))
|
||||||
self.assertFalse(draft.tags.exists())
|
self.assertFalse(draft.tags.exists())
|
||||||
self.assertEqual(draft.docevent_set.count(), events_before + 2)
|
self.assertEqual(draft.docevent_set.count(), events_before + 3)
|
||||||
self.assertTrue("Test comment" in draft.docevent_set.all()[0].desc)
|
self.assertTrue("Test comment" in draft.docevent_set.all()[0].desc)
|
||||||
self.assertTrue("IESG state changed" in draft.docevent_set.all()[1].desc)
|
self.assertTrue("Removed all action holders" in draft.docevent_set.all()[1].desc)
|
||||||
|
self.assertTrue("IESG state changed" in draft.docevent_set.all()[2].desc)
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [])
|
||||||
|
|
||||||
# should have sent two emails, the second one to the iesg with approved message
|
# should have sent two emails, the second one to the iesg with approved message
|
||||||
self.assertEqual(len(outbox), mailbox_before + 2)
|
self.assertEqual(len(outbox), mailbox_before + 2)
|
||||||
self.assertTrue("Approved: " in outbox[-1]['Subject'])
|
self.assertTrue("Approved: " in outbox[-1]['Subject'])
|
||||||
|
@ -79,8 +82,15 @@ class ChangeStateTests(TestCase):
|
||||||
|
|
||||||
def test_change_state(self):
|
def test_change_state(self):
|
||||||
ad = Person.objects.get(user__username="ad")
|
ad = Person.objects.get(user__username="ad")
|
||||||
draft = WgDraftFactory(name='draft-ietf-mars-test',group__acronym='mars',ad=ad,states=[('draft','active'),('draft-iesg','ad-eval')])
|
draft = WgDraftFactory(
|
||||||
|
name='draft-ietf-mars-test',
|
||||||
|
group__acronym='mars',
|
||||||
|
ad=ad,
|
||||||
|
authors=PersonFactory.create_batch(3),
|
||||||
|
states=[('draft','active'),('draft-iesg','ad-eval')]
|
||||||
|
)
|
||||||
DocEventFactory(type='started_iesg_process',by=ad,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
DocEventFactory(type='started_iesg_process',by=ad,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
||||||
|
draft.action_holders.add(ad)
|
||||||
|
|
||||||
url = urlreverse('ietf.doc.views_draft.change_state', kwargs=dict(name=draft.name))
|
url = urlreverse('ietf.doc.views_draft.change_state', kwargs=dict(name=draft.name))
|
||||||
login_testing_unauthorized(self, "secretary", url)
|
login_testing_unauthorized(self, "secretary", url)
|
||||||
|
@ -105,7 +115,7 @@ class ChangeStateTests(TestCase):
|
||||||
self.assertTrue(len(q('form .has-error')) > 0)
|
self.assertTrue(len(q('form .has-error')) > 0)
|
||||||
draft = Document.objects.get(name=draft.name)
|
draft = Document.objects.get(name=draft.name)
|
||||||
self.assertEqual(draft.get_state("draft-iesg"), first_state)
|
self.assertEqual(draft.get_state("draft-iesg"), first_state)
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [ad])
|
||||||
|
|
||||||
# change state
|
# change state
|
||||||
events_before = draft.docevent_set.count()
|
events_before = draft.docevent_set.count()
|
||||||
|
@ -122,9 +132,11 @@ class ChangeStateTests(TestCase):
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "review-e")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "review-e")
|
||||||
self.assertTrue(not draft.tags.filter(slug="ad-f-up"))
|
self.assertTrue(not draft.tags.filter(slug="ad-f-up"))
|
||||||
self.assertTrue(draft.tags.filter(slug="need-rev"))
|
self.assertTrue(draft.tags.filter(slug="need-rev"))
|
||||||
self.assertEqual(draft.docevent_set.count(), events_before + 2)
|
self.assertCountEqual(draft.action_holders.all(), [ad] + draft.authors())
|
||||||
|
self.assertEqual(draft.docevent_set.count(), events_before + 3)
|
||||||
self.assertTrue("Test comment" in draft.docevent_set.all()[0].desc)
|
self.assertTrue("Test comment" in draft.docevent_set.all()[0].desc)
|
||||||
self.assertTrue("IESG state changed" in draft.docevent_set.all()[1].desc)
|
self.assertTrue("Changed action holders" in draft.docevent_set.all()[1].desc)
|
||||||
|
self.assertTrue("IESG state changed" in draft.docevent_set.all()[2].desc)
|
||||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||||
self.assertTrue("State Update Notice" in outbox[-1]['Subject'])
|
self.assertTrue("State Update Notice" in outbox[-1]['Subject'])
|
||||||
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To'])
|
self.assertTrue('draft-ietf-mars-test@' in outbox[-1]['To'])
|
||||||
|
@ -139,8 +151,13 @@ class ChangeStateTests(TestCase):
|
||||||
|
|
||||||
def test_pull_from_rfc_queue(self):
|
def test_pull_from_rfc_queue(self):
|
||||||
ad = Person.objects.get(user__username="ad")
|
ad = Person.objects.get(user__username="ad")
|
||||||
draft = WgDraftFactory(ad=ad,states=[('draft-iesg','rfcqueue')])
|
draft = WgDraftFactory(
|
||||||
|
ad=ad,
|
||||||
|
authors=PersonFactory.create_batch(3),
|
||||||
|
states=[('draft-iesg','rfcqueue')],
|
||||||
|
)
|
||||||
DocEventFactory(type='started_iesg_process',by=ad,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
DocEventFactory(type='started_iesg_process',by=ad,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
||||||
|
draft.action_holders.add(*(draft.authors()))
|
||||||
|
|
||||||
url = urlreverse('ietf.doc.views_draft.change_state', kwargs=dict(name=draft.name))
|
url = urlreverse('ietf.doc.views_draft.change_state', kwargs=dict(name=draft.name))
|
||||||
login_testing_unauthorized(self, "secretary", url)
|
login_testing_unauthorized(self, "secretary", url)
|
||||||
|
@ -156,7 +173,7 @@ class ChangeStateTests(TestCase):
|
||||||
|
|
||||||
draft = Document.objects.get(name=draft.name)
|
draft = Document.objects.get(name=draft.name)
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "review-e")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "review-e")
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [ad])
|
||||||
self.assertEqual(len(outbox), mailbox_before + 2)
|
self.assertEqual(len(outbox), mailbox_before + 2)
|
||||||
|
|
||||||
self.assertTrue(draft.name in outbox[-1]['Subject'])
|
self.assertTrue(draft.name in outbox[-1]['Subject'])
|
||||||
|
@ -234,8 +251,13 @@ class ChangeStateTests(TestCase):
|
||||||
|
|
||||||
def test_request_last_call(self):
|
def test_request_last_call(self):
|
||||||
ad = Person.objects.get(user__username="ad")
|
ad = Person.objects.get(user__username="ad")
|
||||||
draft = WgDraftFactory(ad=ad,states=[('draft-iesg','ad-eval')])
|
draft = WgDraftFactory(
|
||||||
|
ad=ad,
|
||||||
|
authors=PersonFactory.create_batch(3),
|
||||||
|
states=[('draft-iesg','ad-eval')],
|
||||||
|
)
|
||||||
DocEventFactory(type='started_iesg_process',by=ad,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
DocEventFactory(type='started_iesg_process',by=ad,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
||||||
|
draft.action_holders.add(*(draft.authors()))
|
||||||
|
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
self.client.login(username="secretary", password="secretary+password")
|
||||||
url = urlreverse('ietf.doc.views_draft.change_state', kwargs=dict(name=draft.name))
|
url = urlreverse('ietf.doc.views_draft.change_state', kwargs=dict(name=draft.name))
|
||||||
|
@ -247,6 +269,8 @@ class ChangeStateTests(TestCase):
|
||||||
self.assertEqual(r.status_code,200)
|
self.assertEqual(r.status_code,200)
|
||||||
self.assertContains(r, "Your request to issue")
|
self.assertContains(r, "Your request to issue")
|
||||||
|
|
||||||
|
draft = Document.objects.get(name=draft.name)
|
||||||
|
|
||||||
# last call text
|
# last call text
|
||||||
e = draft.latest_event(WriteupDocEvent, type="changed_last_call_text")
|
e = draft.latest_event(WriteupDocEvent, type="changed_last_call_text")
|
||||||
self.assertTrue(e)
|
self.assertTrue(e)
|
||||||
|
@ -279,6 +303,9 @@ class ChangeStateTests(TestCase):
|
||||||
# comment
|
# comment
|
||||||
self.assertTrue("Last call was requested" in draft.latest_event().desc)
|
self.assertTrue("Last call was requested" in draft.latest_event().desc)
|
||||||
|
|
||||||
|
# action holders
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [ad])
|
||||||
|
|
||||||
|
|
||||||
class EditInfoTests(TestCase):
|
class EditInfoTests(TestCase):
|
||||||
def test_edit_info(self):
|
def test_edit_info(self):
|
||||||
|
@ -449,9 +476,10 @@ class EditInfoTests(TestCase):
|
||||||
self.assertEqual(draft.ad, ad)
|
self.assertEqual(draft.ad, ad)
|
||||||
self.assertEqual(draft.note, "This is a note")
|
self.assertEqual(draft.note, "This is a note")
|
||||||
self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat"))
|
self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat"))
|
||||||
self.assertEqual(draft.docevent_set.count(), events_before + 4)
|
self.assertEqual(draft.docevent_set.count(), events_before + 5)
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [draft.ad])
|
||||||
events = list(draft.docevent_set.order_by('time', 'id'))
|
events = list(draft.docevent_set.order_by('time', 'id'))
|
||||||
self.assertEqual(events[-4].type, "started_iesg_process")
|
self.assertEqual(events[-5].type, "started_iesg_process")
|
||||||
self.assertEqual(len(outbox), mailbox_before+1)
|
self.assertEqual(len(outbox), mailbox_before+1)
|
||||||
self.assertTrue('IESG processing' in outbox[-1]['Subject'])
|
self.assertTrue('IESG processing' in outbox[-1]['Subject'])
|
||||||
self.assertTrue('draft-ietf-mars-test2@' in outbox[-1]['To'])
|
self.assertTrue('draft-ietf-mars-test2@' in outbox[-1]['To'])
|
||||||
|
@ -460,6 +488,7 @@ class EditInfoTests(TestCase):
|
||||||
draft.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
draft.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
||||||
draft.set_state(State.objects.get(type='draft-stream-ietf',slug='writeupw'))
|
draft.set_state(State.objects.get(type='draft-stream-ietf',slug='writeupw'))
|
||||||
draft.stream = StreamName.objects.get(slug='ietf')
|
draft.stream = StreamName.objects.get(slug='ietf')
|
||||||
|
draft.action_holders.clear()
|
||||||
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_stream", by=Person.objects.get(user__username="secretary"), desc="Test")])
|
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_stream", by=Person.objects.get(user__username="secretary"), desc="Test")])
|
||||||
r = self.client.post(url,
|
r = self.client.post(url,
|
||||||
dict(intended_std_level=str(draft.intended_std_level_id),
|
dict(intended_std_level=str(draft.intended_std_level_id),
|
||||||
|
@ -473,6 +502,7 @@ class EditInfoTests(TestCase):
|
||||||
draft = Document.objects.get(name=draft.name)
|
draft = Document.objects.get(name=draft.name)
|
||||||
self.assertEqual(draft.get_state_slug('draft-iesg'),'pub-req')
|
self.assertEqual(draft.get_state_slug('draft-iesg'),'pub-req')
|
||||||
self.assertEqual(draft.get_state_slug('draft-stream-ietf'),'sub-pub')
|
self.assertEqual(draft.get_state_slug('draft-stream-ietf'),'sub-pub')
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [draft.ad])
|
||||||
|
|
||||||
def test_edit_consensus(self):
|
def test_edit_consensus(self):
|
||||||
draft = WgDraftFactory()
|
draft = WgDraftFactory()
|
||||||
|
@ -668,8 +698,9 @@ class ExpireIDsTests(DraftFileMixin, TestCase):
|
||||||
|
|
||||||
def test_expire_drafts(self):
|
def test_expire_drafts(self):
|
||||||
mars = GroupFactory(type_id='wg',acronym='mars')
|
mars = GroupFactory(type_id='wg',acronym='mars')
|
||||||
ad_role = RoleFactory(group=mars, name_id='ad', person=Person.objects.get(user__username='ad'))
|
ad = Person.objects.get(user__username='ad')
|
||||||
draft = WgDraftFactory(name='draft-ietf-mars-test',group=mars)
|
ad_role = RoleFactory(group=mars, name_id='ad', person=ad)
|
||||||
|
draft = WgDraftFactory(name='draft-ietf-mars-test',group=mars,ad=ad)
|
||||||
DocEventFactory(type='started_iesg_process',by=ad_role.person,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
DocEventFactory(type='started_iesg_process',by=ad_role.person,doc=draft,rev=draft.rev,desc="Started IESG Process")
|
||||||
|
|
||||||
self.assertEqual(len(list(get_expired_drafts())), 0)
|
self.assertEqual(len(list(get_expired_drafts())), 0)
|
||||||
|
@ -689,6 +720,8 @@ class ExpireIDsTests(DraftFileMixin, TestCase):
|
||||||
|
|
||||||
self.assertEqual(len(list(get_expired_drafts())), 0)
|
self.assertEqual(len(list(get_expired_drafts())), 0)
|
||||||
|
|
||||||
|
draft.action_holders.set([draft.ad])
|
||||||
|
|
||||||
# test notice
|
# test notice
|
||||||
mailbox_before = len(outbox)
|
mailbox_before = len(outbox)
|
||||||
|
|
||||||
|
@ -710,6 +743,8 @@ class ExpireIDsTests(DraftFileMixin, TestCase):
|
||||||
self.assertEqual(draft.get_state_slug(), "expired")
|
self.assertEqual(draft.get_state_slug(), "expired")
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "dead")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "dead")
|
||||||
self.assertTrue(draft.latest_event(type="expired_document"))
|
self.assertTrue(draft.latest_event(type="expired_document"))
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [])
|
||||||
|
self.assertIn('Removed all action holders', draft.latest_event(type='changed_action_holders').desc)
|
||||||
self.assertTrue(not os.path.exists(os.path.join(self.id_dir, txt)))
|
self.assertTrue(not os.path.exists(os.path.join(self.id_dir, txt)))
|
||||||
self.assertTrue(os.path.exists(os.path.join(self.archive_dir, txt)))
|
self.assertTrue(os.path.exists(os.path.join(self.archive_dir, txt)))
|
||||||
|
|
||||||
|
@ -815,12 +850,14 @@ class ExpireLastCallTests(TestCase):
|
||||||
# expire it
|
# expire it
|
||||||
mailbox_before = len(outbox)
|
mailbox_before = len(outbox)
|
||||||
events_before = draft.docevent_set.count()
|
events_before = draft.docevent_set.count()
|
||||||
|
|
||||||
expire_last_call(drafts[0])
|
expire_last_call(drafts[0])
|
||||||
|
|
||||||
draft = Document.objects.get(name=draft.name)
|
draft = Document.objects.get(name=draft.name)
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "writeupw")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "writeupw")
|
||||||
self.assertEqual(draft.docevent_set.count(), events_before + 1)
|
self.assertEqual(draft.docevent_set.count(), events_before + 2)
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [ad])
|
||||||
|
self.assertIn('Changed action holders', draft.latest_event(type='changed_action_holders').desc)
|
||||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||||
self.assertTrue("Last Call Expired" in outbox[-1]["Subject"])
|
self.assertTrue("Last Call Expired" in outbox[-1]["Subject"])
|
||||||
self.assertTrue('iesg-secretary@' in outbox[-1]['Cc'])
|
self.assertTrue('iesg-secretary@' in outbox[-1]['Cc'])
|
||||||
|
@ -844,13 +881,15 @@ class ExpireLastCallTests(TestCase):
|
||||||
drafts = list(get_expired_last_calls())
|
drafts = list(get_expired_last_calls())
|
||||||
self.assertEqual(len(drafts), 1)
|
self.assertEqual(len(drafts), 1)
|
||||||
|
|
||||||
mailbox_before = len(outbox)
|
mailbox_before = len(outbox)
|
||||||
expire_last_call(drafts[0])
|
expire_last_call(drafts[0])
|
||||||
|
|
||||||
d = Document.objects.get(name=draft.name)
|
d = Document.objects.get(name=draft.name)
|
||||||
self.assertEqual(len(outbox), mailbox_before + 2)
|
self.assertEqual(len(outbox), mailbox_before + 2)
|
||||||
self.assertTrue("Review Downrefs From Expired Last Call" in outbox[-1]["Subject"])
|
self.assertTrue("Review Downrefs From Expired Last Call" in outbox[-1]["Subject"])
|
||||||
self.assertTrue(d.ad.email().address in outbox[-1]['To'])
|
self.assertTrue(d.ad.email().address in outbox[-1]['To'])
|
||||||
|
self.assertCountEqual(d.action_holders.all(), [ad])
|
||||||
|
self.assertIn('Changed action holders', d.latest_event(type='changed_action_holders').desc)
|
||||||
|
|
||||||
class IndividualInfoFormsTests(TestCase):
|
class IndividualInfoFormsTests(TestCase):
|
||||||
|
|
||||||
|
@ -1202,12 +1241,117 @@ class IndividualInfoFormsTests(TestCase):
|
||||||
self.assertEqual(doc.docextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
|
self.assertEqual(doc.docextresource_set.get(name__slug='github_repo').display_name, 'Some display text')
|
||||||
self.assertIn(doc.docextresource_set.first().name.slug,str(doc.docextresource_set.first()))
|
self.assertIn(doc.docextresource_set.first().name.slug,str(doc.docextresource_set.first()))
|
||||||
|
|
||||||
|
# This is in views_doc, not views_draft, but there's already mixing and this keeps it with similar tests
|
||||||
|
def do_doc_change_action_holders_test(self, username):
|
||||||
|
# Set up people related to the doc to be sure shortcut buttons appear.
|
||||||
|
doc = Document.objects.get(name=self.docname)
|
||||||
|
doc.documentauthor_set.create(person=PersonFactory())
|
||||||
|
doc.ad = Person.objects.get(user__username='ad')
|
||||||
|
doc.shepherd = EmailFactory()
|
||||||
|
doc.save_with_history([DocEvent.objects.create(doc=doc, rev=doc.rev, type="changed_shepherd", by=Person.objects.get(user__username="secretary"), desc="Test")])
|
||||||
|
RoleFactory(name_id='chair', person=PersonFactory(), group=doc.group)
|
||||||
|
RoleFactory(name_id='techadv', person=PersonFactory(), group=doc.group)
|
||||||
|
RoleFactory(name_id='editor', person=PersonFactory(), group=doc.group)
|
||||||
|
RoleFactory(name_id='secr', person=PersonFactory(), group=doc.group)
|
||||||
|
|
||||||
|
url = urlreverse('ietf.doc.views_doc.edit_action_holders', kwargs=dict(name=doc.name))
|
||||||
|
|
||||||
|
login_testing_unauthorized(self, username, url)
|
||||||
|
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
self.assertEqual(len(q('form input[id=id_reason]')), 1)
|
||||||
|
self.assertEqual(len(q('form input[id=id_action_holders]')), 1)
|
||||||
|
for role_name in [
|
||||||
|
'Author',
|
||||||
|
'Responsible AD',
|
||||||
|
'Shepherd',
|
||||||
|
'Group Chair',
|
||||||
|
'Group Tech Advisor',
|
||||||
|
'Group Editor',
|
||||||
|
'Group Secretary',
|
||||||
|
]:
|
||||||
|
self.assertEqual(len(q('button:contains("Add %s")' % role_name)), 1,
|
||||||
|
'Expected "Add %s" button' % role_name)
|
||||||
|
self.assertEqual(len(q('button:contains("Remove %s")' % role_name)), 1,
|
||||||
|
'Expected "Remove %s" button for' % role_name)
|
||||||
|
|
||||||
|
def _test_changing_ah(action_holders, reason):
|
||||||
|
r = self.client.post(url, dict(
|
||||||
|
reason=reason,
|
||||||
|
action_holders=','.join([str(p.pk) for p in action_holders]),
|
||||||
|
))
|
||||||
|
self.assertEqual(r.status_code, 302)
|
||||||
|
doc = Document.objects.get(name=self.docname)
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), action_holders)
|
||||||
|
event = doc.latest_event(type='changed_action_holders')
|
||||||
|
self.assertIn(reason, event.desc)
|
||||||
|
if action_holders:
|
||||||
|
for ah in action_holders:
|
||||||
|
self.assertIn(ah.plain_name(), event.desc)
|
||||||
|
else:
|
||||||
|
self.assertIn('Removed all', event.desc)
|
||||||
|
|
||||||
|
_test_changing_ah([doc.ad, doc.shepherd.person], 'this is a first test')
|
||||||
|
_test_changing_ah([doc.ad], 'this is a second test')
|
||||||
|
_test_changing_ah(doc.authors(), 'authors can do it, too')
|
||||||
|
_test_changing_ah([], 'clear it back out')
|
||||||
|
|
||||||
|
def test_doc_change_action_holders_as_secretary(self):
|
||||||
|
self.do_doc_change_action_holders_test('secretary')
|
||||||
|
|
||||||
|
def test_doc_change_action_holders_as_ad(self):
|
||||||
|
self.do_doc_change_action_holders_test('ad')
|
||||||
|
|
||||||
|
def do_doc_remind_action_holders_test(self, username):
|
||||||
|
doc = Document.objects.get(name=self.docname)
|
||||||
|
doc.action_holders.set(PersonFactory.create_batch(3))
|
||||||
|
|
||||||
|
url = urlreverse('ietf.doc.views_doc.remind_action_holders', kwargs=dict(name=doc.name))
|
||||||
|
|
||||||
|
login_testing_unauthorized(self, username, url)
|
||||||
|
r = self.client.get(url)
|
||||||
|
self.assertEqual(r.status_code, 200)
|
||||||
|
q = PyQuery(r.content)
|
||||||
|
self.assertEqual(len(q('form textarea[id=id_note]')), 1)
|
||||||
|
self.assertEqual(len(q('button:contains("Send")')), 1)
|
||||||
|
for ah in doc.action_holders.all():
|
||||||
|
self.assertContains(r, ah.plain_name())
|
||||||
|
|
||||||
|
empty_outbox()
|
||||||
|
r = self.client.post(url, dict(note='this is my note')) # note should be < 78 chars to avoid wrapping
|
||||||
|
self.assertEqual(r.status_code, 302)
|
||||||
|
|
||||||
|
self.assertEqual(len(outbox), 1)
|
||||||
|
for ah in doc.action_holders.all():
|
||||||
|
self.assertIn('Reminder: action needed', outbox[0]['Subject'])
|
||||||
|
self.assertIn(ah.email_address(), outbox[0]['To'])
|
||||||
|
self.assertIn(doc.display_name(), outbox[0].as_string())
|
||||||
|
self.assertIn(doc.get_absolute_url(), outbox[0].as_string())
|
||||||
|
self.assertIn('this is my note', outbox[0].as_string())
|
||||||
|
|
||||||
|
# check that nothing is sent when no action holders
|
||||||
|
doc.action_holders.clear()
|
||||||
|
self.client.post(url)
|
||||||
|
self.assertEqual(len(outbox), 1) # still 1
|
||||||
|
|
||||||
|
def test_doc_remind_action_holders_as_ad(self):
|
||||||
|
self.do_doc_remind_action_holders_test('ad')
|
||||||
|
|
||||||
|
def test_doc_remind_action_holders_as_secretary(self):
|
||||||
|
self.do_doc_remind_action_holders_test('secretary')
|
||||||
|
|
||||||
class SubmitToIesgTests(TestCase):
|
class SubmitToIesgTests(TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
role=RoleFactory(group__acronym='mars',name_id='chair',person=PersonFactory(user__username='marschairman'))
|
role=RoleFactory(group__acronym='mars',name_id='chair',person=PersonFactory(user__username='marschairman'))
|
||||||
doc=WgDraftFactory(name='draft-ietf-mars-test',group=role.group,ad=Person.objects.get(user__username='ad'))
|
doc=WgDraftFactory(
|
||||||
|
name='draft-ietf-mars-test',
|
||||||
|
group=role.group,
|
||||||
|
ad=Person.objects.get(user__username='ad'),
|
||||||
|
authors=PersonFactory.create_batch(3),
|
||||||
|
)
|
||||||
self.docname=doc.name
|
self.docname=doc.name
|
||||||
|
|
||||||
def test_verify_permissions(self):
|
def test_verify_permissions(self):
|
||||||
|
@ -1242,6 +1386,7 @@ class SubmitToIesgTests(TestCase):
|
||||||
|
|
||||||
doc = Document.objects.get(name=self.docname)
|
doc = Document.objects.get(name=self.docname)
|
||||||
self.assertEqual(doc.get_state_slug('draft-iesg'),'idexists')
|
self.assertEqual(doc.get_state_slug('draft-iesg'),'idexists')
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [])
|
||||||
|
|
||||||
def test_confirm_submission(self):
|
def test_confirm_submission(self):
|
||||||
url = urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=self.docname))
|
url = urlreverse('ietf.doc.views_draft.to_iesg', kwargs=dict(name=self.docname))
|
||||||
|
@ -1258,16 +1403,14 @@ class SubmitToIesgTests(TestCase):
|
||||||
self.assertTrue(doc.get_state('draft-iesg').slug=='pub-req')
|
self.assertTrue(doc.get_state('draft-iesg').slug=='pub-req')
|
||||||
self.assertTrue(doc.get_state('draft-stream-ietf').slug=='sub-pub')
|
self.assertTrue(doc.get_state('draft-stream-ietf').slug=='sub-pub')
|
||||||
|
|
||||||
# It's not clear what this testing - the view can certainly
|
self.assertCountEqual(doc.action_holders.all(), [doc.ad])
|
||||||
# leave the document without an ad. This line as written only
|
|
||||||
# checks whether the setup document had an ad or not.
|
|
||||||
self.assertTrue(doc.ad!=None)
|
|
||||||
|
|
||||||
new_docevents = set(doc.docevent_set.all()) - docevents_pre
|
new_docevents = set(doc.docevent_set.all()) - docevents_pre
|
||||||
self.assertEqual(len(new_docevents),3)
|
self.assertEqual(len(new_docevents), 4)
|
||||||
new_docevent_type_count = Counter([e.type for e in new_docevents])
|
new_docevent_type_count = Counter([e.type for e in new_docevents])
|
||||||
self.assertEqual(new_docevent_type_count['changed_state'],2)
|
self.assertEqual(new_docevent_type_count['changed_state'],2)
|
||||||
self.assertEqual(new_docevent_type_count['started_iesg_process'],1)
|
self.assertEqual(new_docevent_type_count['started_iesg_process'],1)
|
||||||
|
self.assertEqual(new_docevent_type_count['changed_action_holders'], 1)
|
||||||
|
|
||||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||||
self.assertTrue("Publication has been requested" in outbox[-1]['Subject'])
|
self.assertTrue("Publication has been requested" in outbox[-1]['Subject'])
|
||||||
|
|
207
ietf/doc/tests_utils.py
Normal file
207
ietf/doc/tests_utils.py
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
# Copyright The IETF Trust 2020, All Rights Reserved
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from ietf.group.factories import GroupFactory, RoleFactory
|
||||||
|
from ietf.name.models import DocTagName
|
||||||
|
from ietf.person.factories import PersonFactory
|
||||||
|
from ietf.utils.test_utils import TestCase
|
||||||
|
from ietf.person.models import Person
|
||||||
|
from ietf.doc.factories import DocumentFactory
|
||||||
|
from ietf.doc.models import State, DocumentActionHolder
|
||||||
|
from ietf.doc.utils import update_action_holders, add_state_change_event
|
||||||
|
|
||||||
|
|
||||||
|
class ActionHoldersTests(TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Set up helper for the update_action_holders tests"""
|
||||||
|
self.authors = PersonFactory.create_batch(3)
|
||||||
|
self.ad = Person.objects.get(user__username='ad')
|
||||||
|
self.group = GroupFactory()
|
||||||
|
RoleFactory(name_id='ad', group=self.group, person=self.ad)
|
||||||
|
|
||||||
|
def doc_in_iesg_state(self, slug):
|
||||||
|
return DocumentFactory(authors=self.authors, group=self.group, ad=self.ad, states=[('draft-iesg', slug)])
|
||||||
|
|
||||||
|
def update_doc_state(self, doc, new_state, add_tags=None, remove_tags=None):
|
||||||
|
"""Update document state/tags, create change event, and save"""
|
||||||
|
prev_tags = list(doc.tags.all()) # list to make sure we retrieve now
|
||||||
|
# prev_action_holders = list(doc.action_holders.all())
|
||||||
|
|
||||||
|
prev_state = doc.get_state(new_state.type_id)
|
||||||
|
if new_state != prev_state:
|
||||||
|
doc.set_state(new_state)
|
||||||
|
|
||||||
|
if add_tags:
|
||||||
|
doc.tags.add(*DocTagName.objects.filter(slug__in=add_tags))
|
||||||
|
if remove_tags:
|
||||||
|
doc.tags.remove(*DocTagName.objects.filter(slug__in=remove_tags))
|
||||||
|
new_tags = list(doc.tags.all())
|
||||||
|
|
||||||
|
events = []
|
||||||
|
e = add_state_change_event(
|
||||||
|
doc,
|
||||||
|
Person.objects.get(name='(System)'),
|
||||||
|
prev_state, new_state,
|
||||||
|
prev_tags, new_tags)
|
||||||
|
self.assertIsNotNone(e, 'Test logic error')
|
||||||
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags, new_tags)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
doc.save_with_history(events)
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_action_holders_by_state(self):
|
||||||
|
"""Doc action holders should auto-update correctly on state change"""
|
||||||
|
# Test the transition from every state to each of its 'next_states'
|
||||||
|
|
||||||
|
for initial_state in State.objects.filter(type__slug='draft-iesg'):
|
||||||
|
for next_state in initial_state.next_states.all():
|
||||||
|
# Test with no action holders initially
|
||||||
|
doc = DocumentFactory(
|
||||||
|
authors=self.authors,
|
||||||
|
group=self.group,
|
||||||
|
ad=self.ad,
|
||||||
|
states=[('draft-iesg', initial_state.slug)],
|
||||||
|
)
|
||||||
|
docevents_before = set(doc.docevent_set.all())
|
||||||
|
|
||||||
|
self.update_doc_state(doc, next_state)
|
||||||
|
|
||||||
|
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
|
||||||
|
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
|
||||||
|
|
||||||
|
if next_state.slug in DocumentActionHolder.CLEAR_ACTION_HOLDERS_STATES:
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [])
|
||||||
|
self.assertEqual(len(new_docevents), 1)
|
||||||
|
else:
|
||||||
|
self.assertCountEqual(
|
||||||
|
doc.action_holders.all(), [doc.ad],
|
||||||
|
'AD should be only action holder after transition to %s' % next_state.slug)
|
||||||
|
|
||||||
|
self.assertEqual(len(new_docevents), 2)
|
||||||
|
change_event = doc.latest_event(type='changed_action_holders')
|
||||||
|
self.assertIn(change_event, new_docevents)
|
||||||
|
self.assertIn('Changed action holders', change_event.desc)
|
||||||
|
self.assertIn(doc.ad.name, change_event.desc)
|
||||||
|
doc.delete() # clean up for next iteration
|
||||||
|
|
||||||
|
# Test with action holders initially
|
||||||
|
doc = DocumentFactory(
|
||||||
|
authors=self.authors,
|
||||||
|
group=self.group,
|
||||||
|
ad=self.ad,
|
||||||
|
states=[('draft-iesg', initial_state.slug)],
|
||||||
|
)
|
||||||
|
doc.action_holders.add(*self.authors) # adds all authors
|
||||||
|
docevents_before = set(doc.docevent_set.all())
|
||||||
|
|
||||||
|
self.update_doc_state(doc, next_state)
|
||||||
|
|
||||||
|
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
|
||||||
|
self.assertEqual(len(new_docevents), 2)
|
||||||
|
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
|
||||||
|
change_event = doc.latest_event(type='changed_action_holders')
|
||||||
|
self.assertIn(change_event, new_docevents)
|
||||||
|
|
||||||
|
if next_state.slug in DocumentActionHolder.CLEAR_ACTION_HOLDERS_STATES:
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [])
|
||||||
|
self.assertIn('Removed all action holders', change_event.desc)
|
||||||
|
else:
|
||||||
|
self.assertCountEqual(
|
||||||
|
doc.action_holders.all(), [doc.ad],
|
||||||
|
'AD should be only action holder after transition to %s' % next_state.slug)
|
||||||
|
self.assertIn('Changed action holders', change_event.desc)
|
||||||
|
self.assertIn(doc.ad.name, change_event.desc)
|
||||||
|
doc.delete() # clean up for next iteration
|
||||||
|
|
||||||
|
def test_update_action_holders_with_no_ad(self):
|
||||||
|
"""A document with no AD should be handled gracefully"""
|
||||||
|
doc = self.doc_in_iesg_state('idexists')
|
||||||
|
doc.ad = None
|
||||||
|
doc.save()
|
||||||
|
|
||||||
|
docevents_before = set(doc.docevent_set.all())
|
||||||
|
self.update_doc_state(doc, State.objects.get(slug='pub-req'))
|
||||||
|
new_docevents = set(doc.docevent_set.all()).difference(docevents_before)
|
||||||
|
self.assertEqual(len(new_docevents), 1)
|
||||||
|
self.assertIn(doc.latest_event(type='changed_state'), new_docevents)
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [])
|
||||||
|
|
||||||
|
def test_update_action_holders_resets_age(self):
|
||||||
|
"""Action holder age should reset when document state changes"""
|
||||||
|
doc = self.doc_in_iesg_state('pub-req')
|
||||||
|
doc.action_holders.set([self.ad])
|
||||||
|
dah = doc.documentactionholder_set.get(person=self.ad)
|
||||||
|
dah.time_added = datetime.datetime(2020, 1, 1) # arbitrary date in the past
|
||||||
|
dah.save()
|
||||||
|
|
||||||
|
self.assertNotEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today())
|
||||||
|
self.update_doc_state(doc, State.objects.get(slug='ad-eval'))
|
||||||
|
self.assertEqual(doc.documentactionholder_set.get(person=self.ad).time_added.date(), datetime.date.today())
|
||||||
|
|
||||||
|
def test_update_action_holders_add_tag_need_rev(self):
|
||||||
|
"""Adding need-rev tag adds authors as action holders"""
|
||||||
|
doc = self.doc_in_iesg_state('pub-req')
|
||||||
|
first_author = self.authors[0]
|
||||||
|
doc.action_holders.add(first_author)
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [first_author])
|
||||||
|
self.update_doc_state(doc,
|
||||||
|
doc.get_state('draft-iesg'),
|
||||||
|
add_tags=['need-rev'],
|
||||||
|
remove_tags=None)
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), self.authors)
|
||||||
|
|
||||||
|
def test_update_action_holders_add_tag_need_rev_no_dups(self):
|
||||||
|
"""Adding need-rev tag does not duplicate existing action holders"""
|
||||||
|
doc = self.doc_in_iesg_state('pub-req')
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [])
|
||||||
|
self.update_doc_state(doc,
|
||||||
|
doc.get_state('draft-iesg'),
|
||||||
|
add_tags=['need-rev'],
|
||||||
|
remove_tags=None)
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), self.authors)
|
||||||
|
|
||||||
|
def test_update_action_holders_remove_tag_need_rev(self):
|
||||||
|
"""Removing need-rev tag drops authors as action holders"""
|
||||||
|
doc = self.doc_in_iesg_state('pub-req')
|
||||||
|
doc.tags.add(DocTagName.objects.get(slug='need-rev'))
|
||||||
|
self.assertEqual(doc.action_holders.count(), 0)
|
||||||
|
self.update_doc_state(doc,
|
||||||
|
doc.get_state('draft-iesg'),
|
||||||
|
add_tags=None,
|
||||||
|
remove_tags=['need-rev'])
|
||||||
|
self.assertEqual(doc.action_holders.count(), 0)
|
||||||
|
|
||||||
|
def test_update_action_holders_add_tag_need_rev_ignores_non_authors(self):
|
||||||
|
"""Adding need-rev tag does not affect existing action holders"""
|
||||||
|
doc = self.doc_in_iesg_state('pub-req')
|
||||||
|
doc.action_holders.add(self.ad)
|
||||||
|
self.assertCountEqual(doc.action_holders.all(),[self.ad])
|
||||||
|
self.update_doc_state(doc,
|
||||||
|
doc.get_state('draft-iesg'),
|
||||||
|
add_tags=['need-rev'],
|
||||||
|
remove_tags=None)
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [self.ad] + self.authors)
|
||||||
|
|
||||||
|
def test_update_action_holders_remove_tag_need_rev_ignores_non_authors(self):
|
||||||
|
"""Removing need-rev tag does not affect non-author action holders"""
|
||||||
|
doc = self.doc_in_iesg_state('pub-req')
|
||||||
|
doc.tags.add(DocTagName.objects.get(slug='need-rev'))
|
||||||
|
doc.action_holders.add(self.ad)
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [self.ad])
|
||||||
|
self.update_doc_state(doc,
|
||||||
|
doc.get_state('draft-iesg'),
|
||||||
|
add_tags=None,
|
||||||
|
remove_tags=['need-rev'])
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [self.ad])
|
||||||
|
|
||||||
|
def test_doc_action_holders_enabled(self):
|
||||||
|
"""Action holders should only be enabled in certain states"""
|
||||||
|
doc = self.doc_in_iesg_state('idexists')
|
||||||
|
self.assertFalse(doc.action_holders_enabled())
|
||||||
|
|
||||||
|
for state in State.objects.filter(type='draft-iesg').exclude(slug='idexists'):
|
||||||
|
doc.set_state(state)
|
||||||
|
self.assertTrue(doc.action_holders_enabled())
|
|
@ -104,6 +104,8 @@ urlpatterns = [
|
||||||
url(r'^%(name)s/edit/stream/$' % settings.URL_REGEXPS, views_draft.change_stream),
|
url(r'^%(name)s/edit/stream/$' % settings.URL_REGEXPS, views_draft.change_stream),
|
||||||
url(r'^%(name)s/edit/replaces/$' % settings.URL_REGEXPS, views_draft.replaces),
|
url(r'^%(name)s/edit/replaces/$' % settings.URL_REGEXPS, views_draft.replaces),
|
||||||
url(r'^%(name)s/edit/notify/$' % settings.URL_REGEXPS, views_doc.edit_notify),
|
url(r'^%(name)s/edit/notify/$' % settings.URL_REGEXPS, views_doc.edit_notify),
|
||||||
|
url(r'^%(name)s/edit/actionholders/$' % settings.URL_REGEXPS, views_doc.edit_action_holders),
|
||||||
|
url(r'^%(name)s/edit/remindactionholders/$' % settings.URL_REGEXPS, views_doc.remind_action_holders),
|
||||||
url(r'^%(name)s/edit/suggested-replaces/$' % settings.URL_REGEXPS, views_draft.review_possibly_replaces),
|
url(r'^%(name)s/edit/suggested-replaces/$' % settings.URL_REGEXPS, views_draft.review_possibly_replaces),
|
||||||
url(r'^%(name)s/edit/status/$' % settings.URL_REGEXPS, views_draft.change_intention),
|
url(r'^%(name)s/edit/status/$' % settings.URL_REGEXPS, views_draft.change_intention),
|
||||||
url(r'^%(name)s/edit/telechat/$' % settings.URL_REGEXPS, views_doc.telechat_date),
|
url(r'^%(name)s/edit/telechat/$' % settings.URL_REGEXPS, views_doc.telechat_date),
|
||||||
|
|
|
@ -26,7 +26,7 @@ from ietf.community.utils import docs_tracked_by_community_list
|
||||||
from ietf.doc.models import Document, DocHistory, State, DocumentAuthor, DocHistoryAuthor
|
from ietf.doc.models import Document, DocHistory, State, DocumentAuthor, DocHistoryAuthor
|
||||||
from ietf.doc.models import DocAlias, RelatedDocument, RelatedDocHistory, BallotType, DocReminder
|
from ietf.doc.models import DocAlias, RelatedDocument, RelatedDocHistory, BallotType, DocReminder
|
||||||
from ietf.doc.models import DocEvent, ConsensusDocEvent, BallotDocEvent, IRSGBallotDocEvent, NewRevisionDocEvent, StateDocEvent
|
from ietf.doc.models import DocEvent, ConsensusDocEvent, BallotDocEvent, IRSGBallotDocEvent, NewRevisionDocEvent, StateDocEvent
|
||||||
from ietf.doc.models import TelechatDocEvent
|
from ietf.doc.models import TelechatDocEvent, DocumentActionHolder
|
||||||
from ietf.name.models import DocReminderTypeName, DocRelationshipName
|
from ietf.name.models import DocReminderTypeName, DocRelationshipName
|
||||||
from ietf.group.models import Role, Group
|
from ietf.group.models import Role, Group
|
||||||
from ietf.ietfauth.utils import has_role
|
from ietf.ietfauth.utils import has_role
|
||||||
|
@ -406,11 +406,15 @@ def get_document_content(key, filename, split=True, markup=True):
|
||||||
def tags_suffix(tags):
|
def tags_suffix(tags):
|
||||||
return ("::" + "::".join(t.name for t in tags)) if tags else ""
|
return ("::" + "::".join(t.name for t in tags)) if tags else ""
|
||||||
|
|
||||||
def add_state_change_event(doc, by, prev_state, new_state, prev_tags=[], new_tags=[], timestamp=None):
|
def add_state_change_event(doc, by, prev_state, new_state, prev_tags=None, new_tags=None, timestamp=None):
|
||||||
"""Add doc event to explain that state change just happened."""
|
"""Add doc event to explain that state change just happened."""
|
||||||
if prev_state and new_state:
|
if prev_state and new_state:
|
||||||
assert prev_state.type_id == new_state.type_id
|
assert prev_state.type_id == new_state.type_id
|
||||||
|
|
||||||
|
# convert default args to empty lists
|
||||||
|
prev_tags = prev_tags or []
|
||||||
|
new_tags = new_tags or []
|
||||||
|
|
||||||
if prev_state == new_state and set(prev_tags) == set(new_tags):
|
if prev_state == new_state and set(prev_tags) == set(new_tags):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -426,6 +430,88 @@ def add_state_change_event(doc, by, prev_state, new_state, prev_tags=[], new_tag
|
||||||
e.save()
|
e.save()
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
|
||||||
|
def add_action_holder_change_event(doc, by, prev_set, reason=None):
|
||||||
|
set_changed = False
|
||||||
|
if doc.documentactionholder_set.exclude(person__in=prev_set).exists():
|
||||||
|
set_changed = True # doc has an action holder not in the old set
|
||||||
|
# If set_changed is still False, then all of the current action holders were in
|
||||||
|
# prev_set. Either the sets are the same or the prev_set contains at least one
|
||||||
|
# Person not in the current set, so just check length.
|
||||||
|
if doc.documentactionholder_set.count() != len(prev_set):
|
||||||
|
set_changed = True
|
||||||
|
|
||||||
|
if not set_changed:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if doc.action_holders.exists():
|
||||||
|
ah_names = [person.plain_name() for person in doc.action_holders.all()]
|
||||||
|
description = 'Changed action holders to %s' % ', '.join(ah_names)
|
||||||
|
else:
|
||||||
|
description = 'Removed all action holders'
|
||||||
|
if reason:
|
||||||
|
description += ' (%s)' % reason
|
||||||
|
|
||||||
|
return DocEvent.objects.create(
|
||||||
|
type='changed_action_holders',
|
||||||
|
doc=doc,
|
||||||
|
by=by,
|
||||||
|
rev=doc.rev,
|
||||||
|
desc=description,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def update_action_holders(doc, prev_state=None, new_state=None, prev_tags=None, new_tags=None):
|
||||||
|
"""Update the action holders for doc based on state transition
|
||||||
|
|
||||||
|
Returns an event describing the change which should be passed to doc.save_with_history()
|
||||||
|
|
||||||
|
Only cares about draft-iesg state changes. Places where other state types are updated
|
||||||
|
may not call this method. If you add rules for updating action holders on other state
|
||||||
|
types, be sure this is called in the places that change that state.
|
||||||
|
"""
|
||||||
|
# Should not call this with different state types
|
||||||
|
if prev_state and new_state:
|
||||||
|
assert prev_state.type_id == new_state.type_id
|
||||||
|
|
||||||
|
# Convert tags to sets of slugs
|
||||||
|
prev_tag_slugs = {t.slug for t in (prev_tags or [])}
|
||||||
|
new_tag_slugs = {t.slug for t in (new_tags or [])}
|
||||||
|
|
||||||
|
# Do nothing if state / tag have not changed
|
||||||
|
if (prev_state == new_state) and (prev_tag_slugs == new_tag_slugs):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Remember original list of action holders to later check if it changed
|
||||||
|
prev_set = list(doc.action_holders.all())
|
||||||
|
# Only draft-iesg states are of interest (for now)
|
||||||
|
if (prev_state != new_state) and (getattr(new_state, 'type_id') == 'draft-iesg'):
|
||||||
|
# Clear the action_holders list on a state change. This will reset the age of any that get added back.
|
||||||
|
doc.action_holders.clear()
|
||||||
|
if doc.ad and new_state.slug not in DocumentActionHolder.CLEAR_ACTION_HOLDERS_STATES:
|
||||||
|
# Default to responsible AD for states other than these
|
||||||
|
doc.action_holders.add(doc.ad)
|
||||||
|
|
||||||
|
if prev_tag_slugs != new_tag_slugs:
|
||||||
|
# If we have added or removed the need-rev tag, add or remove authors as action holders
|
||||||
|
if ('need-rev' in prev_tag_slugs) and ('need-rev' not in new_tag_slugs):
|
||||||
|
# Removed the 'need-rev' tag - drop authors from the action holders list
|
||||||
|
DocumentActionHolder.objects.filter(document=doc, person__in=doc.authors()).delete()
|
||||||
|
elif ('need-rev' not in prev_tag_slugs) and ('need-rev' in new_tag_slugs):
|
||||||
|
# Added the 'need-rev' tag - add authors to the action holders list
|
||||||
|
for auth in doc.authors():
|
||||||
|
if not doc.action_holders.filter(pk=auth.pk).exists():
|
||||||
|
doc.action_holders.add(auth)
|
||||||
|
|
||||||
|
# Now create an event if we changed the set
|
||||||
|
return add_action_holder_change_event(
|
||||||
|
doc,
|
||||||
|
Person.objects.get(name='(System)'),
|
||||||
|
prev_set,
|
||||||
|
reason='IESG state changed',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def update_reminder(doc, reminder_type_slug, event, due_date):
|
def update_reminder(doc, reminder_type_slug, event, due_date):
|
||||||
reminder_type = DocReminderTypeName.objects.get(slug=reminder_type_slug)
|
reminder_type = DocReminderTypeName.objects.get(slug=reminder_type_slug)
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ from ietf.doc.models import ( Document, State, DocEvent, BallotDocEvent,
|
||||||
IRSGBallotDocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent,
|
IRSGBallotDocEvent, BallotPositionDocEvent, LastCallDocEvent, WriteupDocEvent,
|
||||||
IESG_SUBSTATE_TAGS, RelatedDocument, BallotType )
|
IESG_SUBSTATE_TAGS, RelatedDocument, BallotType )
|
||||||
from ietf.doc.utils import ( add_state_change_event, close_ballot, close_open_ballots,
|
from ietf.doc.utils import ( add_state_change_event, close_ballot, close_open_ballots,
|
||||||
create_ballot_if_not_open, update_telechat )
|
create_ballot_if_not_open, update_telechat, update_action_holders )
|
||||||
from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred,
|
from ietf.doc.mails import ( email_ballot_deferred, email_ballot_undeferred,
|
||||||
extra_automation_headers, generate_last_call_announcement,
|
extra_automation_headers, generate_last_call_announcement,
|
||||||
generate_issue_ballot_mail, generate_ballot_writeup, generate_ballot_rfceditornote,
|
generate_issue_ballot_mail, generate_ballot_writeup, generate_ballot_rfceditornote,
|
||||||
|
@ -79,9 +79,12 @@ def do_undefer_ballot(request, doc):
|
||||||
doc.tags.remove(*prev_tags)
|
doc.tags.remove(*prev_tags)
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
state_change_event = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
e = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
if state_change_event:
|
if e:
|
||||||
events.append(state_change_event)
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
|
||||||
e = update_telechat(request, doc, by, telechat_date)
|
e = update_telechat(request, doc, by, telechat_date)
|
||||||
if e:
|
if e:
|
||||||
|
@ -453,9 +456,12 @@ def defer_ballot(request, name):
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
|
|
||||||
state_change_event = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
if state_change_event:
|
if e:
|
||||||
events.append(state_change_event)
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
|
||||||
e = update_telechat(request, doc, login, telechat_date)
|
e = update_telechat(request, doc, login, telechat_date)
|
||||||
if e:
|
if e:
|
||||||
|
@ -547,10 +553,16 @@ def lastcalltext(request, name):
|
||||||
doc.set_state(new_state)
|
doc.set_state(new_state)
|
||||||
doc.tags.remove(*prev_tags)
|
doc.tags.remove(*prev_tags)
|
||||||
|
|
||||||
|
events = []
|
||||||
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
|
|
||||||
if e:
|
if e:
|
||||||
doc.save_with_history([e])
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
|
||||||
|
if events:
|
||||||
|
doc.save_with_history(events)
|
||||||
|
|
||||||
request_last_call(request, doc)
|
request_last_call(request, doc)
|
||||||
|
|
||||||
|
@ -628,9 +640,15 @@ def ballot_writeupnotes(request, name):
|
||||||
doc.set_state(new_state)
|
doc.set_state(new_state)
|
||||||
doc.tags.remove(*prev_tags)
|
doc.tags.remove(*prev_tags)
|
||||||
|
|
||||||
sce = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
events = []
|
||||||
if sce:
|
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
doc.save_with_history([sce])
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
if events:
|
||||||
|
doc.save_with_history(events)
|
||||||
|
|
||||||
if not ballot_already_approved:
|
if not ballot_already_approved:
|
||||||
e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore
|
e = create_ballot_if_not_open(request, doc, login, "approve") # pyflakes:ignore
|
||||||
|
@ -893,9 +911,11 @@ def approve_ballot(request, name):
|
||||||
e.desc = "IESG has approved the document"
|
e.desc = "IESG has approved the document"
|
||||||
e.save()
|
e.save()
|
||||||
events.append(e)
|
events.append(e)
|
||||||
|
|
||||||
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
|
||||||
|
|
||||||
|
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
|
||||||
if e:
|
if e:
|
||||||
events.append(e)
|
events.append(e)
|
||||||
|
|
||||||
|
@ -1038,6 +1058,9 @@ def make_last_call(request, name):
|
||||||
doc.tags.remove(*prev_tags)
|
doc.tags.remove(*prev_tags)
|
||||||
|
|
||||||
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
e = add_state_change_event(doc, login, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
if e:
|
if e:
|
||||||
events.append(e)
|
events.append(e)
|
||||||
expiration_date = form.cleaned_data['last_call_expiration_date']
|
expiration_date = form.cleaned_data['last_call_expiration_date']
|
||||||
|
@ -1129,9 +1152,12 @@ def issue_irsg_ballot(request, name):
|
||||||
doc.tags.remove(*prev_tags)
|
doc.tags.remove(*prev_tags)
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
state_change_event = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
e = add_state_change_event(doc, by, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
if state_change_event:
|
if e:
|
||||||
events.append(state_change_event)
|
events.append(e)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
|
||||||
if events:
|
if events:
|
||||||
doc.save_with_history(events)
|
doc.save_with_history(events)
|
||||||
|
|
|
@ -54,21 +54,21 @@ import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDocEvent, BallotType,
|
from ietf.doc.models import ( Document, DocAlias, DocHistory, DocEvent, BallotDocEvent, BallotType,
|
||||||
ConsensusDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent, IanaExpertDocEvent,
|
ConsensusDocEvent, NewRevisionDocEvent, TelechatDocEvent, WriteupDocEvent, IanaExpertDocEvent,
|
||||||
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS )
|
IESG_BALLOT_ACTIVE_STATES, STATUSCHANGE_RELATIONS, DocumentActionHolder )
|
||||||
from ietf.doc.utils import (add_links_in_new_revision_events, augment_events_with_revision,
|
from ietf.doc.utils import (add_links_in_new_revision_events, augment_events_with_revision,
|
||||||
can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id,
|
can_adopt_draft, can_unadopt_draft, get_chartering_type, get_tags_for_stream_id,
|
||||||
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
|
needed_ballot_positions, nice_consensus, prettify_std_name, update_telechat, has_same_ballot,
|
||||||
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
|
get_initial_notify, make_notify_changed_event, make_rev_history, default_consensus,
|
||||||
add_events_message_info, get_unicode_document_content, build_doc_meta_block,
|
add_events_message_info, get_unicode_document_content, build_doc_meta_block,
|
||||||
augment_docs_and_user_with_user_info, irsg_needed_ballot_positions )
|
augment_docs_and_user_with_user_info, irsg_needed_ballot_positions, add_action_holder_change_event )
|
||||||
from ietf.group.models import Role, Group
|
from ietf.group.models import Role, Group
|
||||||
from ietf.group.utils import can_manage_group_type, can_manage_materials, group_features_role_filter
|
from ietf.group.utils import can_manage_group_type, can_manage_materials, group_features_role_filter
|
||||||
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,
|
||||||
role_required, is_individual_draft_author)
|
role_required, is_individual_draft_author)
|
||||||
from ietf.name.models import StreamName, BallotPositionName
|
from ietf.name.models import StreamName, BallotPositionName
|
||||||
from ietf.utils.history import find_history_active_at
|
from ietf.utils.history import find_history_active_at
|
||||||
from ietf.doc.forms import TelechatForm, NotifyForm
|
from ietf.doc.forms import TelechatForm, NotifyForm, ActionHoldersForm
|
||||||
from ietf.doc.mails import email_comment
|
from ietf.doc.mails import email_comment, email_remind_action_holders
|
||||||
from ietf.mailtrigger.utils import gather_relevant_expansions
|
from ietf.mailtrigger.utils import gather_relevant_expansions
|
||||||
from ietf.meeting.models import Session
|
from ietf.meeting.models import Session
|
||||||
from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions, add_event_info_to_session_qs
|
from ietf.meeting.utils import group_sessions, get_upcoming_manageable_sessions, sort_sessions, add_event_info_to_session_qs
|
||||||
|
@ -1275,6 +1275,14 @@ def telechat_date(request, name):
|
||||||
warnings=warnings,
|
warnings=warnings,
|
||||||
login=login))
|
login=login))
|
||||||
|
|
||||||
|
|
||||||
|
def doc_titletext(doc):
|
||||||
|
if doc.type.slug=='conflrev':
|
||||||
|
conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document
|
||||||
|
return 'the conflict review of %s' % conflictdoc.canonical_name()
|
||||||
|
return doc.canonical_name()
|
||||||
|
|
||||||
|
|
||||||
def edit_notify(request, name):
|
def edit_notify(request, name):
|
||||||
"""Change the set of email addresses document change notificaitions go to."""
|
"""Change the set of email addresses document change notificaitions go to."""
|
||||||
|
|
||||||
|
@ -1311,18 +1319,150 @@ def edit_notify(request, name):
|
||||||
init = { "notify" : doc.notify }
|
init = { "notify" : doc.notify }
|
||||||
form = NotifyForm(initial=init)
|
form = NotifyForm(initial=init)
|
||||||
|
|
||||||
if doc.type.slug=='conflrev':
|
|
||||||
conflictdoc = doc.relateddocument_set.get(relationship__slug='conflrev').target.document
|
|
||||||
titletext = 'the conflict review of %s' % conflictdoc.canonical_name()
|
|
||||||
else:
|
|
||||||
titletext = '%s' % doc.canonical_name()
|
|
||||||
return render(request, 'doc/edit_notify.html',
|
return render(request, 'doc/edit_notify.html',
|
||||||
{'form': form,
|
{'form': form,
|
||||||
'doc': doc,
|
'doc': doc,
|
||||||
'titletext': titletext,
|
'titletext': doc_titletext(doc),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@role_required('Area Director', 'Secretariat')
|
||||||
|
def edit_action_holders(request, name):
|
||||||
|
"""Change the set of action holders for a doc"""
|
||||||
|
doc = get_object_or_404(Document, name=name)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ActionHoldersForm(request.POST)
|
||||||
|
if form.is_valid() and 'action_holders' in request.POST:
|
||||||
|
new_action_holders = form.cleaned_data['action_holders'] # Person queryset
|
||||||
|
prev_action_holders = list(doc.action_holders.all())
|
||||||
|
|
||||||
|
# Now update the action holders. We can't use the simple approach of clearing
|
||||||
|
# the set and then adding back the entire new_action_holders. If we did that,
|
||||||
|
# the timestamps that track when each person became an action holder would
|
||||||
|
# reset every time the list was modified. So we need to be careful only
|
||||||
|
# to delete the ones that are really being removed.
|
||||||
|
#
|
||||||
|
# Also need to take care not to delete the people! doc.action_holders.all()
|
||||||
|
# (and other querysets) give the Person objects. We only want to add/delete
|
||||||
|
# the DocumentActionHolder 'through' model objects. That means working directly
|
||||||
|
# with the model or using doc.action_holders.add() and .remove(), which take
|
||||||
|
# Person objects as arguments.
|
||||||
|
existing = DocumentActionHolder.objects.filter(document=doc) # through model
|
||||||
|
to_remove = existing.exclude(person__in=new_action_holders) # through model
|
||||||
|
to_remove.delete() # deletes the DocumentActionHolder objects, leaves the Person objects
|
||||||
|
|
||||||
|
# Get all the Persons who do not have a DocumentActionHolder for this document
|
||||||
|
added_people = new_action_holders.exclude(documentactionholder__document=doc)
|
||||||
|
doc.action_holders.add(*added_people)
|
||||||
|
|
||||||
|
add_action_holder_change_event(doc, request.user.person, prev_action_holders,
|
||||||
|
form.cleaned_data['reason'])
|
||||||
|
|
||||||
|
return redirect('ietf.doc.views_doc.document_main', name=doc.name)
|
||||||
|
|
||||||
|
# When not a POST
|
||||||
|
# Data for quick add/remove of various related Persons
|
||||||
|
doc_role_labels = [] # labels for document-related roles
|
||||||
|
group_role_labels = [] # labels for group-related roles
|
||||||
|
role_ids = dict() # maps role slug to list of Person IDs (assumed numeric in the JavaScript)
|
||||||
|
extra_prefetch = [] # list of Person objects to prefetch for select2 field
|
||||||
|
|
||||||
|
if len(doc.authors()) > 0:
|
||||||
|
doc_role_labels.append(dict(slug='authors', label='Authors'))
|
||||||
|
authors = doc.authors()
|
||||||
|
role_ids['authors'] = [p.pk for p in authors]
|
||||||
|
extra_prefetch += authors
|
||||||
|
|
||||||
|
if doc.ad:
|
||||||
|
doc_role_labels.append(dict(slug='ad', label='Responsible AD'))
|
||||||
|
role_ids['ad'] = [doc.ad.pk]
|
||||||
|
extra_prefetch.append(doc.ad)
|
||||||
|
|
||||||
|
if doc.shepherd:
|
||||||
|
# doc.shepherd is an Email, which is allowed not to have a Person.
|
||||||
|
# The Emails used for shepherds should always have one, though. If not, log the
|
||||||
|
# event and move on without the shepherd. This just means there will not be
|
||||||
|
# add/remove shepherd buttons.
|
||||||
|
log.assertion('doc.shepherd.person',
|
||||||
|
note="A document's shepherd should always have a Person'. Failed for %s"%doc.name)
|
||||||
|
if doc.shepherd.person:
|
||||||
|
doc_role_labels.append(dict(slug='shep', label='Shepherd'))
|
||||||
|
role_ids['shep'] = [doc.shepherd.person.pk]
|
||||||
|
extra_prefetch.append(doc.shepherd.person)
|
||||||
|
|
||||||
|
if doc.group:
|
||||||
|
# UI buttons to add / remove will appear in same order as this list
|
||||||
|
group_roles = doc.group.role_set.filter(
|
||||||
|
name__in=DocumentActionHolder.GROUP_ROLES_OF_INTEREST,
|
||||||
|
).select_related('name', 'person') # name is a RoleName
|
||||||
|
|
||||||
|
# Gather all the roles for this group
|
||||||
|
for role in group_roles:
|
||||||
|
key = 'group_%s' % role.name.slug
|
||||||
|
existing_list = role_ids.get(key)
|
||||||
|
if existing_list:
|
||||||
|
existing_list.append(role.person.pk)
|
||||||
|
else:
|
||||||
|
role_ids[key] = [role.person.pk]
|
||||||
|
group_role_labels.append(dict(
|
||||||
|
sort_order=DocumentActionHolder.GROUP_ROLES_OF_INTEREST.index(role.name.slug),
|
||||||
|
slug=key,
|
||||||
|
label='Group ' + role.name.name, # friendly role name
|
||||||
|
))
|
||||||
|
extra_prefetch.append(role.person)
|
||||||
|
|
||||||
|
# Ensure group role button order is stable
|
||||||
|
group_role_labels.sort(key=lambda r: r['sort_order'])
|
||||||
|
|
||||||
|
form = ActionHoldersForm(initial={'action_holders': doc.action_holders.all()})
|
||||||
|
form.fields['action_holders'].extra_prefetch = extra_prefetch
|
||||||
|
form.fields['action_holders'].widget.attrs["data-role-ids"] = json.dumps(role_ids)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'doc/edit_action_holders.html',
|
||||||
|
{
|
||||||
|
'form': form,
|
||||||
|
'doc': doc,
|
||||||
|
'titletext': doc_titletext(doc),
|
||||||
|
'role_labels': doc_role_labels + group_role_labels,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReminderEmailForm(forms.Form):
|
||||||
|
note = forms.CharField(
|
||||||
|
widget=forms.Textarea,
|
||||||
|
label='Note to action holders',
|
||||||
|
help_text='Optional message to the action holders',
|
||||||
|
required=False,
|
||||||
|
strip=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
@role_required('Area Director', 'Secretariat')
|
||||||
|
def remind_action_holders(request, name):
|
||||||
|
doc = get_object_or_404(Document, name=name)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ReminderEmailForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
email_remind_action_holders(request, doc, form.cleaned_data['note'])
|
||||||
|
return redirect('ietf.doc.views_doc.document_main', name=doc.canonical_name())
|
||||||
|
|
||||||
|
form = ReminderEmailForm()
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
'doc/remind_action_holders.html',
|
||||||
|
{
|
||||||
|
'form': form,
|
||||||
|
'doc': doc,
|
||||||
|
'titletext': doc_titletext(doc),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def email_aliases(request,name=''):
|
def email_aliases(request,name=''):
|
||||||
doc = get_object_or_404(Document, name=name) if name else None
|
doc = get_object_or_404(Document, name=name) if name else None
|
||||||
if not name:
|
if not name:
|
||||||
|
|
|
@ -33,7 +33,7 @@ from ietf.doc.mails import ( email_pulled_from_rfc_queue, email_resurrect_reques
|
||||||
email_iesg_processing_document, email_ad_approved_doc,
|
email_iesg_processing_document, email_ad_approved_doc,
|
||||||
email_iana_expert_review_state_changed )
|
email_iana_expert_review_state_changed )
|
||||||
from ietf.doc.utils import ( add_state_change_event, can_adopt_draft, can_unadopt_draft,
|
from ietf.doc.utils import ( add_state_change_event, can_adopt_draft, can_unadopt_draft,
|
||||||
get_tags_for_stream_id, nice_consensus,
|
get_tags_for_stream_id, nice_consensus, update_action_holders,
|
||||||
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify,
|
update_reminder, update_telechat, make_notify_changed_event, get_initial_notify,
|
||||||
set_replaces_for_document, default_consensus, tags_suffix, )
|
set_replaces_for_document, default_consensus, tags_suffix, )
|
||||||
from ietf.doc.lastcall import request_last_call
|
from ietf.doc.lastcall import request_last_call
|
||||||
|
@ -115,6 +115,7 @@ def change_state(request, name):
|
||||||
|
|
||||||
events = []
|
events = []
|
||||||
|
|
||||||
|
|
||||||
e = add_state_change_event(doc, login, prev_state, new_state,
|
e = add_state_change_event(doc, login, prev_state, new_state,
|
||||||
prev_tags=prev_tags, new_tags=new_tags)
|
prev_tags=prev_tags, new_tags=new_tags)
|
||||||
|
|
||||||
|
@ -124,6 +125,10 @@ def change_state(request, name):
|
||||||
|
|
||||||
events.append(e)
|
events.append(e)
|
||||||
|
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
|
||||||
if comment:
|
if comment:
|
||||||
c = DocEvent(type="added_comment")
|
c = DocEvent(type="added_comment")
|
||||||
c.doc = doc
|
c.doc = doc
|
||||||
|
@ -596,6 +601,9 @@ def to_iesg(request,name):
|
||||||
new_state = target_state[target_map[state_type]]
|
new_state = target_state[target_map[state_type]]
|
||||||
if not prev_state==new_state:
|
if not prev_state==new_state:
|
||||||
doc.set_state(new_state)
|
doc.set_state(new_state)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
events.append(add_state_change_event(doc=doc,by=by,prev_state=prev_state,new_state=new_state))
|
events.append(add_state_change_event(doc=doc,by=by,prev_state=prev_state,new_state=new_state))
|
||||||
|
|
||||||
if not doc.ad == ad :
|
if not doc.ad == ad :
|
||||||
|
@ -764,6 +772,16 @@ def edit_info(request, name):
|
||||||
|
|
||||||
doc.save_with_history(events)
|
doc.save_with_history(events)
|
||||||
|
|
||||||
|
if new_document:
|
||||||
|
# If we created a new doc, update the action holders as though it
|
||||||
|
# started in idexists and moved to its create_in_state. Do this
|
||||||
|
# after the doc has been updated so, e.g., doc.ad is set.
|
||||||
|
update_action_holders(
|
||||||
|
doc,
|
||||||
|
State.objects.get(type='draft-iesg', slug='idexists'),
|
||||||
|
r['create_in_state']
|
||||||
|
)
|
||||||
|
|
||||||
if changes:
|
if changes:
|
||||||
email_iesg_processing_document(request, doc, changes)
|
email_iesg_processing_document(request, doc, changes)
|
||||||
|
|
||||||
|
|
|
@ -205,19 +205,32 @@ class GroupPagesTests(TestCase):
|
||||||
group = GroupFactory()
|
group = GroupFactory()
|
||||||
setup_default_community_list_for_group(group)
|
setup_default_community_list_for_group(group)
|
||||||
draft = WgDraftFactory(group=group)
|
draft = WgDraftFactory(group=group)
|
||||||
|
draft.action_holders.set([PersonFactory()])
|
||||||
draft2 = WgDraftFactory(group=group)
|
draft2 = WgDraftFactory(group=group)
|
||||||
|
draft3 = WgDraftFactory(group=group)
|
||||||
|
draft3.set_state(State.objects.get(type='draft-iesg', slug='pub-req'))
|
||||||
|
draft3.action_holders.set(PersonFactory.create_batch(2))
|
||||||
|
old_dah = draft3.documentactionholder_set.first()
|
||||||
|
old_dah.time_added -= datetime.timedelta(days=173) # make an "old" action holder
|
||||||
|
old_dah.save()
|
||||||
|
|
||||||
clist = CommunityList.objects.get(group=group)
|
clist = CommunityList.objects.get(group=group)
|
||||||
related_docs_rule = clist.searchrule_set.get(rule_type='name_contains')
|
related_docs_rule = clist.searchrule_set.get(rule_type='name_contains')
|
||||||
reset_name_contains_index_for_rule(related_docs_rule)
|
reset_name_contains_index_for_rule(related_docs_rule)
|
||||||
|
|
||||||
for url in group_urlreverse_list(group, 'ietf.group.views.group_documents'):
|
for url in group_urlreverse_list(group, 'ietf.group.views.group_documents'):
|
||||||
r = self.client.get(url)
|
with self.settings(DOC_ACTION_HOLDER_MAX_AGE_DAYS=20):
|
||||||
|
r = self.client.get(url)
|
||||||
self.assertEqual(r.status_code, 200)
|
self.assertEqual(r.status_code, 200)
|
||||||
self.assertContains(r, draft.name)
|
self.assertContains(r, draft.name)
|
||||||
self.assertContains(r, group.name)
|
self.assertContains(r, group.name)
|
||||||
self.assertContains(r, group.acronym)
|
self.assertContains(r, group.acronym)
|
||||||
|
self.assertNotContains(r, draft.action_holders.first().plain_name())
|
||||||
self.assertContains(r, draft2.name)
|
self.assertContains(r, draft2.name)
|
||||||
|
self.assertContains(r, draft3.name)
|
||||||
|
for ah in draft3.action_holders.all():
|
||||||
|
self.assertContains(r, ah.plain_name())
|
||||||
|
self.assertContains(r, 'for 173 days', count=1) # the old_dah should be tagged
|
||||||
|
|
||||||
# Make sure that a logged in user is presented with an opportunity to add results to their community list
|
# Make sure that a logged in user is presented with an opportunity to add results to their community list
|
||||||
self.client.login(username="secretary", password="secretary+password")
|
self.client.login(username="secretary", password="secretary+password")
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
# Copyright The IETF Trust 2020 All Rights Reserved
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
def forward(apps,schema_editor):
|
||||||
|
MailTrigger = apps.get_model('mailtrigger', 'MailTrigger')
|
||||||
|
Recipient = apps.get_model('mailtrigger', 'Recipient')
|
||||||
|
|
||||||
|
(doc_action_holders, _) = Recipient.objects.get_or_create(
|
||||||
|
slug='doc_action_holders',
|
||||||
|
desc='Action holders for a document',
|
||||||
|
template='{% for action_holder in doc.action_holders.all %}{% if doc.shepherd and action_holder == doc.shepherd.person %}{{ doc.shepherd }}{% else %}{{ action_holder.email }}{% endif %}{% if not forloop.last %},{%endif %}{% endfor %}',
|
||||||
|
)
|
||||||
|
(doc_remind_action_holders, _) = MailTrigger.objects.get_or_create(
|
||||||
|
slug='doc_remind_action_holders',
|
||||||
|
desc='Recipients when sending a reminder email to action holders for a document',
|
||||||
|
)
|
||||||
|
doc_remind_action_holders.to.set([doc_action_holders])
|
||||||
|
|
||||||
|
|
||||||
|
def reverse(apps,schema_editor):
|
||||||
|
MailTrigger = apps.get_model('mailtrigger', 'MailTrigger')
|
||||||
|
Recipient = apps.get_model('mailtrigger', 'Recipient')
|
||||||
|
|
||||||
|
MailTrigger.objects.filter(slug='doc_remind_action_holders').delete()
|
||||||
|
Recipient.objects.filter(slug='doc_action_holders').delete()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('mailtrigger', '0020_add_ad_approval_request_mailtriggers'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward, reverse)
|
||||||
|
]
|
|
@ -3573,6 +3573,17 @@
|
||||||
"model": "mailtrigger.mailtrigger",
|
"model": "mailtrigger.mailtrigger",
|
||||||
"pk": "doc_pulled_from_rfc_queue"
|
"pk": "doc_pulled_from_rfc_queue"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"cc": [],
|
||||||
|
"desc": "Recipients when sending a reminder email to action holders for a document",
|
||||||
|
"to": [
|
||||||
|
"doc_action_holders"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"model": "mailtrigger.mailtrigger",
|
||||||
|
"pk": "doc_remind_action_holders"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"cc": [],
|
"cc": [],
|
||||||
|
@ -4999,6 +5010,14 @@
|
||||||
"model": "mailtrigger.recipient",
|
"model": "mailtrigger.recipient",
|
||||||
"pk": "conflict_review_stream_manager"
|
"pk": "conflict_review_stream_manager"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fields": {
|
||||||
|
"desc": "Action holders for a document",
|
||||||
|
"template": "{% for action_holder in doc.action_holders.all %}{% if doc.shepherd and action_holder == doc.shepherd.person %}{{ doc.shepherd }}{% else %}{{ action_holder.email }}{% endif %}{% if not forloop.last %},{%endif %}{% endfor %}"
|
||||||
|
},
|
||||||
|
"model": "mailtrigger.recipient",
|
||||||
|
"pk": "doc_action_holders"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fields": {
|
"fields": {
|
||||||
"desc": "The document's responsible Area Director",
|
"desc": "The document's responsible Area Director",
|
||||||
|
|
|
@ -16,7 +16,7 @@ import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.person.models import Email, Person
|
from ietf.person.models import Email, Person
|
||||||
|
|
||||||
def select2_id_name_json(objs):
|
def select2_id_name(objs):
|
||||||
def format_email(e):
|
def format_email(e):
|
||||||
return escape("%s <%s>" % (e.person.name, e.address))
|
return escape("%s <%s>" % (e.person.name, e.address))
|
||||||
def format_person(p):
|
def format_person(p):
|
||||||
|
@ -33,10 +33,13 @@ def select2_id_name_json(objs):
|
||||||
for p in objs:
|
for p in objs:
|
||||||
p.name_count = c[p.name]
|
p.name_count = c[p.name]
|
||||||
|
|
||||||
|
|
||||||
formatter = format_email if objs and isinstance(objs[0], Email) else format_person
|
formatter = format_email if objs and isinstance(objs[0], Email) else format_person
|
||||||
|
return [{ "id": o.pk, "text": formatter(o) } for o in objs if o]
|
||||||
|
|
||||||
|
|
||||||
|
def select2_id_name_json(objs):
|
||||||
|
return json.dumps(select2_id_name(objs))
|
||||||
|
|
||||||
return json.dumps([{ "id": o.pk, "text": formatter(o) } for o in objs if o])
|
|
||||||
|
|
||||||
class SearchablePersonsField(forms.CharField):
|
class SearchablePersonsField(forms.CharField):
|
||||||
"""Server-based multi-select field for choosing
|
"""Server-based multi-select field for choosing
|
||||||
|
@ -48,12 +51,19 @@ class SearchablePersonsField(forms.CharField):
|
||||||
|
|
||||||
The field uses a comma-separated list of primary keys in a
|
The field uses a comma-separated list of primary keys in a
|
||||||
CharField element as its API with some extra attributes used by
|
CharField element as its API with some extra attributes used by
|
||||||
the Javascript part."""
|
the Javascript part.
|
||||||
|
|
||||||
|
If the field will be programmatically updated, any model instances
|
||||||
|
that may be added to the initial set should be included in the extra_prefetch
|
||||||
|
list. These can then be added by updating val() and triggering the 'change'
|
||||||
|
event on the select2 field in JavaScript.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
max_entries=None, # max number of selected objs
|
max_entries=None, # max number of selected objs
|
||||||
only_users=False, # only select persons who also have a user
|
only_users=False, # only select persons who also have a user
|
||||||
all_emails=False, # select only active email addresses
|
all_emails=False, # select only active email addresses
|
||||||
|
extra_prefetch=None, # extra data records to include in prefetch
|
||||||
model=Person, # or Email
|
model=Person, # or Email
|
||||||
hint_text="Type in name to search for person.",
|
hint_text="Type in name to search for person.",
|
||||||
*args, **kwargs):
|
*args, **kwargs):
|
||||||
|
@ -70,6 +80,9 @@ class SearchablePersonsField(forms.CharField):
|
||||||
self.widget.attrs["data-placeholder"] = hint_text
|
self.widget.attrs["data-placeholder"] = hint_text
|
||||||
if self.max_entries != None:
|
if self.max_entries != None:
|
||||||
self.widget.attrs["data-max-entries"] = self.max_entries
|
self.widget.attrs["data-max-entries"] = self.max_entries
|
||||||
|
|
||||||
|
self.extra_prefetch = extra_prefetch or []
|
||||||
|
assert all([isinstance(obj, self.model) for obj in self.extra_prefetch])
|
||||||
|
|
||||||
def parse_select2_value(self, value):
|
def parse_select2_value(self, value):
|
||||||
return [x.strip() for x in value.split(",") if x.strip()]
|
return [x.strip() for x in value.split(",") if x.strip()]
|
||||||
|
@ -96,7 +109,12 @@ class SearchablePersonsField(forms.CharField):
|
||||||
if isinstance(value, self.model):
|
if isinstance(value, self.model):
|
||||||
value = [value]
|
value = [value]
|
||||||
|
|
||||||
self.widget.attrs["data-pre"] = select2_id_name_json(value)
|
# data-pre is a map from ID to full data. It includes records needed by the
|
||||||
|
# initial value of the field plus any added via extra_prefetch.
|
||||||
|
prefetch_set = set(value).union(set(self.extra_prefetch)) # eliminate duplicates
|
||||||
|
self.widget.attrs["data-pre"] = json.dumps({
|
||||||
|
d['id']: d for d in select2_id_name(list(prefetch_set))
|
||||||
|
})
|
||||||
|
|
||||||
# doing this in the constructor is difficult because the URL
|
# doing this in the constructor is difficult because the URL
|
||||||
# patterns may not have been fully constructed there yet
|
# patterns may not have been fully constructed there yet
|
||||||
|
|
|
@ -10,11 +10,12 @@ import debug # pyflakes:ignore
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from ietf.doc.factories import WgDraftFactory, IndividualRfcFactory, CharterFactory
|
from ietf.doc.factories import WgDraftFactory, IndividualRfcFactory, CharterFactory
|
||||||
from ietf.doc.models import BallotDocEvent, BallotType, BallotPositionDocEvent
|
from ietf.doc.models import BallotDocEvent, BallotType, BallotPositionDocEvent, State, Document
|
||||||
from ietf.doc.utils import update_telechat, create_ballot_if_not_open
|
from ietf.doc.utils import update_telechat, create_ballot_if_not_open
|
||||||
from ietf.utils.test_utils import TestCase
|
from ietf.utils.test_utils import TestCase
|
||||||
from ietf.iesg.models import TelechatDate
|
from ietf.iesg.models import TelechatDate
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
|
from ietf.person.factories import PersonFactory
|
||||||
from ietf.secr.telechat.views import get_next_telechat_date
|
from ietf.secr.telechat.views import get_next_telechat_date
|
||||||
|
|
||||||
SECR_USER='secretary'
|
SECR_USER='secretary'
|
||||||
|
@ -180,3 +181,62 @@ class SecrTelechatTestCase(TestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code,302)
|
self.assertEqual(response.status_code,302)
|
||||||
self.assertEqual(charter.get_state('charter').slug,'notrev')
|
self.assertEqual(charter.get_state('charter').slug,'notrev')
|
||||||
|
|
||||||
|
def test_doc_detail_post_update_state_action_holder_automation(self):
|
||||||
|
"""Updating IESG state of a draft should update action holders"""
|
||||||
|
by = Person.objects.get(name='(System)')
|
||||||
|
draft = WgDraftFactory(
|
||||||
|
states=[('draft-iesg', 'iesg-eva')],
|
||||||
|
ad=Person.objects.get(user__username='ad'),
|
||||||
|
authors=PersonFactory.create_batch(3),
|
||||||
|
)
|
||||||
|
last_week = datetime.date.today()-datetime.timedelta(days=7)
|
||||||
|
BallotDocEvent.objects.create(type='created_ballot',by=by,doc=draft, rev=draft.rev,
|
||||||
|
ballot_type=BallotType.objects.get(doc_type=draft.type,slug='approve'),
|
||||||
|
time=last_week)
|
||||||
|
d = get_next_telechat_date()
|
||||||
|
date = d.strftime('%Y-%m-%d')
|
||||||
|
update_telechat(None, draft, by, d)
|
||||||
|
url = reverse('ietf.secr.telechat.views.doc_detail', kwargs={'date':date, 'name':draft.name})
|
||||||
|
self.client.login(username="secretary", password="secretary+password")
|
||||||
|
|
||||||
|
# Check that there are no action holder DocEvents yet
|
||||||
|
self.assertEqual(draft.docevent_set.filter(type='changed_action_holders').count(), 0)
|
||||||
|
|
||||||
|
# setting to defer should add AD, adding need-rev should add authors
|
||||||
|
response = self.client.post(url,{
|
||||||
|
'submit': 'update_state',
|
||||||
|
'state': State.objects.get(type_id='draft-iesg', slug='defer').pk,
|
||||||
|
'substate': 'need-rev',
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code,302)
|
||||||
|
draft = Document.objects.get(name=draft.name)
|
||||||
|
self.assertEqual(draft.get_state('draft-iesg').slug,'defer')
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [draft.ad] + draft.authors())
|
||||||
|
self.assertEqual(draft.docevent_set.filter(type='changed_action_holders').count(), 1)
|
||||||
|
|
||||||
|
# Removing need-rev should remove authors
|
||||||
|
response = self.client.post(url,{
|
||||||
|
'submit': 'update_state',
|
||||||
|
'state': State.objects.get(type_id='draft-iesg', slug='iesg-eva').pk,
|
||||||
|
'substate': '',
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code,302)
|
||||||
|
draft = Document.objects.get(name=draft.name)
|
||||||
|
self.assertEqual(draft.get_state('draft-iesg').slug,'iesg-eva')
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [draft.ad])
|
||||||
|
self.assertEqual(draft.docevent_set.filter(type='changed_action_holders').count(), 2)
|
||||||
|
|
||||||
|
# Setting to approved should remove all action holders
|
||||||
|
# noinspection DjangoOrm
|
||||||
|
draft.action_holders.add(*(draft.authors())) # add() with through model ok in Django 2.2+
|
||||||
|
response = self.client.post(url,{
|
||||||
|
'submit': 'update_state',
|
||||||
|
'state': State.objects.get(type_id='draft-iesg', slug='approved').pk,
|
||||||
|
'substate': '',
|
||||||
|
})
|
||||||
|
self.assertEqual(response.status_code,302)
|
||||||
|
draft = Document.objects.get(name=draft.name)
|
||||||
|
self.assertEqual(draft.get_state('draft-iesg').slug,'approved')
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [])
|
||||||
|
self.assertEqual(draft.docevent_set.filter(type='changed_action_holders').count(), 3)
|
||||||
|
|
|
@ -12,7 +12,7 @@ from django.utils.functional import curry
|
||||||
import debug # pyflakes:ignore
|
import debug # pyflakes:ignore
|
||||||
|
|
||||||
from ietf.doc.models import DocEvent, Document, BallotDocEvent, BallotPositionDocEvent, BallotType, WriteupDocEvent
|
from ietf.doc.models import DocEvent, Document, BallotDocEvent, BallotPositionDocEvent, BallotType, WriteupDocEvent
|
||||||
from ietf.doc.utils import add_state_change_event
|
from ietf.doc.utils import add_state_change_event, update_action_holders
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
from ietf.doc.lastcall import request_last_call
|
from ietf.doc.lastcall import request_last_call
|
||||||
from ietf.doc.mails import email_state_changed
|
from ietf.doc.mails import email_state_changed
|
||||||
|
@ -284,12 +284,18 @@ def doc_detail(request, date, name):
|
||||||
doc.tags.remove(*prev_tags)
|
doc.tags.remove(*prev_tags)
|
||||||
doc.tags.add(*new_tags)
|
doc.tags.add(*new_tags)
|
||||||
|
|
||||||
e = add_state_change_event(doc, login, prev_state, new_state,
|
events = []
|
||||||
|
sce = add_state_change_event(doc, login, prev_state, new_state,
|
||||||
prev_tags=prev_tags, new_tags=new_tags)
|
prev_tags=prev_tags, new_tags=new_tags)
|
||||||
|
if sce:
|
||||||
|
events.append(sce)
|
||||||
|
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=new_tags)
|
||||||
if e:
|
if e:
|
||||||
doc.save_with_history([e])
|
events.append(e)
|
||||||
|
if events:
|
||||||
|
doc.save_with_history(events)
|
||||||
|
|
||||||
email_state_changed(request, doc, e.desc, 'doc_state_edited')
|
email_state_changed(request, doc, sce.desc, 'doc_state_edited')
|
||||||
|
|
||||||
if new_state.slug == "lc-req":
|
if new_state.slug == "lc-req":
|
||||||
request_last_call(request, doc)
|
request_last_call(request, doc)
|
||||||
|
|
|
@ -705,6 +705,9 @@ DOC_HREFS = {
|
||||||
# e.g. a charter or a review. Must be a tuple, not a list.
|
# e.g. a charter or a review. Must be a tuple, not a list.
|
||||||
DOC_TEXT_FILE_VALID_UPLOAD_MIME_TYPES = ('text/plain', 'text/markdown', 'text/x-rst', 'text/x-markdown', )
|
DOC_TEXT_FILE_VALID_UPLOAD_MIME_TYPES = ('text/plain', 'text/markdown', 'text/x-rst', 'text/x-markdown', )
|
||||||
|
|
||||||
|
# Age limit before action holders are flagged in the document display
|
||||||
|
DOC_ACTION_HOLDER_AGE_LIMIT_DAYS = 20
|
||||||
|
|
||||||
# Override this in settings_local.py if needed
|
# Override this in settings_local.py if needed
|
||||||
CACHE_MIDDLEWARE_SECONDS = 300
|
CACHE_MIDDLEWARE_SECONDS = 300
|
||||||
CACHE_MIDDLEWARE_KEY_PREFIX = ''
|
CACHE_MIDDLEWARE_KEY_PREFIX = ''
|
||||||
|
|
|
@ -6,7 +6,7 @@ function setupSelect2Field(e) {
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var maxEntries = e.data("max-entries");
|
var maxEntries = e.data("max-entries");
|
||||||
var multiple = maxEntries != 1;
|
var multiple = maxEntries !== 1;
|
||||||
var prefetched = e.data("pre");
|
var prefetched = e.data("pre");
|
||||||
e.select2({
|
e.select2({
|
||||||
multiple: multiple,
|
multiple: multiple,
|
||||||
|
@ -27,7 +27,7 @@ function setupSelect2Field(e) {
|
||||||
results: function (results) {
|
results: function (results) {
|
||||||
return {
|
return {
|
||||||
results: results,
|
results: results,
|
||||||
more: results.length == 10
|
more: results.length === 10
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -35,11 +35,27 @@ function setupSelect2Field(e) {
|
||||||
return m;
|
return m;
|
||||||
},
|
},
|
||||||
initSelection: function (element, cb) {
|
initSelection: function (element, cb) {
|
||||||
if (!multiple && prefetched.length > 0)
|
element = $(element); // jquerify
|
||||||
cb(prefetched[0]);
|
|
||||||
else
|
|
||||||
cb(prefetched);
|
|
||||||
|
|
||||||
|
// The original data set will contain any values looked up via ajax
|
||||||
|
var data = element.select2('data');
|
||||||
|
var data_map = {};
|
||||||
|
|
||||||
|
// map id to its data representation
|
||||||
|
for (var ii = 0; ii < data.length; ii++) {
|
||||||
|
var this_item = data[ii];
|
||||||
|
data_map[this_item.id] = this_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert values to data objects, letting element data supersede prefetch
|
||||||
|
var ids = element.val().split(',');
|
||||||
|
if (!multiple && ids.length > 0) {
|
||||||
|
cb(data_map[ids[0]] || prefetched[ids[0]]);
|
||||||
|
} else {
|
||||||
|
cb(ids.map(function(id) {
|
||||||
|
return data_map[id] || prefetched[id];
|
||||||
|
}));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
dropdownCssClass: "bigdrop"
|
dropdownCssClass: "bigdrop"
|
||||||
});
|
});
|
||||||
|
|
|
@ -18,7 +18,7 @@ import debug # pyflakes:ignore
|
||||||
from ietf.doc.models import ( Document, DocAlias, State, StateType, DocEvent, DocRelationshipName,
|
from ietf.doc.models import ( Document, DocAlias, State, StateType, DocEvent, DocRelationshipName,
|
||||||
DocTagName, DocTypeName, RelatedDocument )
|
DocTagName, DocTypeName, RelatedDocument )
|
||||||
from ietf.doc.expire import move_draft_files_to_archive
|
from ietf.doc.expire import move_draft_files_to_archive
|
||||||
from ietf.doc.utils import add_state_change_event, prettify_std_name
|
from ietf.doc.utils import add_state_change_event, prettify_std_name, update_action_holders
|
||||||
from ietf.group.models import Group
|
from ietf.group.models import Group
|
||||||
from ietf.name.models import StdLevelName, StreamName
|
from ietf.name.models import StdLevelName, StreamName
|
||||||
from ietf.person.models import Person
|
from ietf.person.models import Person
|
||||||
|
@ -186,6 +186,9 @@ def update_drafts_from_queue(drafts):
|
||||||
|
|
||||||
d.set_state(next_iesg_state)
|
d.set_state(next_iesg_state)
|
||||||
e = add_state_change_event(d, system, prev_iesg_state, next_iesg_state)
|
e = add_state_change_event(d, system, prev_iesg_state, next_iesg_state)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
e = update_action_holders(d, prev_iesg_state, next_iesg_state)
|
||||||
if e:
|
if e:
|
||||||
events.append(e)
|
events.append(e)
|
||||||
changed.add(name)
|
changed.add(name)
|
||||||
|
@ -457,12 +460,16 @@ def update_docs_from_rfc_index(index_data, errata_data, skip_older_than_date=Non
|
||||||
rfc_published = True
|
rfc_published = True
|
||||||
|
|
||||||
for t in ("draft-iesg", "draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"):
|
for t in ("draft-iesg", "draft-stream-iab", "draft-stream-irtf", "draft-stream-ise"):
|
||||||
slug = doc.get_state_slug(t)
|
prev_state = doc.get_state(t)
|
||||||
if slug and slug not in ("pub", "idexists"):
|
if prev_state is not None:
|
||||||
new_state = State.objects.select_related("type").get(used=True, type=t, slug="pub")
|
if prev_state.slug not in ("pub", "idexists"):
|
||||||
doc.set_state(new_state)
|
new_state = State.objects.select_related("type").get(used=True, type=t, slug="pub")
|
||||||
changes.append("changed %s to %s" % (new_state.type.label, new_state))
|
doc.set_state(new_state)
|
||||||
if t == 'draft-iesg' and not slug:
|
changes.append("changed %s to %s" % (new_state.type.label, new_state))
|
||||||
|
e = update_action_holders(doc, prev_state, new_state)
|
||||||
|
if e:
|
||||||
|
events.append(e)
|
||||||
|
elif t == 'draft-iesg':
|
||||||
doc.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
doc.set_state(State.objects.get(type_id='draft-iesg', slug='idexists'))
|
||||||
|
|
||||||
def parse_relation_list(l):
|
def parse_relation_list(l):
|
||||||
|
|
|
@ -239,9 +239,14 @@ class RFCSyncTests(TestCase):
|
||||||
|
|
||||||
def test_rfc_index(self):
|
def test_rfc_index(self):
|
||||||
area = GroupFactory(type_id='area')
|
area = GroupFactory(type_id='area')
|
||||||
doc = WgDraftFactory(group__parent=area,states=[('draft-iesg','rfcqueue'),('draft-stream-ise','rfc-edit')])
|
doc = WgDraftFactory(
|
||||||
|
group__parent=area,
|
||||||
|
states=[('draft-iesg','rfcqueue'),('draft-stream-ise','rfc-edit')],
|
||||||
|
ad=Person.objects.get(user__username='ad'),
|
||||||
|
)
|
||||||
# it's a bit strange to have draft-stream-ise set when draft-iesg is set
|
# it's a bit strange to have draft-stream-ise set when draft-iesg is set
|
||||||
# too, but for testing purposes ...
|
# too, but for testing purposes ...
|
||||||
|
doc.action_holders.add(doc.ad) # not normally set, but add to be sure it's cleared
|
||||||
|
|
||||||
updated_doc = Document.objects.create(name="draft-ietf-something")
|
updated_doc = Document.objects.create(name="draft-ietf-something")
|
||||||
DocAlias.objects.create(name=updated_doc.name).docs.add(updated_doc)
|
DocAlias.objects.create(name=updated_doc.name).docs.add(updated_doc)
|
||||||
|
@ -358,9 +363,11 @@ class RFCSyncTests(TestCase):
|
||||||
|
|
||||||
doc = Document.objects.get(name=doc.name)
|
doc = Document.objects.get(name=doc.name)
|
||||||
|
|
||||||
self.assertEqual(doc.docevent_set.all()[0].type, "sync_from_rfc_editor")
|
events = doc.docevent_set.all()
|
||||||
self.assertEqual(doc.docevent_set.all()[1].type, "published_rfc")
|
self.assertEqual(events[0].type, "sync_from_rfc_editor")
|
||||||
self.assertEqual(doc.docevent_set.all()[1].time.date(), today)
|
self.assertEqual(events[1].type, "changed_action_holders")
|
||||||
|
self.assertEqual(events[2].type, "published_rfc")
|
||||||
|
self.assertEqual(events[2].time.date(), today)
|
||||||
self.assertTrue("errata" in doc.tags.all().values_list("slug", flat=True))
|
self.assertTrue("errata" in doc.tags.all().values_list("slug", flat=True))
|
||||||
self.assertTrue(DocAlias.objects.filter(name="rfc1234", docs=doc))
|
self.assertTrue(DocAlias.objects.filter(name="rfc1234", docs=doc))
|
||||||
self.assertTrue(DocAlias.objects.filter(name="bcp1", docs=doc))
|
self.assertTrue(DocAlias.objects.filter(name="bcp1", docs=doc))
|
||||||
|
@ -371,6 +378,7 @@ class RFCSyncTests(TestCase):
|
||||||
self.assertEqual(doc.abstract, "This is some interesting text.")
|
self.assertEqual(doc.abstract, "This is some interesting text.")
|
||||||
self.assertEqual(doc.get_state_slug(), "rfc")
|
self.assertEqual(doc.get_state_slug(), "rfc")
|
||||||
self.assertEqual(doc.get_state_slug("draft-iesg"), "pub")
|
self.assertEqual(doc.get_state_slug("draft-iesg"), "pub")
|
||||||
|
self.assertCountEqual(doc.action_holders.all(), [])
|
||||||
self.assertEqual(doc.get_state_slug("draft-stream-ise"), "pub")
|
self.assertEqual(doc.get_state_slug("draft-stream-ise"), "pub")
|
||||||
self.assertEqual(doc.std_level_id, "ps")
|
self.assertEqual(doc.std_level_id, "ps")
|
||||||
self.assertEqual(doc.pages, 42)
|
self.assertEqual(doc.pages, 42)
|
||||||
|
@ -413,7 +421,9 @@ class RFCSyncTests(TestCase):
|
||||||
return t
|
return t
|
||||||
|
|
||||||
def test_rfc_queue(self):
|
def test_rfc_queue(self):
|
||||||
draft = WgDraftFactory(states=[('draft-iesg','ann')])
|
draft = WgDraftFactory(states=[('draft-iesg','ann')], ad=Person.objects.get(user__username='ad'))
|
||||||
|
draft.action_holders.add(draft.ad) # add an action holder so we can test that it's removed later
|
||||||
|
|
||||||
expected_auth48_url = "http://www.rfc-editor.org/auth48/rfc1234"
|
expected_auth48_url = "http://www.rfc-editor.org/auth48/rfc1234"
|
||||||
t = self._generate_rfc_queue_xml(draft,
|
t = self._generate_rfc_queue_xml(draft,
|
||||||
state='EDIT*R*A(1G)',
|
state='EDIT*R*A(1G)',
|
||||||
|
@ -433,10 +443,13 @@ class RFCSyncTests(TestCase):
|
||||||
draft = Document.objects.get(pk=draft.pk)
|
draft = Document.objects.get(pk=draft.pk)
|
||||||
self.assertEqual(draft.get_state_slug("draft-rfceditor"), "edit")
|
self.assertEqual(draft.get_state_slug("draft-rfceditor"), "edit")
|
||||||
self.assertEqual(draft.get_state_slug("draft-iesg"), "rfcqueue")
|
self.assertEqual(draft.get_state_slug("draft-iesg"), "rfcqueue")
|
||||||
|
self.assertCountEqual(draft.action_holders.all(), [])
|
||||||
self.assertEqual(set(draft.tags.all()), set(DocTagName.objects.filter(slug__in=("iana", "ref"))))
|
self.assertEqual(set(draft.tags.all()), set(DocTagName.objects.filter(slug__in=("iana", "ref"))))
|
||||||
self.assertEqual(draft.docevent_set.all()[0].type, "changed_state") # changed draft-iesg state
|
events = draft.docevent_set.all()
|
||||||
self.assertEqual(draft.docevent_set.all()[1].type, "changed_state") # changed draft-rfceditor state
|
self.assertEqual(events[0].type, "changed_state") # changed draft-iesg state
|
||||||
self.assertEqual(draft.docevent_set.all()[2].type, "rfc_editor_received_announcement")
|
self.assertEqual(events[1].type, "changed_action_holders")
|
||||||
|
self.assertEqual(events[2].type, "changed_state") # changed draft-rfceditor state
|
||||||
|
self.assertEqual(events[3].type, "rfc_editor_received_announcement")
|
||||||
|
|
||||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||||
self.assertTrue("RFC Editor queue" in outbox[-1]["Subject"])
|
self.assertTrue("RFC Editor queue" in outbox[-1]["Subject"])
|
||||||
|
|
|
@ -457,6 +457,29 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
{% if doc.action_holders_enabled %}
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Action Holders</th>
|
||||||
|
<td class="edit">
|
||||||
|
{% if can_edit %}
|
||||||
|
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_doc.edit_action_holders' name=doc.name %}">Edit</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>
|
||||||
|
{% if doc.action_holders.exists %}
|
||||||
|
{% for action_holder in doc.documentactionholder_set.all %}
|
||||||
|
<div>{% person_link action_holder.person title=action_holder.role_for_doc %} {{ action_holder|action_holder_badge }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% if can_edit %}<a class="btn btn-default btn-xs" href="{% url "ietf.doc.views_doc.remind_action_holders" name=doc.name %}"><span class="fa fa-envelope-o"></span> Send reminder email </a>{% endif %}
|
||||||
|
{% else %}
|
||||||
|
(None)
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if consensus and doc.stream_id == 'ietf' %}
|
{% if consensus and doc.stream_id == 'ietf' %}
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
{# Copyright The IETF Trust 2015, All Rights Reserved #}
|
||||||
{% load origin %}
|
{% load origin %}
|
||||||
{% load ietf_filters static %}
|
{% load ietf_filters static %}
|
||||||
{% load textfilters %}
|
{% load textfilters person_filters %}
|
||||||
|
|
||||||
{% block pagehead %}
|
{% block pagehead %}
|
||||||
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
|
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
|
||||||
|
@ -43,6 +43,12 @@
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url "ietf.doc.views_doc.document_main" doc.name %}">{{ doc.name }}</a>
|
<a href="{% url "ietf.doc.views_doc.document_main" doc.name %}">{{ doc.name }}</a>
|
||||||
<br><b>{{ doc.title }}</b>
|
<br><b>{{ doc.title }}</b>
|
||||||
|
{% if doc.action_holders_enabled and doc.action_holders.exists %}
|
||||||
|
<br>Action holders:
|
||||||
|
{% for action_holder in doc.documentactionholder_set.all %}
|
||||||
|
{% person_link action_holder.person title=action_holder.role_for_doc %}{{ action_holder|action_holder_badge }}{% if not forloop.last %},{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% if doc.note %}
|
{% if doc.note %}
|
||||||
<br><i>Note: {{ doc.note|linkify|linebreaksbr }}</i>
|
<br><i>Note: {{ doc.note|linkify|linebreaksbr }}</i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
138
ietf/templates/doc/edit_action_holders.html
Normal file
138
ietf/templates/doc/edit_action_holders.html
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{# Copyright The IETF Trust 2020, All Rights Reserved #}
|
||||||
|
{% load origin %}
|
||||||
|
{% load static %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Edit action holders for {{ titletext }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block pagehead %}
|
||||||
|
<link rel="stylesheet" href="{% static 'select2/select2.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'select2-bootstrap-css/select2-bootstrap.min.css' %}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% origin %}
|
||||||
|
<h1>Edit action holders<br><small>{{titletext}}</small></h1>
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="role-toolbar">Related people</label>
|
||||||
|
<div class="btn-toolbar" role="toolbar" id="role-toolbar-{{ role_type_label|slugify }}">
|
||||||
|
{% for doc_role in role_labels %}
|
||||||
|
<div class="btn-group-vertical btn-group-sm" role="group">
|
||||||
|
<button type="button" class="btn btn-default"
|
||||||
|
id="add-{{ doc_role.slug }}"
|
||||||
|
onclick="local_js.add_ah('{{ doc_role.slug }}')">
|
||||||
|
Add {{ doc_role.label }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-default"
|
||||||
|
id="del-{{ doc_role.slug }}"
|
||||||
|
onclick="local_js.del_ah('{{ doc_role.slug }}')">
|
||||||
|
Remove {{ doc_role.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% buttons %}
|
||||||
|
<button type="submit" class="btn btn-primary" name="submit" value="Save">Submit</button>
|
||||||
|
<a class="btn btn-default pull-right" href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">Back</a>
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script src="{% static 'select2/select2.min.js' %}"></script>
|
||||||
|
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
local_js = function () {
|
||||||
|
let select2_elem = $('.select2-field');
|
||||||
|
let role_ids = select2_elem.data('role-ids');
|
||||||
|
|
||||||
|
/* Updates select2 selection in element elem. Data should be an array of
|
||||||
|
* objects with id and text as keys. */
|
||||||
|
function update_selection(elem, entries) {
|
||||||
|
elem.val(entries.join(',')).trigger('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_ah(role) {
|
||||||
|
if (role_ids[role]) {
|
||||||
|
let ids;
|
||||||
|
if (select2_elem.val()) {
|
||||||
|
ids = select2_elem.val().split(',').map(Number).concat(role_ids[role]);
|
||||||
|
} else {
|
||||||
|
ids = role_ids[role];
|
||||||
|
}
|
||||||
|
update_selection(select2_elem, ids);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function del_ah(role) {
|
||||||
|
if (role_ids[role] && select2_elem.val()) {
|
||||||
|
update_selection(select2_elem, select2_elem.val().split(',').filter(
|
||||||
|
function(id){return -1 === role_ids[role].indexOf(Number(id))}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function all_selected(elem, role) {
|
||||||
|
if (!elem.val()) {return false}
|
||||||
|
|
||||||
|
let data_ids = elem.val().split(',').map(Number);
|
||||||
|
for (let ii=0; ii < role_ids[role].length; ii++) {
|
||||||
|
if (-1 === data_ids.indexOf(role_ids[role][ii])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function none_selected(elem, role) {
|
||||||
|
if (!elem.val()) {return true}
|
||||||
|
|
||||||
|
let data_ids = elem.val().split(',').map(Number);
|
||||||
|
for (let ii=0; ii < role_ids[role].length; ii++) {
|
||||||
|
if (-1 !== data_ids.indexOf(role_ids[role][ii])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_buttons() {
|
||||||
|
for (let role_slug in role_ids) {
|
||||||
|
if (!role_ids.hasOwnProperty(role_slug)) { return };
|
||||||
|
|
||||||
|
if (all_selected(select2_elem, role_slug)) {
|
||||||
|
$('#add-' + role_slug).attr('disabled', true);
|
||||||
|
} else {
|
||||||
|
$('#add-' + role_slug).attr('disabled', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (none_selected(select2_elem, role_slug)) {
|
||||||
|
$('#del-' + role_slug).attr('disabled', true);
|
||||||
|
} else {
|
||||||
|
$('#del-' + role_slug).attr('disabled', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
select2_elem.on('change', update_buttons);
|
||||||
|
$(document).ready(update_buttons);
|
||||||
|
|
||||||
|
return {
|
||||||
|
add_ah: add_ah, del_ah: del_ah
|
||||||
|
};
|
||||||
|
}();
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
14
ietf/templates/doc/mail/remind_action_holders_mail.txt
Normal file
14
ietf/templates/doc/mail/remind_action_holders_mail.txt
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
{% autoescape off %}
|
||||||
|
Reminder: your action is needed to allow the publication process for
|
||||||
|
|
||||||
|
{{ doc.display_name }}
|
||||||
|
|
||||||
|
to proceed. This document can be found at
|
||||||
|
|
||||||
|
{{ doc_url }}
|
||||||
|
{% if note %}
|
||||||
|
Please note:
|
||||||
|
|
||||||
|
{{ note|wordwrap:78 }}
|
||||||
|
{% endif %}
|
||||||
|
{% endautoescape %}
|
28
ietf/templates/doc/remind_action_holders.html
Normal file
28
ietf/templates/doc/remind_action_holders.html
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{# Copyright The IETF Trust 2020, All Rights Reserved #}
|
||||||
|
{% load origin %}
|
||||||
|
{% load static %}
|
||||||
|
{% load bootstrap3 %}
|
||||||
|
{% load person_filters %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Send reminder to action holders for {{ titletext }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% origin %}
|
||||||
|
<h1>Send reminder to action holders<br><small>{{ titletext }}</small></h1>
|
||||||
|
|
||||||
|
<form enctype="multipart/form-data" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
{% bootstrap_form form %}
|
||||||
|
|
||||||
|
<p>This reminder will be sent to
|
||||||
|
{% for person in doc.action_holders.all %}
|
||||||
|
{% if forloop.last and not forloop.first %} and {% endif %}{% person_link person %}{% if not forloop.last %}, {% endif %}{% endfor %}.</p>
|
||||||
|
{% buttons %}
|
||||||
|
<button type="submit" class="btn btn-primary" name="submit" value="Send reminder">Send</button>
|
||||||
|
<a class="btn btn-default pull-right" href="{% url "ietf.doc.views_doc.document_main" name=doc.canonical_name %}">Cancel</a>
|
||||||
|
{% endbuttons %}
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
||||||
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
|
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
|
||||||
{% load ietf_filters ballot_icon %}
|
{% load ietf_filters ballot_icon person_filters %}
|
||||||
|
|
||||||
<td class="status">
|
<td class="status">
|
||||||
<div class="pull-right" id="ballot-icon-{{doc.name}}">
|
<div class="pull-right" id="ballot-icon-{{doc.name}}">
|
||||||
|
@ -61,6 +61,12 @@
|
||||||
<span title="Part of {{ m.group.acronym }} milestone: {{ m.desc }}" class="milestone">{{ m.due|date:"M Y" }}</span>{% if not forloop.last %}, {% endif %}
|
<span title="Part of {{ m.group.acronym }} milestone: {{ m.desc }}" class="milestone">{{ m.due|date:"M Y" }}</span>{% if not forloop.last %}, {% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if doc.action_holders_enabled and doc.action_holders.exists %}
|
||||||
|
<br>Action Holders:
|
||||||
|
{% for action_holder in doc.documentactionholder_set.all %}
|
||||||
|
<wbr>{% person_link action_holder.person title=action_holder.role_for_doc %}{{ action_holder|action_holder_badge }}{% if not forloop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
{% else %}{# RFC #}
|
{% else %}{# RFC #}
|
||||||
{{ doc.std_level|safe }} RFC
|
{{ doc.std_level|safe }} RFC
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue