Expose views for concluded nomcoms. Close feedback and nomination. Initial work on factory-boy based testing. Partially addresses #1856
- Legacy-Id: 10520
This commit is contained in:
parent
2197259102
commit
e81b473282
10
ietf/group/factories.py
Normal file
10
ietf/group/factories.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
import factory
|
||||
|
||||
from ietf.group.models import Group
|
||||
|
||||
class GroupFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Group
|
||||
|
||||
name = factory.Faker('sentence',nb_words=6)
|
||||
acronym = factory.Faker('word')
|
|
@ -64,9 +64,9 @@ def has_role(user, role_names, *args, **kwargs):
|
|||
"RG Secretary": Q(person=person,name="secr", group__type="rg", group__state__in=["active","proposed"]),
|
||||
"AG Secretary": Q(person=person,name="secr", group__type="ag", group__state__in=["active"]),
|
||||
"Team Chair": Q(person=person,name="chair", group__type="team", group__state="active"),
|
||||
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
|
||||
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
|
||||
"Nomcom": Q(person=person, group__type="nomcom", group__state="active", group__acronym__icontains=kwargs.get('year', '0000')),
|
||||
"Nomcom Chair": Q(person=person, name="chair", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
|
||||
"Nomcom Advisor": Q(person=person, name="advisor", group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
|
||||
"Nomcom": Q(person=person, group__type="nomcom", group__acronym__icontains=kwargs.get('year', '0000')),
|
||||
"Liaison Manager": Q(person=person,name="liaiman",group__type="sdo",group__state="active", ),
|
||||
"Authorized Individual": Q(person=person,name="auth",group__type="sdo",group__state="active", ),
|
||||
}
|
||||
|
|
125
ietf/nomcom/factories.py
Normal file
125
ietf/nomcom/factories.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
import factory
|
||||
import random
|
||||
|
||||
from ietf.nomcom.models import NomCom, Position, Nominee, NomineePosition
|
||||
from ietf.group.factories import GroupFactory
|
||||
from ietf.person.factories import PersonFactory
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
cert = '''-----BEGIN CERTIFICATE-----
|
||||
MIIDHjCCAgagAwIBAgIJAKDCCjbQboJzMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNV
|
||||
BAMMCE5vbUNvbTE1MB4XDTE0MDQwNDIxMTQxNFoXDTE2MDQwMzIxMTQxNFowEzER
|
||||
MA8GA1UEAwwITm9tQ29tMTUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
|
||||
AQC2QXCsAitYSOgPYor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6M
|
||||
cJ+MCiHECdqDlH6npQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv3
|
||||
0hAD6q9wjqK/m6vR5Y1SsvJYV0y+Yu5j9xUEsojMH7O3NlXWAYOb6oH+f/X7PX27
|
||||
IhtiCwfICMmVWh/hKeXuFx6HSOcH3gZ6Tlk1llfDbE/ArpsZ6JmnLn73+64yqIoO
|
||||
ZOc4JJUPrdsmbNwXoxQSQhrpwjN8NpSkQaJbHGB3G+OWvP4fpqcweFHxlEq1Hhef
|
||||
uR9E6jc3qwxVQfwjbcq6N/4JAgMBAAGjdTBzMB0GA1UdDgQWBBTJow+TJynRWsTQ
|
||||
LzoS861FGb/rxDAOBgNVHQ8BAf8EBAMCBLAwDwYDVR0TAQH/BAUwAwEB/zAcBgNV
|
||||
HREEFTATgRFub21jb20xNUBpZXRmLm9yZzATBgNVHSUEDDAKBggrBgEFBQcDBDAN
|
||||
BgkqhkiG9w0BAQsFAAOCAQEAJwLapB9u5N3iK6SCTqh+PVkigZeB2YMVBW8WA3Ut
|
||||
iRPBj+jHWOpF5pzZHTOcNaAxDEG9lyIlcWqc93A24K/Gen11Tx0hO4FAPOG0+PP8
|
||||
4lx7F6xeeyUNR44pInrB93G2q0jl+3wjZH8uhBKlGji4UTMpDPpEl6uiyQCbkMMm
|
||||
Vr7HZH5Dv/lsjGHHf8uJO7+mcMh+tqxLn3DzPrm61OfeWdkoVX2pTz0imRQ3Es+8
|
||||
I7zNMk+fNNaEEyPnEyHfuWq0uD/qKeP27NZIoINy6E3INQ5QaE2uc1nQULg5y7uJ
|
||||
toX3j+FUe2UiUak3ACXdrOPSsFP0KRrFwuMnuHHXkGj/Uw==
|
||||
-----END CERTIFICATE-----
|
||||
'''
|
||||
|
||||
key = '''-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC2QXCsAitYSOgP
|
||||
Yor77zQnEeHuVqlcuhpH1wpKB+N6WcScA5N3AnX9uZEFOt6McJ+MCiHECdqDlH6n
|
||||
pQTJlpCpIVgAD4B6xzjRBRww8d3lClA/kKwsKzuX93RS0Uv30hAD6q9wjqK/m6vR
|
||||
5Y1SsvJYV0y+Yu5j9xUEsojMH7O3NlXWAYOb6oH+f/X7PX27IhtiCwfICMmVWh/h
|
||||
KeXuFx6HSOcH3gZ6Tlk1llfDbE/ArpsZ6JmnLn73+64yqIoOZOc4JJUPrdsmbNwX
|
||||
oxQSQhrpwjN8NpSkQaJbHGB3G+OWvP4fpqcweFHxlEq1HhefuR9E6jc3qwxVQfwj
|
||||
bcq6N/4JAgMBAAECggEAb5SS4YwWc193S2v+QQ2KdVz6YEuINq/tRQw/TWGVACQT
|
||||
PZzm3FaSXDsOsRAAjiSpWTgewgFyWVpBTGu4CZ73g8RZNvhGpWRwwW8KemCpg/8T
|
||||
cEcnUYdKXdhuzAE9LETb7znwHM4Gj55DzCZopjfOLQ2Ne4XgAy2THaQcIjRKd6Bw
|
||||
3mteJ2ityDj3iFN7cq9ntDzp+2BqLOi7AZmLntmUZxtkPCT6k5/dcKFYQW9Eb3bt
|
||||
MON+BIYVzqhAijkP/cAWmbgZAP9EFng5PpE1lc/shl0W8eX4yvjNoMPRq3wphS4j
|
||||
L16VncUeDep3vR0CECx7gnTfR0uCDEgKow50pzGQAQKBgQDaQWwK/o39zI3lCGzy
|
||||
oSNJRNQJ/iZBkbbwpCCaka7VnBfd0ZH54VEWL3oMTkkWRSZtjsPAqT+ndwZitm0D
|
||||
Kww9FUDMP7j/tMOwAUHYfjYFqFTn6ipkBuby9tbZtL7lgJO6Iu2Qk3afqADD0kcP
|
||||
zRLxcYSLjrmp9NyUlNnpswR4CQKBgQDVxjwG/orCmiuyA1Bu4u1hdUD0w9CKnyjp
|
||||
VTbkv8lxk5V3pYzms2Awb0X43W2OioYGBk5yw+9GCF//xCrfbGV7BLZnDTGShjkJ
|
||||
8oTpLPGBsDSfaKVXE3Hko4LVLBMQIm0tDyuPD1Naia7ZknYn906skonEG8WgHUyp
|
||||
c/BgkvzWAQKBgBdojuL6/FWtO8bFyZGYUMWJ+Uf9FzNPIpTatZh+aYcFj9W9pW9s
|
||||
iBreCrQJLXOTBRUZC8u9G1Olw2yQ7k45rr1aazG83+WlCJv29o32s2qV7E1XYyaJ
|
||||
SvniGZcN+K96w91h46Lu/fkPts1J309FinOU3kdtjmI5HfNdp6WWCrOpAoGBAMjc
|
||||
TEaeIK8cwPWwG4E1A6pQy8mvu2Ckj4I+KSfh9FsdOpGDIdMas8SOqQZet7P5AFjk
|
||||
0A0RgN8iu2DMZyQq62cdVG2bffqY1zs7fhrBueILOEaXwtMAWEFmSWYW1YqRbleq
|
||||
K1luIvms6HdSIGcI/gk0XvG+zn/VR9ToNPHo6lwBAoGBAIrYGYPf+cjZ1V/tNqnL
|
||||
IecEZb4Gkp1hVhOpNT4U+T2LROxrZtFxxsw2vuIRa5a5FtMbDq9Xyhkm0QppliBd
|
||||
KQ38jTT0EaD2+vstTqL8vxupo25RQWV1XsmLL4pLbKnm2HnnwB3vEtsiokWKW0q0
|
||||
Tdb0MiLc+r/zvx8oXtgDjDUa
|
||||
-----END PRIVATE KEY-----
|
||||
'''
|
||||
|
||||
def nomcom_kwargs_for_year(year=None, *args, **kwargs):
|
||||
if not year:
|
||||
year = random.randint(1980,2100)
|
||||
if 'group__state_id' not in kwargs:
|
||||
kwargs['group__state_id']='active'
|
||||
if 'group__acronym' not in kwargs:
|
||||
kwargs['group__acronym'] = 'nomcom%d'%year
|
||||
if 'group__name' not in kwargs:
|
||||
kwargs['group__name'] = 'TEST VERSION of IAB/IESG Nominating Committee %d/%d'%(year,year+1)
|
||||
return kwargs
|
||||
|
||||
|
||||
class NomComFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = NomCom
|
||||
|
||||
group = factory.SubFactory(GroupFactory,type_id='nomcom')
|
||||
|
||||
public_key = factory.django.FileField(data=cert)
|
||||
|
||||
@factory.post_generation
|
||||
def populate_positions(self, create, extracted, **kwargs):
|
||||
'''
|
||||
Create a set of nominees and positions unless NomcomFactory is called
|
||||
with populate_positions=False
|
||||
'''
|
||||
if extracted is None:
|
||||
extracted = True
|
||||
if create and extracted:
|
||||
nominees = [Nominee.objects.create(nomcom=self, email=PersonFactory().email_set.first()) for i in range(2)]
|
||||
positions = [PositionFactory(nomcom=self) for i in range(3)]
|
||||
|
||||
def npc(x,y):
|
||||
return NomineePosition.objects.create(position=x,
|
||||
nominee=y,
|
||||
state_id='accepted')
|
||||
# This gives us positions with 0, 1 and 2 nominees, and
|
||||
# one person who's been nomminated for more than one position
|
||||
npc(positions[0],nominees[0])
|
||||
npc(positions[1],nominees[0])
|
||||
npc(positions[1],nominees[1])
|
||||
|
||||
@factory.post_generation
|
||||
def populate_personnel(self, create, extracted, **kwargs):
|
||||
'''
|
||||
Create a default set of role holders, unless the factory is called
|
||||
with populate_personnel=False
|
||||
'''
|
||||
if extracted is None:
|
||||
extracted = True
|
||||
if create and extracted:
|
||||
#roles= ['chair', 'advisor'] + ['member']*10
|
||||
roles = ['chair', 'advisor', 'member']
|
||||
for role in roles:
|
||||
p = PersonFactory()
|
||||
self.group.role_set.create(name_id=role,person=p,email=p.email_set.first())
|
||||
|
||||
class PositionFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Position
|
||||
|
||||
name = factory.Faker('sentence',nb_words=10)
|
||||
description = factory.Faker('paragraph',nb_sentences=4)
|
||||
is_open = True
|
||||
|
|
@ -66,6 +66,14 @@ class NomCom(models.Model):
|
|||
if created:
|
||||
initialize_templates_for_group(self)
|
||||
|
||||
def year(self):
|
||||
year = getattr(self,'_cached_year',None)
|
||||
if year is None:
|
||||
if self.group and self.group.acronym.startswith('nomcom'):
|
||||
year = int(self.group.acronym[6:])
|
||||
self._cached_year = year
|
||||
return year
|
||||
|
||||
|
||||
def delete_nomcom(sender, **kwargs):
|
||||
nomcom = kwargs.get('instance', None)
|
||||
|
|
|
@ -30,6 +30,9 @@ from ietf.nomcom.forms import EditMembersForm, EditMembersFormPreview
|
|||
from ietf.nomcom.utils import get_nomcom_by_year, get_or_create_nominee
|
||||
from ietf.nomcom.management.commands.send_reminders import Command, is_time_to_send
|
||||
|
||||
from ietf.nomcom.factories import NomComFactory, nomcom_kwargs_for_year
|
||||
from ietf.person.factories import PersonFactory
|
||||
|
||||
client_test_cert_files = None
|
||||
|
||||
def get_cert_files():
|
||||
|
@ -920,3 +923,51 @@ class ReminderTest(TestCase):
|
|||
self.assertEqual(len(outbox), messages_before + 1)
|
||||
self.assertTrue('nominee1@' in outbox[-1]['To'])
|
||||
|
||||
class InactiveNomcomTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.nc = NomComFactory.create(**nomcom_kwargs_for_year(group__state_id='conclude'))
|
||||
self.plain_person = PersonFactory.create()
|
||||
|
||||
def test_feedback_closed(self):
|
||||
for view in ['nomcom_public_feedback', 'nomcom_private_feedback']:
|
||||
url = reverse(view, kwargs={'year': self.nc.year()})
|
||||
who = self.plain_person if 'public' in view else self.nc.group.role_set.filter(name='member').first().person
|
||||
login_testing_unauthorized(self, who.user.username, url)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertTrue( '(Concluded)' in q('h1').text())
|
||||
self.assertTrue( 'closed' in q('#instructions').text())
|
||||
self.assertTrue( q('#nominees a') )
|
||||
self.assertFalse( q('#nominees a[href]') )
|
||||
|
||||
url += "?nominee=%d&position=%d" % (self.nc.nominee_set.first().id, self.nc.nominee_set.first().nomineeposition_set.first().position.id)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertFalse( q('#feedbackform'))
|
||||
|
||||
empty_outbox()
|
||||
fb_before = self.nc.feedback_set.count()
|
||||
test_data = {'comments': u'Test feedback view. Comments with accents äöåÄÖÅ éáíóú âêîôû ü àèìòù.',
|
||||
'nominator_email': self.plain_person.email_set.first().address,
|
||||
'confirmation': True}
|
||||
response = self.client.post(url, test_data)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertTrue( 'closed' in q('#instructions').text())
|
||||
self.assertEqual( len(outbox), 0 )
|
||||
self.assertEqual( fb_before, self.nc.feedback_set.count() )
|
||||
|
||||
def test_nominations_closed(self):
|
||||
for view in ['nomcom_public_nominate', 'nomcom_private_nominate']:
|
||||
url = reverse(view, kwargs={'year': self.nc.year() })
|
||||
who = self.plain_person if 'public' in view else self.nc.group.role_set.filter(name='member').first().person
|
||||
login_testing_unauthorized(self, who.user.username, url)
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
q = PyQuery(response.content)
|
||||
self.assertTrue( '(Concluded)' in q('h1').text())
|
||||
self.assertTrue( 'closed' in q('.alert-warning').text())
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ def get_nomcom_by_year(year):
|
|||
from ietf.nomcom.models import NomCom
|
||||
return get_object_or_404(NomCom,
|
||||
group__acronym__icontains=year,
|
||||
group__state__slug='active')
|
||||
)
|
||||
|
||||
|
||||
def get_year_by_nomcom(nomcom):
|
||||
|
|
|
@ -322,6 +322,14 @@ def nominate(request, year, public):
|
|||
'year': year,
|
||||
'selected': 'nominate'}, RequestContext(request))
|
||||
|
||||
if nomcom.group.state_id == 'conclude':
|
||||
message = ('warning', "Nominations to this Nomcom are closed.")
|
||||
return render_to_response(template,
|
||||
{'message': message,
|
||||
'nomcom': nomcom,
|
||||
'year': year,
|
||||
'selected': 'nominate'}, RequestContext(request))
|
||||
|
||||
message = None
|
||||
if request.method == 'POST':
|
||||
form = NominateForm(data=request.POST, nomcom=nomcom, user=request.user, public=public)
|
||||
|
@ -355,6 +363,7 @@ def feedback(request, year, public):
|
|||
has_publickey = nomcom.public_key and True or False
|
||||
nominee = None
|
||||
position = None
|
||||
if nomcom.group.state_id != 'conclude':
|
||||
selected_nominee = request.GET.get('nominee')
|
||||
selected_position = request.GET.get('position')
|
||||
if selected_nominee and selected_position:
|
||||
|
@ -379,7 +388,7 @@ def feedback(request, year, public):
|
|||
})
|
||||
|
||||
message = None
|
||||
if request.method == 'POST':
|
||||
if nominee and position and request.method == 'POST':
|
||||
form = FeedbackForm(data=request.POST,
|
||||
nomcom=nomcom, user=request.user,
|
||||
public=public, position=position, nominee=nominee)
|
||||
|
|
56
ietf/person/factories.py
Normal file
56
ietf/person/factories.py
Normal file
|
@ -0,0 +1,56 @@
|
|||
import factory
|
||||
import faker
|
||||
|
||||
from unidecode import unidecode
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from ietf.person.models import Person, Alias, Email
|
||||
|
||||
fake = faker.Factory.create()
|
||||
|
||||
class UserFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = User
|
||||
django_get_or_create = ('username',)
|
||||
|
||||
first_name = factory.Faker('first_name')
|
||||
last_name = factory.Faker('last_name')
|
||||
email = factory.LazyAttribute(lambda u: '%s.%s@%s'%(u.first_name,u.last_name,fake.domain_name()))
|
||||
username = factory.LazyAttribute(lambda u: u.email)
|
||||
|
||||
@factory.post_generation
|
||||
def set_password(self, create, extracted, **kwargs):
|
||||
self.set_password( '%s+password' % self.username )
|
||||
|
||||
class PersonFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Person
|
||||
|
||||
user = factory.SubFactory(UserFactory)
|
||||
name = factory.LazyAttribute(lambda p: '%s %s'%(p.user.first_name,p.user.last_name))
|
||||
ascii = factory.LazyAttribute(lambda p: unidecode(p.name))
|
||||
|
||||
@factory.post_generation
|
||||
def default_aliases(self, create, extracted, **kwargs):
|
||||
make_alias = getattr(AliasFactory, 'create' if create else 'build')
|
||||
make_alias(person=self,name=self.name)
|
||||
make_alias(person=self,name=self.ascii)
|
||||
|
||||
@factory.post_generation
|
||||
def default_emails(self, create, extracted, **kwargs):
|
||||
make_email = getattr(EmailFactory, 'create' if create else 'build')
|
||||
make_email(person=self,address=self.user.email)
|
||||
|
||||
class AliasFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Alias
|
||||
django_get_or_create = ('name',)
|
||||
|
||||
name = factory.Faker('name')
|
||||
|
||||
class EmailFactory(factory.DjangoModelFactory):
|
||||
class Meta:
|
||||
model = Email
|
||||
django_get_or_create = ('address',)
|
||||
|
||||
address = '%s.%s@%s' % (factory.Faker('first_name'),factory.Faker('last_name'),factory.Faker('domain_name'))
|
|
@ -9,8 +9,12 @@
|
|||
|
||||
{% block nomcom_content %}
|
||||
{% origin %}
|
||||
<p class="alert alert-info">
|
||||
<p id="instructions" class="alert alert-info">
|
||||
{% if nomcom.group.state_id == 'conclude' %}
|
||||
Feedback to this nomcom is closed.
|
||||
{% else %}
|
||||
Select a nominee from the list of nominees to the right to obtain a new feedback form.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if message %}
|
||||
|
@ -21,7 +25,7 @@
|
|||
|
||||
{% if nomcom|has_publickey %}
|
||||
<div class="row">
|
||||
<div class="col-sm-4 col-sm-push-8">
|
||||
<div id="nominees" class="col-sm-4 col-sm-push-8">
|
||||
<h3>Nominees</h3>
|
||||
|
||||
{% for p in positions %}
|
||||
|
@ -29,7 +33,7 @@
|
|||
<h4>{{ p.name }}</h4>
|
||||
<div class="btn-group-vertical form-group">
|
||||
{% for np in p.nomineeposition_set.accepted.not_duplicated %}
|
||||
<a class="btn btn-default btn-xs" href="?nominee={{np.nominee.id}}&position={{ np.position.id}}">
|
||||
<a class="btn btn-default btn-xs" {% if nomcom.group.state_id != 'conclude' %}href="?nominee={{np.nominee.id}}&position={{ np.position.id}}"{% endif %}>
|
||||
{{ np.nominee }}
|
||||
{% add_num_nominations user np.position np.nominee %}
|
||||
</a>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<h1>NomCom {{ year }} <small>Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}</small></h1>
|
||||
<h1>NomCom {{ year }} {% if nomcom.group.state_id == 'conclude' %}(Concluded){% endif %} <small>Private area {% if is_chair_task %}- Chair/Advisors only{% endif %}</small></h1>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li {% if selected == "index" %}class="active"{% endif %}><a href="{% url "nomcom_private_index" year %}">Nominees</a></li>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
{% block content %}
|
||||
{% origin %}
|
||||
|
||||
<h1>NomCom {{ year }}</h1>
|
||||
<h1>NomCom {{ year }} {% if nomcom.group.state_id == 'conclude' %}(Concluded){% endif %}</h1>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist">
|
||||
<li {% if selected == "index" %}class="active"{% endif %}><a href="{% url "nomcom_year_index" year %}">Home</a></li>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<p class="alert alert-{{ message.0 }}">{{ message.1 }}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if nomcom|has_publickey %}
|
||||
{% if form %}
|
||||
|
||||
<form id="nominate-form" method="post">
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
{% bootstrap_messages %}
|
||||
|
||||
{% if nomcom|has_publickey %}
|
||||
{% if form %}
|
||||
<form id="nominate-form" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
|
|
|
@ -25,3 +25,5 @@ six>=1.8.0
|
|||
wsgiref>=0.1.2
|
||||
xml2rfc>=2.5.0
|
||||
django>=1.7.10,<1.8
|
||||
factory-boy>=2.6.0
|
||||
Unidecode>=0.4.18
|
||||
|
|
Loading…
Reference in a new issue