Make it possible to close nominations without closing feedback. Fixes #2255. Commit ready for merge.
- Legacy-Id: 13399
This commit is contained in:
parent
1a18130c57
commit
ac8e5f5402
|
@ -24,7 +24,7 @@ class NomineePositionAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
|
|
||||||
class PositionAdmin(admin.ModelAdmin):
|
class PositionAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'nomcom', 'is_open')
|
list_display = ('name', 'nomcom', 'is_open', 'accepting_nominations', 'accepting_feedback')
|
||||||
list_filter = ('nomcom',)
|
list_filter = ('nomcom',)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,8 @@ class PositionFactory(factory.DjangoModelFactory):
|
||||||
|
|
||||||
name = factory.Faker('sentence',nb_words=5)
|
name = factory.Faker('sentence',nb_words=5)
|
||||||
is_open = True
|
is_open = True
|
||||||
|
accepting_nominations = True
|
||||||
|
accepting_feedback = True
|
||||||
|
|
||||||
class NomineeFactory(factory.DjangoModelFactory):
|
class NomineeFactory(factory.DjangoModelFactory):
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
|
@ -41,7 +41,7 @@ class PositionNomineeField(forms.ChoiceField):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.nomcom = kwargs.pop('nomcom')
|
self.nomcom = kwargs.pop('nomcom')
|
||||||
positions = Position.objects.get_by_nomcom(self.nomcom).opened().order_by('name')
|
positions = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True).order_by('name')
|
||||||
results = []
|
results = []
|
||||||
for position in positions:
|
for position in positions:
|
||||||
accepted_nominees = [np.nominee for np in NomineePosition.objects.filter(position=position,state='accepted').exclude(nominee__duplicated__isnull=False)]
|
accepted_nominees = [np.nominee for np in NomineePosition.objects.filter(position=position,state='accepted').exclude(nominee__duplicated__isnull=False)]
|
||||||
|
@ -62,7 +62,7 @@ class PositionNomineeField(forms.ChoiceField):
|
||||||
return nominee
|
return nominee
|
||||||
(position_id, nominee_id) = nominee.split('_')
|
(position_id, nominee_id) = nominee.split('_')
|
||||||
try:
|
try:
|
||||||
position = Position.objects.get_by_nomcom(self.nomcom).opened().get(id=position_id)
|
position = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True).get(id=position_id)
|
||||||
except Position.DoesNotExist:
|
except Position.DoesNotExist:
|
||||||
raise forms.ValidationError('Invalid nominee')
|
raise forms.ValidationError('Invalid nominee')
|
||||||
try:
|
try:
|
||||||
|
@ -82,7 +82,7 @@ class MultiplePositionNomineeField(forms.MultipleChoiceField, PositionNomineeFie
|
||||||
return nominee
|
return nominee
|
||||||
(position_id, nominee_id) = nominee.split('_')
|
(position_id, nominee_id) = nominee.split('_')
|
||||||
try:
|
try:
|
||||||
position = Position.objects.get_by_nomcom(self.nomcom).opened().get(id=position_id)
|
position = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True).get(id=position_id)
|
||||||
except Position.DoesNotExist:
|
except Position.DoesNotExist:
|
||||||
raise forms.ValidationError('Invalid nominee')
|
raise forms.ValidationError('Invalid nominee')
|
||||||
try:
|
try:
|
||||||
|
@ -356,7 +356,9 @@ class NominateForm(forms.ModelForm):
|
||||||
self.fields['searched_email'].help_text = 'Search by name or email address. Click <a href="%s">here</a> if the search does not find the candidate you want to nominate.' % reverse(new_person_url_name,kwargs={'year':self.nomcom.year()})
|
self.fields['searched_email'].help_text = 'Search by name or email address. Click <a href="%s">here</a> if the search does not find the candidate you want to nominate.' % reverse(new_person_url_name,kwargs={'year':self.nomcom.year()})
|
||||||
self.fields['nominator_email'].label = 'Nominator email'
|
self.fields['nominator_email'].label = 'Nominator email'
|
||||||
if self.nomcom:
|
if self.nomcom:
|
||||||
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened()
|
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True)
|
||||||
|
if self.public:
|
||||||
|
self.fields['position'].queryset = self.fields['position'].queryset.filter(accepting_nominations=True)
|
||||||
self.fields['qualifications'].help_text = self.nomcom.initial_text
|
self.fields['qualifications'].help_text = self.nomcom.initial_text
|
||||||
|
|
||||||
if not self.public:
|
if not self.public:
|
||||||
|
@ -454,7 +456,9 @@ class NominateNewPersonForm(forms.ModelForm):
|
||||||
|
|
||||||
self.fields['nominator_email'].label = 'Nominator email'
|
self.fields['nominator_email'].label = 'Nominator email'
|
||||||
if self.nomcom:
|
if self.nomcom:
|
||||||
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).opened()
|
self.fields['position'].queryset = Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True)
|
||||||
|
if self.public:
|
||||||
|
self.fields['position'].queryset = self.fields['position'].queryset.filter(accepting_nominations=True)
|
||||||
self.fields['qualifications'].help_text = self.nomcom.initial_text
|
self.fields['qualifications'].help_text = self.nomcom.initial_text
|
||||||
|
|
||||||
if not self.public:
|
if not self.public:
|
||||||
|
@ -680,7 +684,7 @@ class PositionForm(forms.ModelForm):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Position
|
model = Position
|
||||||
fields = ('name', 'is_open')
|
fields = ('name', 'is_open', 'accepting_nominations', 'accepting_feedback')
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.nomcom = kwargs.pop('nomcom', None)
|
self.nomcom = kwargs.pop('nomcom', None)
|
||||||
|
@ -766,7 +770,7 @@ class MutableFeedbackForm(forms.ModelForm):
|
||||||
widget=forms.SelectMultiple(attrs={'class':'nominee_multi_select','size':'12'}),
|
widget=forms.SelectMultiple(attrs={'class':'nominee_multi_select','size':'12'}),
|
||||||
help_text='Hold down "Control", or "Command" on a Mac, to select more than one.')
|
help_text='Hold down "Control", or "Command" on a Mac, to select more than one.')
|
||||||
else:
|
else:
|
||||||
self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).opened(), label="Position")
|
self.fields['position'] = forms.ModelChoiceField(queryset=Position.objects.get_by_nomcom(self.nomcom).filter(is_open=True), label="Position")
|
||||||
self.fields['searched_email'] = SearchableEmailField(only_users=False,help_text="Try to find the candidate you are classifying with this field first. Only use the name and email fields below if this search does not find the candidate.",label="Candidate",required=False)
|
self.fields['searched_email'] = SearchableEmailField(only_users=False,help_text="Try to find the candidate you are classifying with this field first. Only use the name and email fields below if this search does not find the candidate.",label="Candidate",required=False)
|
||||||
self.fields['candidate_name'] = forms.CharField(label="Candidate name",help_text="Only fill in this name field if the search doesn't find the person you are classifying",required=False)
|
self.fields['candidate_name'] = forms.CharField(label="Candidate name",help_text="Only fill in this name field if the search doesn't find the person you are classifying",required=False)
|
||||||
self.fields['candidate_email'] = forms.EmailField(label="Candidate email",help_text="Only fill in this email field if the search doesn't find the person you are classifying",required=False)
|
self.fields['candidate_email'] = forms.EmailField(label="Candidate email",help_text="Only fill in this email field if the search doesn't find the person you are classifying",required=False)
|
||||||
|
@ -830,7 +834,6 @@ class MutableFeedbackForm(forms.ModelForm):
|
||||||
feedback.positions.add(position)
|
feedback.positions.add(position)
|
||||||
return feedback
|
return feedback
|
||||||
|
|
||||||
|
|
||||||
FullFeedbackFormSet = forms.modelformset_factory(
|
FullFeedbackFormSet = forms.modelformset_factory(
|
||||||
model=Feedback,
|
model=Feedback,
|
||||||
extra=0,
|
extra=0,
|
||||||
|
|
|
@ -68,14 +68,6 @@ class PositionQuerySet(QuerySet):
|
||||||
def get_by_nomcom(self, nomcom):
|
def get_by_nomcom(self, nomcom):
|
||||||
return self.filter(nomcom=nomcom)
|
return self.filter(nomcom=nomcom)
|
||||||
|
|
||||||
def opened(self):
|
|
||||||
""" only opened positions """
|
|
||||||
return self.filter(is_open=True)
|
|
||||||
|
|
||||||
def closed(self):
|
|
||||||
""" only closed positions """
|
|
||||||
return self.filter(is_open=False)
|
|
||||||
|
|
||||||
|
|
||||||
class PositionManager(models.Manager, MixinManager):
|
class PositionManager(models.Manager, MixinManager):
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-05-19 07:58
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
from django.db.models import F
|
||||||
|
|
||||||
|
def forward(apps, schema_editor):
|
||||||
|
Position = apps.get_model('nomcom','Position')
|
||||||
|
Position.objects.update(accepting_nominations=F('is_open'))
|
||||||
|
Position.objects.update(accepting_feedback=F('is_open'))
|
||||||
|
|
||||||
|
def reverse(apps, schema_editor):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('nomcom', '0012_auto_20170210_0205'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='position',
|
||||||
|
name='accepting_feedback',
|
||||||
|
field=models.BooleanField(default=False, verbose_name=b'Is accepting feedback'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='position',
|
||||||
|
name='accepting_nominations',
|
||||||
|
field=models.BooleanField(default=False, verbose_name=b'Is accepting nominations'),
|
||||||
|
),
|
||||||
|
migrations.RunPython(forward,reverse)
|
||||||
|
]
|
20
ietf/nomcom/migrations/0014_alter_is_open_help_text.py
Normal file
20
ietf/nomcom/migrations/0014_alter_is_open_help_text.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.10.7 on 2017-05-22 08:19
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('nomcom', '0013_position_nomination_feedback_switches'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='position',
|
||||||
|
name='is_open',
|
||||||
|
field=models.BooleanField(default=False, help_text=b'Set is_open when the nomcom is working on a position. Clear it when an appointment is confirmed.', verbose_name=b'Is open'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -163,7 +163,9 @@ class Position(models.Model):
|
||||||
name = models.CharField(verbose_name='Name', max_length=255, help_text='This short description will appear on the Nomination and Feedback pages. Be as descriptive as necessary. Past examples: "Transport AD", "IAB Member"')
|
name = models.CharField(verbose_name='Name', max_length=255, help_text='This short description will appear on the Nomination and Feedback pages. Be as descriptive as necessary. Past examples: "Transport AD", "IAB Member"')
|
||||||
requirement = models.ForeignKey(DBTemplate, related_name='requirement', null=True, editable=False)
|
requirement = models.ForeignKey(DBTemplate, related_name='requirement', null=True, editable=False)
|
||||||
questionnaire = models.ForeignKey(DBTemplate, related_name='questionnaire', null=True, editable=False)
|
questionnaire = models.ForeignKey(DBTemplate, related_name='questionnaire', null=True, editable=False)
|
||||||
is_open = models.BooleanField(verbose_name='Is open', default=False)
|
is_open = models.BooleanField(verbose_name='Is open', default=False, help_text="Set is_open when the nomcom is working on a position. Clear it when an appointment is confirmed.")
|
||||||
|
accepting_nominations = models.BooleanField(verbose_name='Is accepting nominations', default=False)
|
||||||
|
accepting_feedback = models.BooleanField(verbose_name='Is accepting feedback', default=False)
|
||||||
|
|
||||||
objects = PositionManager()
|
objects = PositionManager()
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,8 @@ class PositionResource(ModelResource):
|
||||||
"id": ALL,
|
"id": ALL,
|
||||||
"name": ALL,
|
"name": ALL,
|
||||||
"is_open": ALL,
|
"is_open": ALL,
|
||||||
|
"accepting_nominations": ALL,
|
||||||
|
"accepting_feedback": ALL,
|
||||||
"nomcom": ALL_WITH_RELATIONS,
|
"nomcom": ALL_WITH_RELATIONS,
|
||||||
"requirement": ALL_WITH_RELATIONS,
|
"requirement": ALL_WITH_RELATIONS,
|
||||||
"questionnaire": ALL_WITH_RELATIONS,
|
"questionnaire": ALL_WITH_RELATIONS,
|
||||||
|
|
|
@ -130,7 +130,9 @@ def nomcom_test_data():
|
||||||
for name in POSITIONS:
|
for name in POSITIONS:
|
||||||
position, created = Position.objects.get_or_create(nomcom=nomcom,
|
position, created = Position.objects.get_or_create(nomcom=nomcom,
|
||||||
name=name,
|
name=name,
|
||||||
is_open=True)
|
is_open=True,
|
||||||
|
accepting_nominations=True,
|
||||||
|
accepting_feedback=True)
|
||||||
|
|
||||||
ChangeStateGroupEvent.objects.get_or_create(group=group,
|
ChangeStateGroupEvent.objects.get_or_create(group=group,
|
||||||
type="changed_state",
|
type="changed_state",
|
||||||
|
|
|
@ -1771,3 +1771,93 @@ class MergePersonTests(TestCase):
|
||||||
self.assertEqual(self.nc.nominee_set.get(pk=self.nominee2.pk).feedback_set.count(),2)
|
self.assertEqual(self.nc.nominee_set.get(pk=self.nominee2.pk).feedback_set.count(),2)
|
||||||
self.assertFalse(self.nc.nominee_set.filter(pk=self.nominee1.pk).exists())
|
self.assertFalse(self.nc.nominee_set.filter(pk=self.nominee1.pk).exists())
|
||||||
|
|
||||||
|
class AcceptingTests(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
build_test_public_keys_dir(self)
|
||||||
|
self.nc = NomComFactory(**nomcom_kwargs_for_year())
|
||||||
|
self.plain_person = PersonFactory.create()
|
||||||
|
self.member = self.nc.group.role_set.filter(name='member').first().person
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
clean_test_public_keys_dir(self)
|
||||||
|
|
||||||
|
def test_public_accepting_nominations(self):
|
||||||
|
url = reverse('ietf.nomcom.views.public_nominate',kwargs={'year':self.nc.year()})
|
||||||
|
|
||||||
|
login_testing_unauthorized(self,self.plain_person.user.username,url)
|
||||||
|
response = self.client.get(url)
|
||||||
|
q=PyQuery(response.content)
|
||||||
|
self.assertEqual( len(q('#id_position option')) , 4 )
|
||||||
|
|
||||||
|
pos = self.nc.position_set.first()
|
||||||
|
pos.accepting_nominations=False
|
||||||
|
pos.save()
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
q=PyQuery(response.content)
|
||||||
|
self.assertEqual( len(q('#id_position option')) , 3 )
|
||||||
|
|
||||||
|
def test_private_accepting_nominations(self):
|
||||||
|
url = reverse('ietf.nomcom.views.private_nominate',kwargs={'year':self.nc.year()})
|
||||||
|
|
||||||
|
login_testing_unauthorized(self,self.member.user.username,url)
|
||||||
|
response = self.client.get(url)
|
||||||
|
q=PyQuery(response.content)
|
||||||
|
self.assertEqual( len(q('#id_position option')) , 4 )
|
||||||
|
|
||||||
|
pos = self.nc.position_set.first()
|
||||||
|
pos.accepting_nominations=False
|
||||||
|
pos.save()
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
q=PyQuery(response.content)
|
||||||
|
self.assertEqual( len(q('#id_position option')) , 4 )
|
||||||
|
|
||||||
|
def test_public_accepting_feedback(self):
|
||||||
|
url = reverse('ietf.nomcom.views.public_feedback',kwargs={'year':self.nc.year()})
|
||||||
|
|
||||||
|
login_testing_unauthorized(self,self.plain_person.user.username,url)
|
||||||
|
response = self.client.get(url)
|
||||||
|
q=PyQuery(response.content)
|
||||||
|
self.assertEqual( len(q('.badge')) , 3 )
|
||||||
|
|
||||||
|
pos = self.nc.position_set.first()
|
||||||
|
pos.accepting_feedback=False
|
||||||
|
pos.save()
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
q=PyQuery(response.content)
|
||||||
|
self.assertEqual( len(q('.badge')) , 2 )
|
||||||
|
|
||||||
|
url += "?nominee=%d&position=%d" % (pos.nominee_set.first().pk, pos.pk)
|
||||||
|
response = self.client.get(url)
|
||||||
|
self.assertTrue('not currently accepting feedback' in unicontent(response))
|
||||||
|
|
||||||
|
test_data = {'comments': 'junk',
|
||||||
|
'position_name': pos.name,
|
||||||
|
'nominee_name': pos.nominee_set.first().email.person.name,
|
||||||
|
'nominee_email': pos.nominee_set.first().email.address,
|
||||||
|
'confirmation': False,
|
||||||
|
'nominator_email': self.plain_person.email().address,
|
||||||
|
'nominator_name': self.plain_person.plain_name(),
|
||||||
|
}
|
||||||
|
response = self.client.post(url, test_data)
|
||||||
|
self.assertTrue('not currently accepting feedback' in unicontent(response))
|
||||||
|
|
||||||
|
def test_private_accepting_feedback(self):
|
||||||
|
url = reverse('ietf.nomcom.views.private_feedback',kwargs={'year':self.nc.year()})
|
||||||
|
|
||||||
|
login_testing_unauthorized(self,self.member.user.username,url)
|
||||||
|
response = self.client.get(url)
|
||||||
|
q=PyQuery(response.content)
|
||||||
|
self.assertEqual( len(q('.badge')) , 3 )
|
||||||
|
|
||||||
|
pos = self.nc.position_set.first()
|
||||||
|
pos.accepting_feedback=False
|
||||||
|
pos.save()
|
||||||
|
|
||||||
|
response = self.client.get(url)
|
||||||
|
q=PyQuery(response.content)
|
||||||
|
self.assertEqual( len(q('.badge')) , 3 )
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -173,6 +173,7 @@ def private_index(request, year):
|
||||||
if selected_state == questionnaire_state:
|
if selected_state == questionnaire_state:
|
||||||
nominee_positions = [np for np in nominee_positions if np.questionnaires]
|
nominee_positions = [np for np in nominee_positions if np.questionnaires]
|
||||||
|
|
||||||
|
# TODO- just build this dict using open Positions: see #2254
|
||||||
stats = all_nominee_positions.values('position__name', 'position__id').annotate(total=Count('position'))
|
stats = all_nominee_positions.values('position__name', 'position__id').annotate(total=Count('position'))
|
||||||
states = list(NomineePositionStateName.objects.values('slug', 'name')) + [{'slug': questionnaire_state, 'name': u'Questionnaire'}]
|
states = list(NomineePositionStateName.objects.values('slug', 'name')) + [{'slug': questionnaire_state, 'name': u'Questionnaire'}]
|
||||||
positions = set([ n.position for n in all_nominee_positions.order_by('position__name') ])
|
positions = set([ n.position for n in all_nominee_positions.order_by('position__name') ])
|
||||||
|
@ -421,7 +422,10 @@ def feedback(request, year, public):
|
||||||
nominee = get_object_or_404(Nominee, id=selected_nominee)
|
nominee = get_object_or_404(Nominee, id=selected_nominee)
|
||||||
position = get_object_or_404(Position, id=selected_position)
|
position = get_object_or_404(Position, id=selected_position)
|
||||||
|
|
||||||
positions = Position.objects.get_by_nomcom(nomcom=nomcom).opened()
|
if public:
|
||||||
|
positions = Position.objects.get_by_nomcom(nomcom=nomcom).filter(is_open=True,accepting_feedback=True)
|
||||||
|
else:
|
||||||
|
positions = Position.objects.get_by_nomcom(nomcom=nomcom).filter(is_open=True)
|
||||||
|
|
||||||
user_comments = Feedback.objects.filter(nomcom=nomcom,
|
user_comments = Feedback.objects.filter(nomcom=nomcom,
|
||||||
type='comment',
|
type='comment',
|
||||||
|
@ -445,6 +449,18 @@ def feedback(request, year, public):
|
||||||
'base_template': base_template
|
'base_template': base_template
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if public and position and not (position.is_open and position.accepting_feedback):
|
||||||
|
messages.warning(request, "This Nomcom is not currently accepting feedback for "+position.name)
|
||||||
|
return render(request, 'nomcom/feedback.html', {
|
||||||
|
'form': None,
|
||||||
|
'nomcom': nomcom,
|
||||||
|
'year': year,
|
||||||
|
'selected': 'feedback',
|
||||||
|
'positions': positions,
|
||||||
|
'counts' : counts,
|
||||||
|
'base_template': base_template
|
||||||
|
})
|
||||||
|
|
||||||
if nominee and position and request.method == 'POST':
|
if nominee and position and request.method == 'POST':
|
||||||
form = FeedbackForm(data=request.POST,
|
form = FeedbackForm(data=request.POST,
|
||||||
nomcom=nomcom, user=request.user,
|
nomcom=nomcom, user=request.user,
|
||||||
|
|
|
@ -21,6 +21,16 @@
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
{% for position in group.list %}
|
{% for position in group.list %}
|
||||||
<h4>{{ position.name }}</h4>
|
<h4>{{ position.name }}</h4>
|
||||||
|
{% if group.grouper %}
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>Accepting</dt>
|
||||||
|
<dd>
|
||||||
|
{% if position.accepting_nominations %}Nominations{% endif %}
|
||||||
|
{% if position.accepting_nominations and position.accepting_feedback %}and{% endif %}
|
||||||
|
{% if position.accepting_feedback %}Feedback{% endif %}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
{% endif %}
|
||||||
<dl class="dl-horizontal">
|
<dl class="dl-horizontal">
|
||||||
<dt>Templates</dt>
|
<dt>Templates</dt>
|
||||||
<dd>
|
<dd>
|
||||||
|
|
Loading…
Reference in a new issue