Skip to content

Commit e11583a

Browse files
Allow assignment of Person as "action holder" for a Doc, plus rudimentary automation of assignment. Fixes ietf-tools#3146. Commit ready for merge.
- Legacy-Id: 18829
1 parent df37793 commit e11583a

36 files changed

Lines changed: 1458 additions & 100 deletions

ietf/doc/admin.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
StateDocEvent, ConsensusDocEvent, BallotType, BallotDocEvent, WriteupDocEvent, LastCallDocEvent,
1212
TelechatDocEvent, BallotPositionDocEvent, ReviewRequestDocEvent, InitialReviewDocEvent,
1313
AddedMessageEvent, SubmissionDocEvent, DeletedEvent, EditedAuthorsDocEvent, DocumentURL,
14-
ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource )
14+
ReviewAssignmentDocEvent, IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder )
1515

1616
from ietf.utils.validators import validate_external_resource_value
1717

@@ -34,6 +34,11 @@ class DocAuthorInline(admin.TabularInline):
3434
raw_id_fields = ['person', 'email']
3535
extra = 1
3636

37+
class DocActionHolderInline(admin.TabularInline):
38+
model = DocumentActionHolder
39+
raw_id_fields = ['person']
40+
extra = 1
41+
3742
class RelatedDocumentInline(admin.TabularInline):
3843
model = RelatedDocument
3944
def this(self, instance):
@@ -72,7 +77,7 @@ class DocumentAdmin(admin.ModelAdmin):
7277
search_fields = ['name']
7378
list_filter = ['type']
7479
raw_id_fields = ['group', 'shepherd', 'ad']
75-
inlines = [DocAuthorInline, RelatedDocumentInline, AdditionalUrlInLine]
80+
inlines = [DocAuthorInline, DocActionHolderInline, RelatedDocumentInline, AdditionalUrlInLine]
7681
form = DocumentForm
7782

7883
def save_model(self, request, obj, form, change):
@@ -137,6 +142,13 @@ class BallotTypeAdmin(admin.ModelAdmin):
137142
list_display = ["slug", "doc_type", "name", "question"]
138143
admin.site.register(BallotType, BallotTypeAdmin)
139144

145+
146+
class DocumentActionHolderAdmin(admin.ModelAdmin):
147+
list_display = ['id', 'document', 'person', 'time_added']
148+
raw_id_fields = ['document', 'person']
149+
admin.site.register(DocumentActionHolder, DocumentActionHolderAdmin)
150+
151+
140152
# events
141153

142154
class DocEventAdmin(admin.ModelAdmin):

ietf/doc/expire.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from ietf.doc.models import Document, DocEvent, State, IESG_SUBSTATE_TAGS
1616
from ietf.person.models import Person
1717
from ietf.meeting.models import Meeting
18-
from ietf.doc.utils import add_state_change_event
18+
from ietf.doc.utils import add_state_change_event, update_action_holders
1919
from ietf.mailtrigger.utils import gather_address_lists
2020

2121

@@ -171,6 +171,9 @@ def expire_draft(doc):
171171
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
172172
if e:
173173
events.append(e)
174+
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
175+
if e:
176+
events.append(e)
174177

175178
events.append(DocEvent.objects.create(doc=doc, rev=doc.rev, by=system, type="expired_document", desc="Document has expired"))
176179

ietf/doc/factories.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
from django.conf import settings
1313

1414
from ietf.doc.models import ( Document, DocEvent, NewRevisionDocEvent, DocAlias, State, DocumentAuthor,
15-
StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent)
15+
StateDocEvent, BallotPositionDocEvent, BallotDocEvent, BallotType, IRSGBallotDocEvent, TelechatDocEvent,
16+
DocumentActionHolder)
1617
from ietf.group.models import Group
1718

1819
def draft_name_generator(type_id,group,n):
@@ -358,3 +359,9 @@ class Meta:
358359
balloter = factory.SubFactory('ietf.person.factories.PersonFactory')
359360
pos_id = 'discuss'
360361

362+
class DocumentActionHolderFactory(factory.DjangoModelFactory):
363+
class Meta:
364+
model = DocumentActionHolder
365+
366+
document = factory.SubFactory(WgDraftFactory)
367+
person = factory.SubFactory('ietf.person.factories.PersonFactory')

ietf/doc/forms.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from ietf.doc.models import RelatedDocument
1111
from ietf.iesg.models import TelechatDate
1212
from ietf.iesg.utils import telechat_page_count
13+
from ietf.person.fields import SearchablePersonsField
14+
1315

