Rewrite change stream state page, moving it to doc/views_draft.py,
port associated tests, make the recommended next states clickable with Javascript so a standard state change is just two clicks (next state and save button) - Legacy-Id: 6288
This commit is contained in:
parent
f1e0be1033
commit
8c88bc5aec
ietf
doc
templates/doc
|
@ -9,7 +9,7 @@ from django.core.urlresolvers import reverse as urlreverse
|
|||
|
||||
from ietf.utils.mail import send_mail, send_mail_text
|
||||
from ietf.ipr.search import iprs_from_docs, related_docs
|
||||
from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent
|
||||
from ietf.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias, ConsensusDocEvent, DocTagName
|
||||
from ietf.person.models import Person
|
||||
from ietf.group.models import Group, Role
|
||||
|
||||
|
@ -414,7 +414,7 @@ def email_last_call_expired(doc):
|
|||
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()),
|
||||
cc="iesg-secretary@ietf.org")
|
||||
|
||||
def stream_state_email_recipients(doc, extra_recipients):
|
||||
def stream_state_email_recipients(doc, extra_recipients=[]):
|
||||
persons = set()
|
||||
res = []
|
||||
for r in Role.objects.filter(group=doc.group, name__in=("chair", "delegate")).select_related("person", "email"):
|
||||
|
@ -426,14 +426,25 @@ def stream_state_email_recipients(doc, extra_recipients):
|
|||
res.append(email.formatted_email())
|
||||
persons.add(email.person)
|
||||
|
||||
for x in extra_recipients:
|
||||
if not x in res:
|
||||
res.append(x)
|
||||
for p in extra_recipients:
|
||||
if not p in persons:
|
||||
res.append(p.formatted_email())
|
||||
persons.add(p)
|
||||
|
||||
return res
|
||||
|
||||
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment="", extra_recipients=[]):
|
||||
recipients = stream_state_email_recipients(doc, extra_recipients)
|
||||
|
||||
def email_draft_adopted(request, doc, by, comment):
|
||||
recipients = stream_state_email_recipients(doc)
|
||||
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
|
||||
u"%s adopted in %s %s" % (doc.name, doc.group.acronym, doc.group.type.name),
|
||||
'doc/mail/draft_adopted_email.txt',
|
||||
dict(doc=doc,
|
||||
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
||||
by=by,
|
||||
comment=comment))
|
||||
|
||||
def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=""):
|
||||
recipients = stream_state_email_recipients(doc)
|
||||
|
||||
state_type = (prev_state or new_state).type
|
||||
|
||||
|
@ -448,12 +459,20 @@ def email_stream_state_changed(request, doc, prev_state, new_state, by, comment=
|
|||
by=by,
|
||||
comment=comment))
|
||||
|
||||
def email_draft_adopted(request, doc, by, comment):
|
||||
recipients = stream_state_email_recipients(doc, [])
|
||||
def email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment=""):
|
||||
extra_recipients = []
|
||||
|
||||
if DocTagName.objects.get(slug="sheph-u") in added_tags and doc.shepherd:
|
||||
extra_recipients.append(doc.shepherd)
|
||||
|
||||
recipients = stream_state_email_recipients(doc, extra_recipients)
|
||||
|
||||
send_mail(request, recipients, settings.DEFAULT_FROM_EMAIL,
|
||||
u"%s adopted in %s %s" % (doc.name, doc.group.acronym, doc.group.type.name),
|
||||
'doc/mail/draft_adopted_email.txt',
|
||||
u"Tags changed for %s" % doc.name,
|
||||
'doc/mail/stream_tags_changed_email.txt',
|
||||
dict(doc=doc,
|
||||
url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(),
|
||||
added=added_tags,
|
||||
removed=removed_tags,
|
||||
by=by,
|
||||
comment=comment))
|
||||
|
|
|
@ -11,6 +11,7 @@ from pyquery import PyQuery
|
|||
import debug
|
||||
|
||||
from ietf.doc.models import *
|
||||
from ietf.doc .utils import *
|
||||
from ietf.name.models import *
|
||||
from ietf.group.models import *
|
||||
from ietf.person.models import *
|
||||
|
@ -999,9 +1000,9 @@ class AdoptDraftTests(django.test.TestCase):
|
|||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEquals(r.status_code, 200)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
self.assertEquals(len(q('form select[name="group"] option')), 1) # we can only select "mars"
|
||||
self.assertEqual(len(q('form select[name="group"] option')), 1) # we can only select "mars"
|
||||
|
||||
# adopt in mars WG
|
||||
mailbox_before = len(outbox)
|
||||
|
@ -1010,13 +1011,100 @@ class AdoptDraftTests(django.test.TestCase):
|
|||
dict(comment="some comment",
|
||||
group=Group.objects.get(acronym="mars").pk,
|
||||
weeks="10"))
|
||||
self.assertEquals(r.status_code, 302)
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEquals(draft.group.acronym, "mars")
|
||||
self.assertEquals(draft.stream_id, "ietf")
|
||||
self.assertEquals(draft.docevent_set.count() - events_before, 4)
|
||||
self.assertEquals(len(outbox), mailbox_before + 1)
|
||||
self.assertEqual(draft.group.acronym, "mars")
|
||||
self.assertEqual(draft.stream_id, "ietf")
|
||||
self.assertEqual(draft.docevent_set.count() - events_before, 4)
|
||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||
self.assertTrue("adopted" in outbox[-1]["Subject"].lower())
|
||||
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
|
||||
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
|
||||
|
||||
class ChangeStreamStateTests(django.test.TestCase):
|
||||
fixtures = ['names']
|
||||
|
||||
def test_set_tags(self):
|
||||
draft = make_test_data()
|
||||
draft.tags = DocTagName.objects.filter(slug="w-expert")
|
||||
draft.group.unused_tags.add("w-refdoc")
|
||||
|
||||
url = urlreverse('doc_change_stream_state', kwargs=dict(name=draft.name))
|
||||
login_testing_unauthorized(self, "marschairman", url)
|
||||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
# make sure the unused tags are hidden
|
||||
unused = draft.group.unused_tags.values_list("slug", flat=True)
|
||||
for t in q("input[name=tags]"):
|
||||
self.assertTrue(t.attrib["value"] not in unused)
|
||||
|
||||
# set tags
|
||||
mailbox_before = len(outbox)
|
||||
events_before = draft.docevent_set.count()
|
||||
r = self.client.post(url,
|
||||
dict(new_state=draft.get_state("draft-stream-%s" % draft.stream_id).pk,
|
||||
comment="some comment",
|
||||
weeks="10",
|
||||
tags=["need-aut", "sheph-u"],
|
||||
))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.tags.count(), 2)
|
||||
self.assertEqual(draft.tags.filter(slug="w-expert").count(), 0)
|
||||
self.assertEqual(draft.tags.filter(slug="need-aut").count(), 1)
|
||||
self.assertEqual(draft.tags.filter(slug="sheph-u").count(), 1)
|
||||
self.assertEqual(draft.docevent_set.count() - events_before, 2)
|
||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||
self.assertTrue("tags changed" in outbox[-1]["Subject"].lower())
|
||||
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
|
||||
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
|
||||
self.assertTrue("plain@example.com" in unicode(outbox[-1]))
|
||||
|
||||
def test_set_state(self):
|
||||
draft = make_test_data()
|
||||
|
||||
url = urlreverse('doc_change_stream_state', kwargs=dict(name=draft.name))
|
||||
login_testing_unauthorized(self, "marschairman", url)
|
||||
|
||||
# get
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
# make sure the unused states are hidden
|
||||
unused = draft.group.unused_states.values_list("pk", flat=True)
|
||||
for t in q("select[name=new_state]").find("option[name=tags]"):
|
||||
self.assertTrue(t.attrib["value"] not in unused)
|
||||
self.assertEqual(len(q('select[name=new_state]')), 1)
|
||||
|
||||
# set new state
|
||||
old_state = draft.get_state("draft-stream-%s" % draft.stream_id )
|
||||
new_state = State.objects.get(used=True, type="draft-stream-%s" % draft.stream_id, slug="parked")
|
||||
self.assertNotEqual(old_state, new_state)
|
||||
mailbox_before = len(outbox)
|
||||
events_before = draft.docevent_set.count()
|
||||
|
||||
r = self.client.post(url,
|
||||
dict(new_state=new_state.pk,
|
||||
comment="some comment",
|
||||
weeks="10",
|
||||
tags=[t.pk for t in draft.tags.filter(slug__in=get_tags_for_stream_id(draft.stream_id))],
|
||||
))
|
||||
self.assertEqual(r.status_code, 302)
|
||||
|
||||
draft = Document.objects.get(pk=draft.pk)
|
||||
self.assertEqual(draft.get_state("draft-stream-%s" % draft.stream_id), new_state)
|
||||
self.assertEqual(draft.docevent_set.count() - events_before, 2)
|
||||
reminder = DocReminder.objects.filter(event__doc=draft, type="stream-s")
|
||||
self.assertEqual(len(reminder), 1)
|
||||
due = datetime.datetime.now() + datetime.timedelta(weeks=10)
|
||||
self.assertTrue(due - datetime.timedelta(days=1) <= reminder[0].due <= due + datetime.timedelta(days=1))
|
||||
self.assertEqual(len(outbox), mailbox_before + 1)
|
||||
self.assertTrue("state changed" in outbox[-1]["Subject"].lower())
|
||||
self.assertTrue("wgchairman@ietf.org" in unicode(outbox[-1]))
|
||||
self.assertTrue("wgdelegate@ietf.org" in unicode(outbox[-1]))
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ from django.db.models import Max
|
|||
from django.conf import settings
|
||||
from django.forms.util import ErrorList
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.template.defaultfilters import pluralize
|
||||
|
||||
from ietf.utils.mail import send_mail_text, send_mail_message
|
||||
from ietf.ietfauth.decorators import role_required
|
||||
|
@ -1123,7 +1124,7 @@ def adopt_draft(request, name):
|
|||
doc = get_object_or_404(Document, type="draft", name=name)
|
||||
|
||||
if not can_adopt_draft(request.user, doc):
|
||||
return HttpResponseForbidden("You don't have permission to access this view")
|
||||
return HttpResponseForbidden("You don't have permission to access this page")
|
||||
|
||||
if request.method == 'POST':
|
||||
form = AdoptDraftForm(request.POST, user=request.user)
|
||||
|
@ -1137,8 +1138,6 @@ def adopt_draft(request, name):
|
|||
doc.time = datetime.datetime.now()
|
||||
|
||||
group = form.cleaned_data["group"]
|
||||
comment = form.cleaned_data["comment"].strip()
|
||||
|
||||
if group.type.slug == "rg":
|
||||
new_stream = StreamName.objects.get(slug="irtf")
|
||||
adopt_state_slug = "active"
|
||||
|
@ -1146,6 +1145,7 @@ def adopt_draft(request, name):
|
|||
new_stream = StreamName.objects.get(slug="ietf")
|
||||
adopt_state_slug = "c-adopt"
|
||||
|
||||
# stream
|
||||
if doc.stream != new_stream:
|
||||
e = DocEvent(type="changed_stream", time=doc.time, by=by, doc=doc)
|
||||
e.desc = u"Changed stream to <b>%s</b>" % new_stream.name
|
||||
|
@ -1154,6 +1154,7 @@ def adopt_draft(request, name):
|
|||
e.save()
|
||||
doc.stream = new_stream
|
||||
|
||||
# group
|
||||
if group != doc.group:
|
||||
e = DocEvent(type="changed_group", time=doc.time, by=by, doc=doc)
|
||||
e.desc = u"Changed group to <b>%s (%s)</b>" % (group.name, group.acronym.upper())
|
||||
|
@ -1164,9 +1165,9 @@ def adopt_draft(request, name):
|
|||
|
||||
doc.save()
|
||||
|
||||
# state
|
||||
prev_state = doc.get_state("draft-stream-%s" % doc.stream_id)
|
||||
new_state = State.objects.get(slug=adopt_state_slug, type="draft-stream-%s" % doc.stream_id, used=True)
|
||||
|
||||
if new_state != prev_state:
|
||||
doc.set_state(new_state)
|
||||
e = add_state_change_event(doc, by, prev_state, new_state, doc.time)
|
||||
|
@ -1177,6 +1178,8 @@ def adopt_draft(request, name):
|
|||
|
||||
update_reminder(doc, "stream-s", e, due_date)
|
||||
|
||||
# comment
|
||||
comment = form.cleaned_data["comment"].strip()
|
||||
if comment:
|
||||
e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc)
|
||||
e.desc = comment
|
||||
|
@ -1194,5 +1197,128 @@ def adopt_draft(request, name):
|
|||
},
|
||||
context_instance=RequestContext(request))
|
||||
|
||||
def change_stream_state(request):
|
||||
pass
|
||||
class ChangeStreamStateForm(forms.Form):
|
||||
new_state = forms.ModelChoiceField(queryset=State.objects.filter(used=True), label='State')
|
||||
weeks = forms.IntegerField(label='Expected weeks in state',required=False)
|
||||
comment = forms.CharField(widget=forms.Textarea, required=False, help_text="Optional comment for the document history")
|
||||
tags = forms.ModelMultipleChoiceField(queryset=DocTagName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
doc = kwargs.pop("doc")
|
||||
state_type = kwargs.pop("state_type")
|
||||
super(ChangeStreamStateForm, self).__init__(*args, **kwargs)
|
||||
|
||||
f = self.fields["new_state"]
|
||||
f.queryset = f.queryset.filter(type=state_type)
|
||||
if doc.group:
|
||||
unused_states = doc.group.unused_states.values_list("pk", flat=True)
|
||||
f.queryset = f.queryset.exclude(pk__in=unused_states)
|
||||
f.label = state_type.label
|
||||
|
||||
f = self.fields['tags']
|
||||
f.queryset = f.queryset.filter(slug__in=get_tags_for_stream_id(doc.stream_id))
|
||||
if doc.group:
|
||||
unused_tags = doc.group.unused_tags.values_list("pk", flat=True)
|
||||
f.queryset = f.queryset.exclude(pk__in=unused_tags)
|
||||
|
||||
def next_states_for_stream_state(doc, state_type, current_state):
|
||||
# find next states
|
||||
next_states = []
|
||||
if current_state:
|
||||
next_states = current_state.next_states.all()
|
||||
|
||||
if doc.stream_id == "ietf" and doc.group:
|
||||
transitions = doc.group.groupstatetransitions_set.filter(state=current_state)
|
||||
if transitions:
|
||||
next_states = transitions[0].next_states.all()
|
||||
else:
|
||||
# return the initial state
|
||||
states = State.objects.filter(used=True, type=state_type).order_by('order')
|
||||
if states:
|
||||
next_states = states[:1]
|
||||
|
||||
if doc.group:
|
||||
unused_states = doc.group.unused_states.values_list("pk", flat=True)
|
||||
next_states = [n for n in next_states if n.pk not in unused_states]
|
||||
|
||||
return next_states
|
||||
|
||||
@login_required
|
||||
def change_stream_state(request, name):
|
||||
doc = get_object_or_404(Document, type="draft", name=name)
|
||||
if not doc.stream:
|
||||
raise Http404
|
||||
|
||||
if not is_authorized_in_doc_stream(request.user, doc):
|
||||
return HttpResponseForbidden("You don't have permission to access this page")
|
||||
|
||||
state_type = StateType.objects.get(slug="draft-stream-%s" % doc.stream_id)
|
||||
prev_state = doc.get_state(state_type.slug)
|
||||
next_states = next_states_for_stream_state(doc, state_type, prev_state)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ChangeStreamStateForm(request.POST, doc=doc, state_type=state_type)
|
||||
if form.is_valid():
|
||||
by = request.user.get_profile()
|
||||
|
||||
save_document_in_history(doc)
|
||||
|
||||
doc.time = datetime.datetime.now()
|
||||
comment = form.cleaned_data["comment"].strip()
|
||||
|
||||
# state
|
||||
new_state = form.cleaned_data["new_state"]
|
||||
if new_state != prev_state:
|
||||
doc.set_state(new_state)
|
||||
e = add_state_change_event(doc, by, prev_state, new_state, doc.time)
|
||||
|
||||
due_date = None
|
||||
if form.cleaned_data["weeks"] != None:
|
||||
due_date = datetime.date.today() + datetime.timedelta(weeks=form.cleaned_data["weeks"])
|
||||
|
||||
update_reminder(doc, "stream-s", e, due_date)
|
||||
|
||||
email_stream_state_changed(request, doc, prev_state, new_state, by, comment)
|
||||
|
||||
# tags
|
||||
existing_tags = set(doc.tags.all())
|
||||
new_tags = set(form.cleaned_data["tags"])
|
||||
|
||||
if existing_tags != new_tags:
|
||||
doc.tags = new_tags
|
||||
|
||||
e = DocEvent(type="changed_document", time=doc.time, by=by, doc=doc)
|
||||
added_tags = new_tags - existing_tags
|
||||
removed_tags = existing_tags - new_tags
|
||||
l = []
|
||||
if added_tags:
|
||||
l.append(u"Tag%s %s set." % (pluralize(added_tags), ", ".join(t.name for t in added_tags)))
|
||||
if removed_tags:
|
||||
l.append(u"Tag%s %s cleared." % (pluralize(removed_tags), ", ".join(t.name for t in removed_tags)))
|
||||
e.desc = " ".join(l)
|
||||
e.save()
|
||||
|
||||
email_stream_tags_changed(request, doc, added_tags, removed_tags, by, comment)
|
||||
|
||||
# comment
|
||||
if comment:
|
||||
e = DocEvent(type="added_comment", time=doc.time, by=by, doc=doc)
|
||||
e.desc = comment
|
||||
e.save()
|
||||
|
||||
return HttpResponseRedirect(doc.get_absolute_url())
|
||||
else:
|
||||
form = ChangeStreamStateForm(initial=dict(new_state=prev_state.pk),
|
||||
doc=doc, state_type=state_type)
|
||||
|
||||
milestones = doc.groupmilestone_set.all()
|
||||
|
||||
|
||||
return render_to_response("doc/draft/change_stream_state.html",
|
||||
{"doc": doc,
|
||||
"form": form,
|
||||
"milestones": milestones,
|
||||
"state_type": state_type,
|
||||
"next_states": next_states,
|
||||
},
|
||||
context_instance=RequestContext(request))
|
||||
|
|
73
ietf/templates/doc/draft/change_stream_state.html
Normal file
73
ietf/templates/doc/draft/change_stream_state.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Change {{ state_type.label }} of {{ doc }}{% endblock %}
|
||||
|
||||
{% block morecss %}
|
||||
.next-states { margin-bottom: 1em; }
|
||||
.next-states a {
|
||||
display: inline-block;
|
||||
margin: 0 0.2em;
|
||||
padding: 0.2em 0.3em;
|
||||
border-radius: 0.2em;
|
||||
cursor: pointer;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.next-states a:hover { background-color: #eee; transition-duration: 0.2s; }
|
||||
form.change-state th { max-width: 8em; }
|
||||
form.change-state th, form.change-state td { padding-bottom: 0.3em; }
|
||||
form.change-state #id_comment { width: 30em; }
|
||||
form.change-state #id_weeks { width: 2em;}
|
||||
form.change-state .actions { text-align: right; padding-top: 1em; }
|
||||
form.change-state ul { padding: 0; margin: 0; }
|
||||
form.change-state ul li { padding: 0; padding-bottom: 0.1em; margin: 0; list-style-type: none; }
|
||||
form.change-state ul li input { vertical-align: middle; }
|
||||
form.change-state ul li label { cursor: pointer; }
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Change {{ state_type.label }} of {{ doc }}</h1>
|
||||
|
||||
<p class="intro">For help on the states, see the <a href="{% url state_help type=state_type.slug %}">state table</a>.</p>
|
||||
|
||||
{% if next_states %}
|
||||
<div class="next-states">
|
||||
<span>Recommended next state{{next_states|pluralize}}:</span>
|
||||
{% for state in next_states %}<a tabindex="0" data-state="{{ state.pk }}">{{ state.name }}</a> {% if not forloop.last %} or {% endif %}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form class="change-state" action="" method="post">
|
||||
<table cellspacing="0">
|
||||
{% for field in form.visible_fields %}
|
||||
<tr>
|
||||
<th>{{ field.label_tag }}:</th>
|
||||
<td>{{ field }}
|
||||
{% if field.help_text %}<div class="help">{{ field.help_text }}</div>{% endif %}
|
||||
|
||||
{{ field.errors }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
<tr>
|
||||
<td colspan="2" class="actions">
|
||||
<a class="button" href="{{ doc.get_absolute_url }}">Cancel</a>
|
||||
<input class="button" type="submit" value="Save"/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
<script>
|
||||
jQuery(document).ready(function () {
|
||||
jQuery(".next-states a").click(function (e) {
|
||||
e.preventDefault();
|
||||
var s = jQuery(this).data("state");
|
||||
console.log(s)
|
||||
jQuery("#id_new_state").val(s);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
10
ietf/templates/doc/mail/stream_tags_changed_email.txt
Normal file
10
ietf/templates/doc/mail/stream_tags_changed_email.txt
Normal file
|
@ -0,0 +1,10 @@
|
|||
{% autoescape off %}{% filter wordwrap:73 %}
|
||||
The tags on {{ doc }} have been changed by {{ by }}:
|
||||
{{ url }}
|
||||
|
||||
{% if added %}Tag{{ added|pluralize }} {% for t in added %}"{{ t }}"{% if not forloop.last %}, {% endif %}{% endfor %} added.{% endif %}
|
||||
{% if removed %}Tag{{ removed|pluralize }} {% for t in removed %}"{{ t }}"{% if not forloop.last %}, {% endif %}{% endfor %} cleared.{% endif %}
|
||||
{% if comment %}
|
||||
|
||||
Comment:
|
||||
{{ comment }}{% endif %}{% endfilter %}{% endautoescape %}
|
Loading…
Reference in a new issue