datatracker/ietf/doc/views_status_change.py
Robert Sparks 7d464fcc55
fix: keep the ftp dir in sync when the datatracker creates artifacts (#8401)
* chore: fix typo in log message

* fix: write new conflict reviews to the ftp directory

* fix: log exception description

* chore: avoid reusing a name for mypy

* fix: write non-meeting-related slides to ftp/slides

* fix: write status changes to ftp dir

* fix: copy draft yang modules to the ftp dir

the last commit above (see #8401) has no tests :(. There aren't tests of this yang scaffolding to add to. Adding a set of tests should be an independent larger effort.
2025-01-09 11:23:20 -06:00

750 lines
31 KiB
Python

# Copyright The IETF Trust 2012-2020, All Rights Reserved
# -*- coding: utf-8 -*-
import datetime
import io
import os
from pathlib import Path
import re
from typing import Dict # pyflakes:ignore
from django import forms
from django.shortcuts import render, get_object_or_404, redirect
from django.http import Http404, HttpResponseRedirect
from django.urls import reverse
from django.template.loader import render_to_string
from django.conf import settings
from django.utils.encoding import force_str
from django.utils.html import escape
import debug # pyflakes:ignore
from ietf.doc.mails import email_ad_approved_status_change
from ietf.doc.models import ( Document, State, DocEvent, BallotDocEvent,
BallotPositionDocEvent, NewRevisionDocEvent, WriteupDocEvent, STATUSCHANGE_RELATIONS )
from ietf.doc.forms import AdForm
from ietf.doc.lastcall import request_last_call
from ietf.doc.utils import add_state_change_event, update_telechat, close_open_ballots, create_ballot_if_not_open
from ietf.doc.views_ballot import LastCallTextForm
from ietf.group.models import Group
from ietf.iesg.models import TelechatDate
from ietf.ietfauth.utils import has_role, role_required
from ietf.mailtrigger.utils import gather_address_lists
from ietf.name.models import DocRelationshipName, StdLevelName
from ietf.person.models import Person
from ietf.utils.log import log
from ietf.utils.mail import send_mail_preformatted
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.utils.timezone import date_today, DEADLINE_TZINFO
class ChangeStateForm(forms.Form):
new_state = forms.ModelChoiceField(State.objects.filter(type="statchg", used=True), label="Status Change Evaluation State", empty_label=None, required=True)
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the review history.", required=False, strip=False)
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super(ChangeStateForm, self).__init__(*args, **kwargs)
if not has_role(user, "Secretariat"):
self.fields["new_state"].queryset = self.fields["new_state"].queryset.exclude(slug="appr-sent")
@role_required("Area Director", "Secretariat")
def change_state(request, name, option=None):
"""Change state of an status-change document, notifying parties as necessary
and logging the change as a comment."""
status_change = get_object_or_404(Document, type="statchg", name=name)
login = request.user.person
if request.method == 'POST':
form = ChangeStateForm(request.POST, user=request.user)
if form.is_valid():
clean = form.cleaned_data
new_state = clean['new_state']
comment = clean['comment'].rstrip()
if comment:
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=login)
c.desc = comment
c.save()
prev_state = status_change.get_state()
if new_state != prev_state:
status_change.set_state(new_state)
events = []
events.append(add_state_change_event(status_change, login, prev_state, new_state))
status_change.save_with_history(events)
if new_state.slug == "iesgeval":
e = create_ballot_if_not_open(request, status_change, login, "statchg", status_change.time) # pyflakes:ignore
ballot = status_change.latest_event(BallotDocEvent, type="created_ballot")
if has_role(request.user, "Area Director") and not status_change.latest_event(BallotPositionDocEvent, balloter=login, ballot=ballot, type="changed_ballot_position"):
# The AD putting a status change into iesgeval who doesn't already have a position is saying "yes"
pos = BallotPositionDocEvent(doc=status_change, rev=status_change.rev, by=login)
pos.ballot = ballot
pos.type = "changed_ballot_position"
pos.balloter = login
pos.pos_id = "yes"
pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.balloter.plain_name())
pos.save()
send_status_change_eval_email(request,status_change)
if new_state.slug == "lc-req":
request_last_call(request, status_change)
return render(request, 'doc/draft/last_call_requested.html',
dict(doc=status_change,
url = status_change.get_absolute_url(),
))
elif new_state.slug == 'appr-pend' and has_role(request.user, "Area Director"):
related_docs = status_change.relateddocument_set.filter(
relationship__slug__in=STATUSCHANGE_RELATIONS
)
related_doc_info = [
dict(title=rel_doc.target.title,
name=rel_doc.target.name,
newstatus=newstatus(rel_doc))
for rel_doc in related_docs
]
email_ad_approved_status_change(
request,
status_change,
related_doc_info=related_doc_info,
)
return redirect('ietf.doc.views_doc.document_main', name=status_change.name)
else:
s = status_change.get_state()
init = dict(new_state=s.pk if s else None,
type='statchg',
label='Status Change Evaluation State',
)
form = ChangeStateForm(initial=init, user=request.user)
return render(request, 'doc/change_state.html',
dict(form=form,
doc=status_change,
login=login,
help_url=reverse('ietf.doc.views_help.state_help', kwargs=dict(type="status-change")),
))
def send_status_change_eval_email(request,doc):
for target in ('iesg_ballot_issued', 'ballot_issued_iana'):
addrs = gather_address_lists(target,doc=doc).as_strings()
msg = render_to_string("doc/eval_email.txt",
dict(doc=doc,
doc_url = settings.IDTRACKER_BASE_URL+doc.get_absolute_url(),
to = addrs.to,
cc = addrs.cc
)
)
send_mail_preformatted(request,msg)
class UploadForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Status change text", help_text="Edit the status change text.", required=False, strip=False)
txt = forms.FileField(label=".txt format", help_text="Or upload a .txt file.", required=False)
def clean_content(self):
return self.cleaned_data["content"].replace("\r", "")
def clean_txt(self):
return get_cleaned_text_file_content(self.cleaned_data["txt"])
def save(self, doc):
basename = f"{doc.name}-{doc.rev}.txt"
filename = Path(settings.STATUS_CHANGE_PATH) / basename
with io.open(filename, 'w', encoding='utf-8') as destination:
if self.cleaned_data['txt']:
destination.write(self.cleaned_data['txt'])
else:
destination.write(self.cleaned_data['content'])
try:
ftp_filename = Path(settings.FTP_DIR) / "status-changes" / basename
os.link(filename, ftp_filename) # Path.hardlink is not available until 3.10
except IOError as ex:
log(
"There was an error creating a hardlink at %s pointing to %s: %s"
% (ftp_filename, filename, ex)
)
#This is very close to submit on charter - can we get better reuse?
@role_required('Area Director','Secretariat')
def submit(request, name):
doc = get_object_or_404(Document, type="statchg", name=name)
login = request.user.person
path = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.name, doc.rev))
not_uploaded_yet = doc.rev == "00" and not os.path.exists(path)
if not_uploaded_yet:
# this case is special - the status change text document doesn't actually exist yet
next_rev = doc.rev
else:
next_rev = "%02d" % (int(doc.rev)+1)
if request.method == 'POST':
if "submit_response" in request.POST:
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
doc.rev = next_rev
events = []
e = NewRevisionDocEvent(doc=doc, by=login, type="new_revision")
e.desc = "New version available: <b>%s-%s.txt</b>" % (doc.name, doc.rev)
e.rev = doc.rev
e.save()
events.append(e)
# Save file on disk
form.save(doc)
doc.save_with_history(events)
return redirect('ietf.doc.views_doc.document_main', name=doc.name)
elif "reset_text" in request.POST:
init = { "content": render_to_string("doc/status_change/initial_template.txt",dict())}
form = UploadForm(initial=init)
# Protect against handcrufted malicious posts
else:
form = None
else:
form = None
if not form:
init = { "content": ""}
if not_uploaded_yet:
init["content"] = render_to_string("doc/status_change/initial_template.txt",
dict(),
)
else:
filename = os.path.join(settings.STATUS_CHANGE_PATH, '%s-%s.txt' % (doc.name, doc.rev))
try:
with io.open(filename, 'r') as f:
init["content"] = f.read()
except IOError:
pass
form = UploadForm(initial=init)
return render(request, 'doc/status_change/submit.html',
{'form': form,
'next_rev': next_rev,
'doc' : doc,
})
class ChangeTitleForm(forms.Form):
title = forms.CharField(max_length=255, label="Title", required=True)
@role_required("Area Director", "Secretariat")
def edit_title(request, name):
"""Change the title for this status_change document."""
status_change = get_object_or_404(Document, type="statchg", name=name)
if request.method == 'POST':
form = ChangeTitleForm(request.POST)
if form.is_valid():
status_change.title = form.cleaned_data['title']
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=request.user.person)
c.desc = "Title changed to '%s'"%status_change.title
c.save()
status_change.save_with_history([c])
return redirect("ietf.doc.views_doc.document_main", name=status_change.name)
else:
init = { "title" : status_change.title }
form = ChangeTitleForm(initial=init)
titletext = '%s-%s.txt' % (status_change.name,status_change.rev)
return render(request, 'doc/change_title.html',
{'form': form,
'doc': status_change,
'titletext' : titletext,
},
)
@role_required("Area Director", "Secretariat")
def edit_ad(request, name):
"""Change the shepherding Area Director for this status_change."""
status_change = get_object_or_404(Document, type="statchg", name=name)
if request.method == 'POST':
form = AdForm(request.POST)
if form.is_valid():
status_change.ad = form.cleaned_data['ad']
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=request.user.person)
c.desc = "Shepherding AD changed to "+status_change.ad.name
c.save()
status_change.save_with_history([c])
return redirect("ietf.doc.views_doc.document_main", name=status_change.name)
else:
init = { "ad" : status_change.ad_id }
form = AdForm(initial=init)
titletext = '%s-%s.txt' % (status_change.name,status_change.rev)
return render(request, 'doc/change_ad.html',
{'form': form,
'doc': status_change,
'titletext' : titletext,
},
)
def newstatus(relateddoc):
level_map = {
'tops' : 'ps',
'tois' : 'std',
'tohist' : 'hist',
'toinf' : 'inf',
'tobcp' : 'bcp',
'toexp' : 'exp',
}
return StdLevelName.objects.get(slug=level_map[relateddoc.relationship.slug])
def default_approval_text(status_change,relateddoc):
current_text = status_change.text_or_error() # pyflakes:ignore
if relateddoc.target.std_level_id in ('std','ps','ds','bcp',):
action = "Protocol Action"
else:
action = "Document Action"
addrs = gather_address_lists('ballot_approved_status_change',doc=status_change).as_strings(compact=False)
text = render_to_string("doc/status_change/approval_text.txt",
dict(status_change=status_change,
status_change_url = settings.IDTRACKER_BASE_URL+status_change.get_absolute_url(),
relateddoc= relateddoc,
relateddoc_url = settings.IDTRACKER_BASE_URL+relateddoc.target.get_absolute_url(),
approved_text = current_text,
action=action,
newstatus=newstatus(relateddoc),
to=addrs.to,
cc=addrs.cc,
)
)
return text
from django.forms.formsets import formset_factory
class AnnouncementForm(forms.Form):
announcement_text = forms.CharField(widget=forms.Textarea, label="Status Change Announcement", help_text="Edit the announcement message.", required=True, strip=False)
label = None
def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self.label = self.initial.get('label')
@role_required("Secretariat")
def approve(request, name):
"""Approve this status change, setting the appropriate state and send the announcements to the right parties."""
status_change = get_object_or_404(Document, type="statchg", name=name)
if status_change.get_state('statchg').slug not in ('appr-pend'):
raise Http404
login = request.user.person
AnnouncementFormSet = formset_factory(AnnouncementForm,extra=0)
if request.method == 'POST':
formset = AnnouncementFormSet(request.POST)
if formset.is_valid():
prev_state = status_change.get_state()
new_state = State.objects.get(type='statchg', slug='appr-sent')
status_change.set_state(new_state)
events = []
events.append(add_state_change_event(status_change, login, prev_state, new_state))
close_open_ballots(status_change, login)
e = DocEvent(doc=status_change, rev=status_change.rev, by=login)
e.type = "iesg_approved"
e.desc = "IESG has approved the status change"
e.save()
events.append(e)
status_change.save_with_history(events)
for form in formset.forms:
send_mail_preformatted(request, form.cleaned_data['announcement_text'], extra={})
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=login)
c.desc = "The following approval message was sent\n"+form.cleaned_data['announcement_text']
c.save()
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
# Add a document event to each target
c = DocEvent(type="added_comment", doc=rel.target, rev=rel.target.rev, by=login)
c.desc = "New status of %s approved by the IESG\n%s%s" % (newstatus(rel), settings.IDTRACKER_BASE_URL,reverse('ietf.doc.views_doc.document_main', kwargs={'name': status_change.name}))
c.save()
return HttpResponseRedirect(status_change.get_absolute_url())
else:
init = []
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
init.append({"announcement_text" : escape(default_approval_text(status_change,rel)),
"label": "Announcement text for %s to %s"%(rel.target.name,newstatus(rel)),
})
formset = AnnouncementFormSet(initial=init)
for form in formset.forms:
form.fields['announcement_text'].label=form.label
return render(request, 'doc/status_change/approve.html',
dict(
doc = status_change,
formset = formset,
))
def clean_helper(form, formtype):
cleaned_data = super(formtype, form).clean()
new_relations = {}
rfc_fields = {}
status_fields={}
for k in sorted(form.data.keys()):
v = form.data[k].lower()
if k.startswith('new_relation_row'):
if re.match(r'\d{1,4}',v):
v = 'rfc'+v
rfc_fields[k[17:]]=v
elif k.startswith('statchg_relation_row'):
status_fields[k[21:]]=v
for key in rfc_fields:
if rfc_fields[key]!="":
if key in status_fields:
new_relations[rfc_fields[key]]=status_fields[key]
else:
new_relations[rfc_fields[key]]=None
form.relations = new_relations
errors=[]
for key in new_relations:
if not re.match(r'(?i)rfc\d{1,4}',key):
errors.append(key+" is not a valid RFC - please use the form RFCn\n")
elif not Document.objects.filter(name=key):
errors.append(key+" does not exist\n")
if new_relations[key] not in STATUSCHANGE_RELATIONS:
errors.append("Please choose a new status level for "+key+"\n")
if errors:
raise forms.ValidationError(errors)
cleaned_data['relations']=new_relations
return cleaned_data
class EditStatusChangeForm(forms.Form):
relations={} # type: Dict[str, str]
def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self.relations = self.initial.get('relations')
def clean(self):
return clean_helper(self,EditStatusChangeForm)
class StartStatusChangeForm(forms.Form):
document_name = forms.CharField(max_length=255, label="Document name", help_text="A descriptive name such as status-change-md2-to-historic is better than status-change-rfc1319.", required=True)
title = forms.CharField(max_length=255, label="Title", required=True)
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active",role__group__type='area').order_by('name'),
label="Shepherding AD", empty_label="(None)", required=False)
create_in_state = forms.ModelChoiceField(State.objects.filter(type="statchg", slug__in=("needshep", "adrev")), empty_label=None, required=False)
notify = forms.CharField(
widget=forms.Textarea,
max_length=1023,
label="Notice emails",
help_text="Separate email addresses with commas.",
required=False,
)
telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False, widget=forms.Select(attrs={'onchange':'make_bold()'}))
relations={} # type: Dict[str, str]
def __init__(self, *args, **kwargs):
super(self.__class__, self).__init__(*args, **kwargs)
self.relations = self.initial.get('relations')
# telechat choices
dates = [d.date for d in TelechatDate.objects.active().order_by('date')]
self.fields['telechat_date'].choices = [("", "(not on agenda)")] + [(d, d.strftime("%Y-%m-%d")) for d in dates]
def clean_document_name(self):
name = self.cleaned_data['document_name']
errors=[]
if re.search("[^a-z0-9-]", name):
errors.append("The name of the document may only contain digits, lowercase letters and dashes")
if re.search("--", name):
errors.append("Please do not put more than one hyphen between any two words in the name")
if name.startswith('status-change'):
errors.append("status-change- will be added automatically as a prefix")
if name.startswith('-'):
errors.append("status-change- will be added automatically as a prefix, starting with a - will result in status-change-%s"%name)
if re.search("-[0-9]{2}$", name):
errors.append("This name looks like ends in a version number. -00 will be added automatically. Please adjust the end of the name.")
if Document.objects.filter(name='status-change-%s'%name):
errors.append("status-change-%s already exists"%name)
if name.endswith('CHANGETHIS'):
errors.append("Please change CHANGETHIS to reflect the intent of this status change")
if errors:
raise forms.ValidationError(errors)
return name
def clean_title(self):
title = self.cleaned_data['title']
errors=[]
if title.endswith('CHANGETHIS'):
errors.append("Please change CHANGETHIS to reflect the intent of this status change")
if errors:
raise forms.ValidationError(errors)
return title
def clean(self):
return clean_helper(self,StartStatusChangeForm)
def rfc_status_changes(request):
"""Show the rfc status changes that are under consideration, and those that are completed."""
docs=Document.objects.filter(type__slug='statchg')
doclist=[x for x in docs]
doclist.sort(key=lambda obj: obj.get_state().order)
return render(request, 'doc/status_change/status_changes.html',
{'docs' : doclist,
},
)
@role_required("Area Director","Secretariat")
def start_rfc_status_change(request, name=None):
"""Start the RFC status change review process, setting the initial shepherding AD, and possibly putting the review on a telechat."""
if name:
if not re.match("(?i)rfc[0-9]{1,4}",name):
raise Http404
seed_rfc = get_object_or_404(Document, type="rfc", name=name)
login = request.user.person
relation_slugs = DocRelationshipName.objects.filter(slug__in=STATUSCHANGE_RELATIONS)
if request.method == 'POST':
form = StartStatusChangeForm(request.POST)
if form.is_valid():
iesg_group = Group.objects.get(acronym='iesg')
status_change = Document.objects.create(
type_id="statchg",
name='status-change-'+form.cleaned_data['document_name'],
title=form.cleaned_data['title'],
rev="00",
ad=form.cleaned_data['ad'],
notify=form.cleaned_data['notify'],
stream_id='ietf',
group=iesg_group,
)
status_change.set_state(form.cleaned_data['create_in_state'])
for key in form.cleaned_data['relations']:
status_change.relateddocument_set.create(target=Document.objects.get(name=key),
relationship_id=form.cleaned_data['relations'][key])
tc_date = form.cleaned_data['telechat_date']
if tc_date:
update_telechat(request, status_change, login, tc_date)
return HttpResponseRedirect(status_change.get_absolute_url())
else:
init = {}
if name:
init['title'] = "%s to CHANGETHIS" % seed_rfc.title
init['document_name'] = "%s-to-CHANGETHIS" % seed_rfc.name
relations={}
relations[seed_rfc.name]=None
init['relations'] = relations
form = StartStatusChangeForm(initial=init)
return render(request, 'doc/status_change/start.html',
{'form': form,
'relation_slugs': relation_slugs,
},
)
@role_required("Area Director", "Secretariat")
def edit_relations(request, name):
"""Change the affected set of RFCs"""
status_change = get_object_or_404(Document, type="statchg", name=name)
login = request.user.person
relation_slugs = DocRelationshipName.objects.filter(slug__in=STATUSCHANGE_RELATIONS)
if request.method == 'POST':
form = EditStatusChangeForm(request.POST)
if form.is_valid():
old_relations={}
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
old_relations[rel.target.name]=rel.relationship.slug
new_relations=form.cleaned_data['relations']
status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS).delete()
for key in new_relations:
status_change.relateddocument_set.create(target=Document.objects.get(name=key),
relationship_id=new_relations[key])
c = DocEvent(type="added_comment", doc=status_change, rev=status_change.rev, by=login)
c.desc = "Affected RFC list changed.\nOLD:"
for relname,relslug in (set(old_relations.items())-set(new_relations.items())):
c.desc += "\n "+relname+": "+DocRelationshipName.objects.get(slug=relslug).name
c.desc += "\nNEW:"
for relname,relslug in (set(new_relations.items())-set(old_relations.items())):
c.desc += "\n "+relname+": "+DocRelationshipName.objects.get(slug=relslug).name
c.desc += "\n"
c.save()
return HttpResponseRedirect(status_change.get_absolute_url())
else:
relations={}
for rel in status_change.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS):
relations[rel.target.name]=rel.relationship.slug
init = { "relations":relations,
}
form = EditStatusChangeForm(initial=init)
return render(request, 'doc/status_change/edit_relations.html',
{
'doc': status_change,
'form': form,
'relation_slugs': relation_slugs,
},
)
def generate_last_call_text(request, doc):
# requester should be set based on doc.group once the group for a status change can be set to something other than the IESG
# and when groups are set, vary the expiration time accordingly
requester = "an individual participant"
expiration_date = date_today(DEADLINE_TZINFO) + datetime.timedelta(days=28)
cc = []
new_text = render_to_string("doc/status_change/last_call_announcement.txt",
dict(doc=doc,
settings=settings,
requester=requester,
expiration_date=expiration_date.strftime("%Y-%m-%d"),
changes=['%s from %s to %s\n (%s)'%(rel.target.name.upper(),rel.target.std_level.name,newstatus(rel),rel.target.title) for rel in doc.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS)],
urls=[rel.target.get_absolute_url() for rel in doc.relateddocument_set.filter(relationship__slug__in=STATUSCHANGE_RELATIONS)],
cc=cc
)
)
e = WriteupDocEvent()
e.type = 'changed_last_call_text'
e.by = request.user.person
e.doc = doc
e.rev = doc.rev
e.desc = 'Last call announcement was generated'
e.text = force_str(new_text)
e.save()
return e
@role_required("Area Director", "Secretariat")
def last_call(request, name):
"""Edit the Last Call Text for this status change and possibly request IETF LC"""
status_change = get_object_or_404(Document, type="statchg", name=name)
login = request.user.person
last_call_event = status_change.latest_event(WriteupDocEvent, type="changed_last_call_text")
if not last_call_event:
last_call_event = generate_last_call_text(request, status_change)
form = LastCallTextForm(initial=dict(last_call_text=escape(last_call_event.text)))
if request.method == 'POST':
if "save_last_call_text" in request.POST or ("send_last_call_request" in request.POST and status_change.ad is not None):
form = LastCallTextForm(request.POST)
if form.is_valid():
events = []
t = form.cleaned_data['last_call_text']
if t != last_call_event.text:
e = WriteupDocEvent(doc=status_change, rev=status_change.rev, by=login)
e.by = login
e.type = "changed_last_call_text"
e.desc = "Last call announcement was changed"
e.text = t
e.save()
events.append(e)
if "send_last_call_request" in request.POST:
prev_state = status_change.get_state()
new_state = State.objects.get(type='statchg', slug='lc-req')
status_change.set_state(new_state)
e = add_state_change_event(status_change, login, prev_state, new_state)
if e:
events.append(e)
if events:
status_change.save_with_history(events)
request_last_call(request, status_change)
return render(request, 'doc/draft/last_call_requested.html',
dict(doc=status_change,
url = status_change.get_absolute_url(),
)
)
if "regenerate_last_call_text" in request.POST:
e = generate_last_call_text(request,status_change)
form = LastCallTextForm(initial=dict(last_call_text=escape(e.text)))
return render(request, 'doc/status_change/last_call.html',
dict(doc=status_change,
back_url = status_change.get_absolute_url(),
last_call_event = last_call_event,
last_call_form = form,
),
)