1416
class TelechatForm(forms.Form):
1517
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 @@ def clean_notify(self):
5456
addrspecs = [x.strip() for x in self.cleaned_data["notify"].split(',')]
5557
return ', '.join(addrspecs)
5658

59+
class ActionHoldersForm(forms.Form):
60+
action_holders = SearchablePersonsField(required=False)
61+
reason = forms.CharField(
62+
label='Reason for change',
63+
required=False,
64+
max_length=255,
65+
strip=True,
66+
)
67+
5768
IESG_APPROVED_STATE_LIST = ("ann", "rfcqueue", "pub")
5869

5970
class AddDownrefForm(forms.Form):

ietf/doc/lastcall.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ietf.doc.models import Document, State, DocEvent, LastCallDocEvent, WriteupDocEvent
88
from ietf.doc.models import IESG_SUBSTATE_TAGS
99
from ietf.person.models import Person
10-
from ietf.doc.utils import add_state_change_event
10+
from ietf.doc.utils import add_state_change_event, update_action_holders
1111
from ietf.doc.mails import generate_ballot_writeup, generate_approval_mail, generate_last_call_announcement
1212
from ietf.doc.mails import send_last_call_request, email_last_call_expired, email_last_call_expired_with_downref
1313

@@ -60,9 +60,14 @@ def expire_last_call(doc):
6060
doc.tags.remove(*prev_tags)
6161

6262
system = Person.objects.get(name="(System)")
63+
events = []
6364
e = add_state_change_event(doc, system, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
6465
if e:
65-
doc.save_with_history([e])
66+
events.append(e)
67+
e = update_action_holders(doc, prev_state, new_state, prev_tags=prev_tags, new_tags=[])
68+
if e:
69+
events.append(e)
70+
doc.save_with_history(events)
6671

6772
email_last_call_expired(doc)
6873

ietf/doc/mails.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import debug # pyflakes:ignore
1616
from ietf.doc.templatetags.mail_filters import std_level_prompt
1717

18+
from ietf.utils import log
1819
from ietf.utils.mail import send_mail, send_mail_text
1920
from ietf.ipr.utils import iprs_from_docs, related_docs
2021
from ietf.doc.models import WriteupDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent
@@ -127,6 +128,22 @@ def email_iesg_processing_document(request, doc, changes):
127128
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
128129
cc=addrs.cc)
129130

131+
def email_remind_action_holders(request, doc, note=None):
132+
addrs = gather_address_lists('doc_remind_action_holders', doc=doc)
133+
log.assertion(
134+
'not doc.action_holders.exclude(email__in=addrs.to).exists()',
135+
note='All action holders should receive a reminder email. Failed for %s.' % doc.name,
136+
)
137+
send_mail(request, addrs.to, None,
138+
'Reminder: action needed for %s' % doc.display_name(),
139+
'doc/mail/remind_action_holders_mail.txt',
140+
dict(
141+
doc=doc,
142+
doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
143+
note=note,
144+
),
145+
cc=addrs.cc)
146+
130147
def html_to_text(html):
131148
return strip_tags(html.replace("&lt;", "<").replace("&gt;", ">").replace("&amp;", "&").replace("<br>", "\n"))
132149

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Generated by Django 2.2.17 on 2021-01-15 12:50
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('group', '0040_lengthen_used_roles_fields'), # only needed for schema vs data ordering
10+
('doc', '0039_auto_20201109_0439'),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name='docevent',
16+
name='type',
17+
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),
18+
),
19+
]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generated by Django 2.2.17 on 2021-01-15 12:50
2+
3+
import datetime
4+
from django.db import migrations, models
5+
import django.db.models.deletion
6+
import ietf.utils.models
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('person', '0018_auto_20201109_0439'),
13+
('doc', '0040_add_changed_action_holders_docevent_type'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='DocumentActionHolder',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('time_added', models.DateTimeField(default=datetime.datetime.now)),
22+
('document', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='doc.Document')),
23+
('person', ietf.utils.models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='person.Person')),
24+
],
25+
),
26+
migrations.AddField(
27+
model_name='document',
28+
name='action_holders',
29+
field=models.ManyToManyField(blank=True, through='doc.DocumentActionHolder', to='person.Person'),
30+
),
31+
migrations.AddConstraint(
32+
model_name='documentactionholder',
33+
constraint=models.UniqueConstraint(fields=('document', 'person'), name='unique_action_holder'),
34+
),
35+
]

ietf/doc/models.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,45 @@ def __str__(self):
630630
return u"%s %s (%s)" % (self.document.name, self.person, self.order)
631631

632632

633+
class DocumentActionHolder(models.Model):
634+
"""Action holder for a document"""
635+
document = ForeignKey('Document')
636+
person = ForeignKey(Person)
637+
time_added = models.DateTimeField(default=datetime.datetime.now)
638+
639+
CLEAR_ACTION_HOLDERS_STATES = ['approved', 'ann', 'rfcqueue', 'pub', 'dead'] # draft-iesg state slugs
640+
GROUP_ROLES_OF_INTEREST = ['chair', 'techadv', 'editor', 'secr']
641+
642+
def __str__(self):
643+
return str(self.person)
644+
645+
class Meta:
646+
constraints = [
647+
models.UniqueConstraint(fields=['document', 'person'], name='unique_action_holder')
648+
]
649+
650+
def role_for_doc(self):
651+
"""Brief string description of this person's relationship to the doc"""
652+
roles = []
653+
if self.person in self.document.authors():
654+
roles.append('Author')
655+
if self.person == self.document.ad:
656+
roles.append('Responsible AD')
657+
if self.document.shepherd and self.person == self.document.shepherd.person:
658+
roles.append('Shepherd')
659+
if self.document.group:
660+
roles.extend([
661+
'Group %s' % role.name.name
662+
for role in self.document.group.role_set.filter(
663+
name__in=self.GROUP_ROLES_OF_INTEREST,
664+
person=self.person,
665+
)
666+
])
667+
668+
if not roles:
669+
roles.append('Action Holder')
670+
return ', '.join(roles)
671+
633672
validate_docname = RegexValidator(
634673
r'^[-a-z0-9]+$',
635674
"Provide a valid document name consisting of lowercase letters, numbers and hyphens.",
@@ -638,6 +677,8 @@ def __str__(self):
638677

639678
class Document(DocumentInfo):
640679
name = models.CharField(max_length=255, validators=[validate_docname,], unique=True) # immutable
680+
681+
action_holders = models.ManyToManyField(Person, through=DocumentActionHolder, blank=True)
641682

642683
def __str__(self):
643684
return self.name
@@ -869,6 +910,11 @@ def fake_history_obj(self, rev):
869910

870911
return dh
871912

913+
def action_holders_enabled(self):
914+
"""Is the action holder list active for this document?"""
915+
iesg_state = self.get_state('draft-iesg')
916+
return iesg_state and iesg_state.slug != 'idexists'
917+
872918
class DocumentURL(models.Model):
873919
doc = ForeignKey(Document)
874920
tag = ForeignKey(DocUrlTagName)
@@ -1000,6 +1046,7 @@ class DocReminder(models.Model):
10001046
("published_rfc", "Published RFC"),
10011047
("added_suggested_replaces", "Added suggested replacement relationships"),
10021048
("reviewed_suggested_replaces", "Reviewed suggested replacement relationships"),
1049+
("changed_action_holders", "Changed action holders for document"),
10031050

10041051
# WG events
10051052
("changed_group", "Changed group"),

ietf/doc/resources.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
InitialReviewDocEvent, DocHistoryAuthor, BallotDocEvent, RelatedDocument,
1818
RelatedDocHistory, BallotPositionDocEvent, AddedMessageEvent, SubmissionDocEvent,
1919
ReviewRequestDocEvent, ReviewAssignmentDocEvent, EditedAuthorsDocEvent, DocumentURL,
20-
IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource )
20+
IanaExpertDocEvent, IRSGBallotDocEvent, DocExtResource, DocumentActionHolder )
2121

2222
from ietf.name.resources import BallotPositionNameResource, DocTypeNameResource
2323
class BallotTypeResource(ModelResource):
@@ -787,3 +787,22 @@ class Meta:
787787
"name": ALL_WITH_RELATIONS,
788788
}
789789
api.doc.register(DocExtResourceResource())
790+
791+
792+
from ietf.person.resources import PersonResource
793+
class DocumentActionHolderResource(ModelResource):
794+
document = ToOneField(DocumentResource, 'document')
795+
person = ToOneField(PersonResource, 'person')
796+
class Meta:
797+
queryset = DocumentActionHolder.objects.all()
798+
serializer = api.Serializer()
799+
cache = SimpleCache()
800+
#resource_name = 'documentactionholder'
801+
ordering = ['id', ]
802+
filtering = {
803+
"id": ALL,
804+
"time_added": ALL,
805+
"document": ALL_WITH_RELATIONS,
806+
"person": ALL_WITH_RELATIONS,
807+
}
808+
api.doc.register(DocumentActionHolderResource())

0 commit comments

Comments
 (0)