Add person, affiliation and country (through django-countries) to

DocumentAuthor, rename author field to email and make it optional (for
modeling old email-less submissions), remove the authors many to many
referencing field from Document as it is not really pointing the right
place.

Update the Secretariat tools to show affiliation and country.

Add migration for getting rid of the fake email addresses that the
migration script created some years ago (just set the author email
field to null).
 - Legacy-Id: 12739
This commit is contained in:
Ole Laursen 2017-01-26 17:10:08 +00:00
parent e381dac958
commit 9308948195
43 changed files with 329 additions and 142 deletions

View file

@ -212,9 +212,9 @@ class ToOneField(tastypie.fields.ToOneField):
if not foreign_obj:
if not self.null:
if callable(self.attribute):
raise ApiFieldError("The related resource for resource %s could not be found." % (previous_obj))
raise ApiFieldError(u"The related resource for resource %s could not be found." % (previous_obj))
else:
raise ApiFieldError("The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr))
raise ApiFieldError(u"The model '%r' has an empty attribute '%s' and doesn't allow a null value." % (previous_obj, attr))
return None
fk_resource = self.get_related_resource(foreign_obj)

View file

@ -66,7 +66,7 @@ if "<" in from_email:
submission = Submission.objects.filter(name=draft).latest('submission_date')
document = Document.objects.get(name=draft)
emails = [ author.address for author in document.authors.all() ]
emails = [ author.email.address for author in document.documentauthor_set.all() if author.email ]
timestrings = []
for file in [ Path(settings.INTERNET_DRAFT_PATH) / ("%s-%s.txt"%(draft, submission.rev)),

View file

@ -65,7 +65,7 @@ def get_draft_authors_emails(draft):
" Get list of authors for the given draft."
# This feels 'correct'; however, it creates fairly large delta
return [email.email_address() for email in draft.authors.all()]
return [author.email.email_address() for author in draft.documentauthor_set.all() if author.email.email_address()]
# This gives fairly small delta compared to current state,
# however, it seems to be wrong (doesn't check for emails being

View file

@ -57,7 +57,7 @@ def port_rules_to_typed_system(apps, schema_editor):
elif rule.rule_type in ["author", "author_rfc"]:
found_persons = list(try_to_uniquify_person(rule, Person.objects.filter(email__documentauthor__id__gte=1).filter(name__icontains=rule.value).distinct()))
found_persons = list(try_to_uniquify_person(rule, Person.objects.filter(documentauthor__id__gte=1).filter(name__icontains=rule.value).distinct()))
if found_persons:
rule.person = found_persons[0]

View file

@ -31,7 +31,7 @@ class CommunityListTests(TestCase):
rule_state_iesg = SearchRule.objects.create(rule_type="state_iesg", state=State.objects.get(type="draft-iesg", slug="lc"), community_list=clist)
rule_author = SearchRule.objects.create(rule_type="author", state=State.objects.get(type="draft", slug="active"), person=Person.objects.filter(email__documentauthor__document=draft).first(), community_list=clist)
rule_author = SearchRule.objects.create(rule_type="author", state=State.objects.get(type="draft", slug="active"), person=Person.objects.filter(documentauthor__document=draft).first(), community_list=clist)
rule_ad = SearchRule.objects.create(rule_type="ad", state=State.objects.get(type="draft", slug="active"), person=draft.ad, community_list=clist)
@ -113,7 +113,7 @@ class CommunityListTests(TestCase):
r = self.client.post(url, {
"action": "add_rule",
"rule_type": "author_rfc",
"author_rfc-person": Person.objects.filter(email__documentauthor__document=draft).first().pk,
"author_rfc-person": Person.objects.filter(documentauthor__document=draft).first().pk,
"author_rfc-state": State.objects.get(type="draft", slug="rfc").pk,
})
self.assertEqual(r.status_code, 302)

View file

@ -88,7 +88,7 @@ def docs_matching_community_list_rule(rule):
elif rule.rule_type.startswith("state_"):
return docs.filter(states=rule.state)
elif rule.rule_type in ["author", "author_rfc"]:
return docs.filter(states=rule.state, documentauthor__author__person=rule.person)
return docs.filter(states=rule.state, documentauthor__person=rule.person)
elif rule.rule_type == "ad":
return docs.filter(states=rule.state, ad=rule.person)
elif rule.rule_type == "shepherd":
@ -121,7 +121,7 @@ def community_list_rules_matching_doc(doc):
rules |= SearchRule.objects.filter(
rule_type__in=["author", "author_rfc"],
state__in=states,
person__in=list(Person.objects.filter(email__documentauthor__document=doc)),
person__in=list(Person.objects.filter(documentauthor__document=doc)),
)
if doc.ad_id:

View file

@ -25,7 +25,7 @@ class DocAliasInline(admin.TabularInline):
class DocAuthorInline(admin.TabularInline):
model = DocumentAuthor
raw_id_fields = ['author', ]
raw_id_fields = ['person', 'email']
extra = 1
class RelatedDocumentInline(admin.TabularInline):
@ -99,7 +99,7 @@ class DocumentAdmin(admin.ModelAdmin):
list_display = ['name', 'rev', 'group', 'pages', 'intended_std_level', 'author_list', 'time']
search_fields = ['name']
list_filter = ['type']
raw_id_fields = ['authors', 'group', 'shepherd', 'ad']
raw_id_fields = ['group', 'shepherd', 'ad']
inlines = [DocAliasInline, DocAuthorInline, RelatedDocumentInline, ]
form = DocumentForm
@ -121,7 +121,7 @@ class DocHistoryAdmin(admin.ModelAdmin):
list_display = ['doc', 'rev', 'state', 'group', 'pages', 'intended_std_level', 'author_list', 'time']
search_fields = ['doc__name']
ordering = ['time', 'doc', 'rev']
raw_id_fields = ['doc', 'authors', 'group', 'shepherd', 'ad']
raw_id_fields = ['doc', 'group', 'shepherd', 'ad']
def state(self, instance):
return instance.get_state()
@ -174,7 +174,7 @@ class BallotPositionDocEventAdmin(DocEventAdmin):
admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin)
class DocumentAuthorAdmin(admin.ModelAdmin):
list_display = ['id', 'document', 'author', 'order']
search_fields = [ 'document__name', 'author__address', ]
list_display = ['id', 'document', 'person', 'email', 'order']
search_fields = [ 'document__name', 'person__name', 'email__address', ]
admin.site.register(DocumentAuthor, DocumentAuthorAdmin)

View file

@ -46,8 +46,8 @@ class DocumentFactory(factory.DjangoModelFactory):
def authors(obj, create, extracted, **kwargs): # pylint: disable=no-self-argument
if create and extracted:
order = 0
for email in extracted:
DocumentAuthor.objects.create(document=obj, author=email, order=order)
for person in extracted:
DocumentAuthor.objects.create(document=obj, person=person, order=order)
order += 1
@classmethod

View file

@ -2,6 +2,7 @@
from __future__ import unicode_literals
from django.db import migrations, models
import django_countries.fields
class Migration(migrations.Migration):
@ -32,4 +33,84 @@ class Migration(migrations.Migration):
name='formal_languages',
field=models.ManyToManyField(help_text=b'Formal languages used in document', to='name.FormalLanguageName', blank=True),
),
migrations.RemoveField(
model_name='dochistory',
name='authors',
),
migrations.RemoveField(
model_name='document',
name='authors',
),
migrations.AddField(
model_name='dochistoryauthor',
name='affiliation',
field=models.CharField(help_text=b'Organization/company used by author for submission', max_length=100, blank=True),
),
migrations.AddField(
model_name='dochistoryauthor',
name='country',
field=django_countries.fields.CountryField(blank=True, help_text=b'Country used by author for submission', max_length=2),
),
migrations.RenameField(
model_name='dochistoryauthor',
old_name='author',
new_name='email',
),
migrations.AlterField(
model_name='dochistoryauthor',
name='email',
field=models.ForeignKey(blank=True, to='person.Email', help_text=b'Email address used by author for submission', null=True),
),
migrations.AddField(
model_name='dochistoryauthor',
name='person',
field=models.ForeignKey(blank=True, to='person.Person', null=True),
),
migrations.AddField(
model_name='documentauthor',
name='affiliation',
field=models.CharField(help_text=b'Organization/company used by author for submission', max_length=100, blank=True),
),
migrations.AddField(
model_name='documentauthor',
name='country',
field=django_countries.fields.CountryField(blank=True, help_text=b'Country used by author for submission', max_length=2),
),
migrations.RenameField(
model_name='documentauthor',
old_name='author',
new_name='email',
),
migrations.AlterField(
model_name='documentauthor',
name='email',
field=models.ForeignKey(blank=True, to='person.Email', help_text=b'Email address used by author for submission', null=True),
),
migrations.AddField(
model_name='documentauthor',
name='person',
field=models.ForeignKey(blank=True, to='person.Person', null=True),
),
migrations.AlterField(
model_name='dochistoryauthor',
name='document',
field=models.ForeignKey(related_name='documentauthor_set', to='doc.DocHistory'),
),
migrations.AlterField(
model_name='dochistoryauthor',
name='order',
field=models.IntegerField(default=1),
),
migrations.RunSQL("update doc_documentauthor a inner join person_email e on a.email_id = e.address set a.person_id = e.person_id;", migrations.RunSQL.noop),
migrations.RunSQL("update doc_dochistoryauthor a inner join person_email e on a.email_id = e.address set a.person_id = e.person_id;", migrations.RunSQL.noop),
migrations.AlterField(
model_name='documentauthor',
name='person',
field=models.ForeignKey(to='person.Person'),
),
migrations.AlterField(
model_name='dochistoryauthor',
name='person',
field=models.ForeignKey(to='person.Person'),
),
]

View file

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def fix_invalid_emails(apps, schema_editor):
Email = apps.get_model("person", "Email")
Role = apps.get_model("group", "Role")
RoleHistory = apps.get_model("group", "RoleHistory")
e = Email.objects.filter(address="unknown-email-Gigi-Karmous-Edwards").first()
if e:
# according to ftp://ietf.org/ietf/97dec/adsl-minutes-97dec.txt
new_e, _ = Email.objects.get_or_create(
address="GiGi.Karmous-Edwards@pulse.com",
primary=e.primary,
active=e.active,
person=e.person,
)
Role.objects.filter(email=e).update(email=new_e)
RoleHistory.objects.filter(email=e).update(email=new_e)
e.delete()
e = Email.objects.filter(address="unknown-email-Pat-Thaler").first()
if e:
# current chair email
new_e = Email.objects.get(address="pat.thaler@broadcom.com")
Role.objects.filter(email=e).update(email=new_e)
RoleHistory.objects.filter(email=e).update(email=new_e)
e.delete()
Email = apps.get_model("person", "Email")
DocumentAuthor = apps.get_model("doc", "DocumentAuthor")
DocHistoryAuthor = apps.get_model("doc", "DocHistoryAuthor")
DocumentAuthor.objects.filter(email__address__startswith="unknown-email-").exclude(email__address__contains="@").update(email=None)
DocHistoryAuthor.objects.filter(email__address__startswith="unknown-email-").exclude(email__address__contains="@").update(email=None)
Email.objects.exclude(address__contains="@").filter(address__startswith="unknown-email-").delete()
class Migration(migrations.Migration):
dependencies = [
('doc', '0020_auto_20170112_0753'),
('person', '0014_auto_20160613_0751'),
('group', '0009_auto_20150930_0758'),
]
operations = [
migrations.RunPython(fix_invalid_emails, migrations.RunPython.noop),
]

View file

@ -11,6 +11,8 @@ from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.utils.html import mark_safe
from django_countries.fields import CountryField
import debug # pyflakes:ignore
from ietf.group.models import Group
@ -254,7 +256,7 @@ class DocumentInfo(models.Model):
return state.name
def author_list(self):
return ", ".join(email.address for email in self.authors.all())
return u", ".join(author.email_id for author in self.documentauthor_set.all() if author.email_id)
# This, and several other ballot related functions here, assume that there is only one active ballot for a document at any point in time.
# If that assumption is violated, they will only expose the most recently created ballot
@ -399,20 +401,32 @@ class RelatedDocument(models.Model):
return None
class DocumentAuthor(models.Model):
document = models.ForeignKey('Document')
author = models.ForeignKey(Email, help_text="Email address used by author for submission")
class DocumentAuthorInfo(models.Model):
person = models.ForeignKey(Person)
# email should only be null for some historic documents
email = models.ForeignKey(Email, help_text="Email address used by author for submission", blank=True, null=True)
affiliation = models.CharField(max_length=100, blank=True, help_text="Organization/company used by author for submission")
country = CountryField(blank=True, help_text="Country used by author for submission")
order = models.IntegerField(default=1)
def __unicode__(self):
return u"%s %s (%s)" % (self.document.name, self.author.get_name(), self.order)
def formatted_email(self):
if self.email:
return u'"%s" <%s>' % (self.person.plain_ascii(), self.email.address)
else:
return ""
class Meta:
abstract = True
ordering = ["document", "order"]
class DocumentAuthor(DocumentAuthorInfo):
document = models.ForeignKey('Document')
def __unicode__(self):
return u"%s %s (%s)" % (self.document.name, self.person, self.order)
class Document(DocumentInfo):
name = models.CharField(max_length=255, primary_key=True) # immutable
authors = models.ManyToManyField(Email, through=DocumentAuthor, blank=True)
def __unicode__(self):
return self.name
@ -609,16 +623,13 @@ class RelatedDocHistory(models.Model):
def __unicode__(self):
return u"%s %s %s" % (self.source.doc.name, self.relationship.name.lower(), self.target.name)
class DocHistoryAuthor(models.Model):
document = models.ForeignKey('DocHistory')
author = models.ForeignKey(Email)
order = models.IntegerField()
class DocHistoryAuthor(DocumentAuthorInfo):
# use same naming convention as non-history version to make it a bit
# easier to write generic code
document = models.ForeignKey('DocHistory', related_name="documentauthor_set")
def __unicode__(self):
return u"%s %s (%s)" % (self.document.doc.name, self.author.get_name(), self.order)
class Meta:
ordering = ["document", "order"]
return u"%s %s (%s)" % (self.document.doc.name, self.person, self.order)
class DocHistory(DocumentInfo):
doc = models.ForeignKey(Document, related_name="history_set")
@ -627,7 +638,7 @@ class DocHistory(DocumentInfo):
# canonical_name and replace the function on Document with a
# property
name = models.CharField(max_length=255)
authors = models.ManyToManyField(Email, through=DocHistoryAuthor, blank=True)
def __unicode__(self):
return unicode(self.doc.name)

View file

@ -99,7 +99,6 @@ class DocumentResource(ModelResource):
shepherd = ToOneField(EmailResource, 'shepherd', null=True)
states = ToManyField(StateResource, 'states', null=True)
tags = ToManyField(DocTagNameResource, 'tags', null=True)
authors = ToManyField(EmailResource, 'authors', null=True)
rfc = CharField(attribute='rfc_number', null=True)
class Meta:
cache = SimpleCache()
@ -128,14 +127,14 @@ class DocumentResource(ModelResource):
"shepherd": ALL_WITH_RELATIONS,
"states": ALL_WITH_RELATIONS,
"tags": ALL_WITH_RELATIONS,
"authors": ALL_WITH_RELATIONS,
}
api.doc.register(DocumentResource())
from ietf.person.resources import EmailResource
from ietf.person.resources import PersonResource, EmailResource
class DocumentAuthorResource(ModelResource):
person = ToOneField(PersonResource, 'person')
email = ToOneField(EmailResource, 'email', null=True)
document = ToOneField(DocumentResource, 'document')
author = ToOneField(EmailResource, 'author')
class Meta:
cache = SimpleCache()
queryset = DocumentAuthor.objects.all()
@ -143,9 +142,12 @@ class DocumentAuthorResource(ModelResource):
#resource_name = 'documentauthor'
filtering = {
"id": ALL,
"affiliation": ALL,
"country": ALL,
"order": ALL,
"person": ALL_WITH_RELATIONS,
"email": ALL_WITH_RELATIONS,
"document": ALL_WITH_RELATIONS,
"author": ALL_WITH_RELATIONS,
}
api.doc.register(DocumentAuthorResource())
@ -207,7 +209,6 @@ class DocHistoryResource(ModelResource):
doc = ToOneField(DocumentResource, 'doc')
states = ToManyField(StateResource, 'states', null=True)
tags = ToManyField(DocTagNameResource, 'tags', null=True)
authors = ToManyField(EmailResource, 'authors', null=True)
class Meta:
cache = SimpleCache()
queryset = DocHistory.objects.all()
@ -237,7 +238,6 @@ class DocHistoryResource(ModelResource):
"doc": ALL_WITH_RELATIONS,
"states": ALL_WITH_RELATIONS,
"tags": ALL_WITH_RELATIONS,
"authors": ALL_WITH_RELATIONS,
}
api.doc.register(DocHistoryResource())
@ -405,10 +405,11 @@ class InitialReviewDocEventResource(ModelResource):
}
api.doc.register(InitialReviewDocEventResource())
from ietf.person.resources import EmailResource
from ietf.person.resources import PersonResource, EmailResource
class DocHistoryAuthorResource(ModelResource):
person = ToOneField(PersonResource, 'person')
email = ToOneField(EmailResource, 'email', null=True)
document = ToOneField(DocHistoryResource, 'document')
author = ToOneField(EmailResource, 'author')
class Meta:
cache = SimpleCache()
queryset = DocHistoryAuthor.objects.all()
@ -416,9 +417,12 @@ class DocHistoryAuthorResource(ModelResource):
#resource_name = 'dochistoryauthor'
filtering = {
"id": ALL,
"affiliation": ALL,
"country": ALL,
"order": ALL,
"person": ALL_WITH_RELATIONS,
"email": ALL_WITH_RELATIONS,
"document": ALL_WITH_RELATIONS,
"author": ALL_WITH_RELATIONS,
}
api.doc.register(DocHistoryAuthorResource())

View file

@ -87,7 +87,7 @@ class SearchTests(TestCase):
self.assertTrue(draft.title in unicontent(r))
# find by author
r = self.client.get(base_url + "?activedrafts=on&by=author&author=%s" % draft.authors.all()[0].person.name_parts()[1])
r = self.client.get(base_url + "?activedrafts=on&by=author&author=%s" % draft.documentauthor_set.first().person.name_parts()[1])
self.assertEqual(r.status_code, 200)
self.assertTrue(draft.title in unicontent(r))
@ -1223,7 +1223,7 @@ class ChartTests(ResourceTestCaseMixin, TestCase):
person = PersonFactory.create()
DocumentFactory.create(
states=[('draft','active')],
authors=[person.email(), ],
authors=[person, ],
)
conf_url = urlreverse('ietf.doc.views_stats.chart_conf_person_drafts', kwargs=dict(id=person.id))

View file

@ -377,7 +377,10 @@ class EditInfoTests(TestCase):
DocumentAuthor.objects.create(
document=draft,
author=Email.objects.get(address="aread@ietf.org"),
person=Person.objects.get(email__address="aread@ietf.org"),
email=Email.objects.get(address="aread@ietf.org"),
country="US",
affiliation="",
order=1
)
@ -1361,7 +1364,9 @@ class ChangeReplacesTests(TestCase):
expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
group=mars_wg,
)
self.basea.documentauthor_set.create(author=Email.objects.create(address="basea_author@example.com"),order=1)
p = Person.objects.create(address="basea_author")
e = Email.objects.create(address="basea_author@example.com", person=p)
self.basea.documentauthor_set.create(person=p, email=e, order=1)
self.baseb = Document.objects.create(
name="draft-test-base-b",
@ -1372,7 +1377,9 @@ class ChangeReplacesTests(TestCase):
expires=datetime.datetime.now() - datetime.timedelta(days = 365 - settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
group=mars_wg,
)
self.baseb.documentauthor_set.create(author=Email.objects.create(address="baseb_author@example.com"),order=1)
p = Person.objects.create(name="baseb_author")
e = Email.objects.create(address="baseb_author@example.com", person=p)
self.baseb.documentauthor_set.create(person=p, email=e, order=1)
self.replacea = Document.objects.create(
name="draft-test-replace-a",
@ -1383,7 +1390,9 @@ class ChangeReplacesTests(TestCase):
expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
group=mars_wg,
)
self.replacea.documentauthor_set.create(author=Email.objects.create(address="replacea_author@example.com"),order=1)
p = Person.objects.create(name="replacea_author")
e = Email.objects.create(address="replacea_author@example.com", person=p)
self.replacea.documentauthor_set.create(person=p, email=e, order=1)
self.replaceboth = Document.objects.create(
name="draft-test-replace-both",
@ -1394,7 +1403,9 @@ class ChangeReplacesTests(TestCase):
expires=datetime.datetime.now() + datetime.timedelta(days = settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
group=mars_wg,
)
self.replaceboth.documentauthor_set.create(author=Email.objects.create(address="replaceboth_author@example.com"),order=1)
p = Person.objects.create(name="replaceboth_author")
e = Email.objects.create(address="replaceboth_author@example.com", person=p)
self.replaceboth.documentauthor_set.create(person=p, email=e, order=1)
self.basea.set_state(State.objects.get(used=True, type="draft", slug="active"))
self.baseb.set_state(State.objects.get(used=True, type="draft", slug="expired"))

View file

@ -258,10 +258,7 @@ class ReviewTests(TestCase):
# set up some reviewer-suitability factors
reviewer_email = Email.objects.get(person__user__username="reviewer")
DocumentAuthor.objects.create(
author=reviewer_email,
document=doc,
)
DocumentAuthor.objects.create(person=reviewer_email.person, email=reviewer_email, document=doc)
doc.rev = "10"
doc.save_with_history([DocEvent.objects.create(doc=doc, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])

View file

@ -57,7 +57,6 @@ from ietf.group.models import Role
from ietf.group.utils import can_manage_group, can_manage_materials
from ietf.ietfauth.utils import has_role, is_authorized_in_doc_stream, user_is_person, role_required
from ietf.name.models import StreamName, BallotPositionName
from ietf.person.models import Email
from ietf.utils.history import find_history_active_at
from ietf.doc.forms import TelechatForm, NotifyForm
from ietf.doc.mails import email_comment
@ -167,7 +166,7 @@ def document_main(request, name, rev=None):
can_edit_replaces = has_role(request.user, ("Area Director", "Secretariat", "IRTF Chair", "WG Chair", "RG Chair", "WG Secretary", "RG Secretary"))
is_author = unicode(request.user) in set([email.address for email in doc.authors.all()])
is_author = request.user.is_authenticated() and doc.documentauthor_set.filter(person__user=request.user).exists()
can_view_possibly_replaces = can_edit_replaces or is_author
rfc_number = name[3:] if name.startswith("") else None
@ -957,11 +956,11 @@ def document_json(request, name, rev=None):
data["intended_std_level"] = extract_name(doc.intended_std_level)
data["std_level"] = extract_name(doc.std_level)
data["authors"] = [
dict(name=e.person.name,
email=e.address,
affiliation=e.person.affiliation)
for e in Email.objects.filter(documentauthor__document=doc).select_related("person").order_by("documentauthor__order")
]
dict(name=author.person.name,
email=author.email.address,
affiliation=author.affiliation)
for author in doc.documentauthor_set.all().select_related("person", "email").order_by("order")
]
data["shepherd"] = doc.shepherd.formatted_email() if doc.shepherd else None
data["ad"] = doc.ad.role_email("ad").formatted_email() if doc.ad else None

View file

@ -167,7 +167,7 @@ def retrieve_search_results(form, all_types=False):
# radio choices
by = query["by"]
if by == "author":
docs = docs.filter(authors__person__alias__name__icontains=query["author"])
docs = docs.filter(documentauthor__person__alias__name__icontains=query["author"])
elif by == "group":
docs = docs.filter(group__acronym=query["group"])
elif by == "area":

View file

@ -170,7 +170,7 @@ def chart_data_person_drafts(request, id):
if not person:
data = []
else:
data = model_to_timeline_data(DocEvent, doc__authors__person=person, type='new_revision')
data = model_to_timeline_data(DocEvent, doc__documentauthor__person=person, type='new_revision')
return JsonResponse(data, safe=False)

View file

@ -115,15 +115,15 @@ def all_id2_txt():
file_types = file_types_for_drafts()
authors = {}
for a in DocumentAuthor.objects.filter(document__name__startswith="draft-").order_by("order").select_related("author", "author__person").iterator():
for a in DocumentAuthor.objects.filter(document__name__startswith="draft-").order_by("order").select_related("email", "person").iterator():
if a.document_id not in authors:
l = authors[a.document_id] = []
else:
l = authors[a.document_id]
if "@" in a.author.address:
l.append(u'%s <%s>' % (a.author.person.plain_name().replace("@", ""), a.author.address.replace(",", "")))
if a.email:
l.append(u'%s <%s>' % (a.person.plain_name().replace("@", ""), a.email.address.replace(",", "")))
else:
l.append(a.author.person.plain_name())
l.append(a.person.plain_name())
shepherds = dict((e.pk, e.formatted_email().replace('"', ''))
for e in Email.objects.filter(shepherd_document_set__type="draft").select_related("person").distinct())
@ -234,12 +234,12 @@ def active_drafts_index_by_group(extra_values=()):
d["initial_rev_time"] = time
# add authors
for a in DocumentAuthor.objects.filter(document__states=active_state).order_by("order").select_related("author__person"):
for a in DocumentAuthor.objects.filter(document__states=active_state).order_by("order").select_related("person"):
d = docs_dict.get(a.document_id)
if d:
if "authors" not in d:
d["authors"] = []
d["authors"].append(a.author.person.plain_ascii()) # This should probably change to .plain_name() when non-ascii names are permitted
d["authors"].append(a.person.plain_ascii()) # This should probably change to .plain_name() when non-ascii names are permitted
# put docs into groups
for d in docs_dict.itervalues():

View file

@ -99,7 +99,7 @@ class IndexTests(TestCase):
self.assertEqual(t[12], ".pdf,.txt")
self.assertEqual(t[13], draft.title)
author = draft.documentauthor_set.order_by("order").get()
self.assertEqual(t[14], u"%s <%s>" % (author.author.person.name, author.author.address))
self.assertEqual(t[14], u"%s <%s>" % (author.person.name, author.email.address))
self.assertEqual(t[15], u"%s <%s>" % (draft.shepherd.person.name, draft.shepherd.address))
self.assertEqual(t[16], u"%s <%s>" % (draft.ad.plain_ascii(), draft.ad.email_address()))

View file

@ -63,15 +63,15 @@ def get_document_emails(ipr):
messages = []
for rel in ipr.iprdocrel_set.all():
doc = rel.document.document
authors = doc.authors.all()
if is_draft(doc):
doc_info = 'Internet-Draft entitled "{}" ({})'.format(doc.title,doc.name)
else:
doc_info = 'RFC entitled "{}" (RFC{})'.format(doc.title,get_rfc_num(doc))
addrs = gather_address_lists('ipr_posted_on_doc',doc=doc).as_strings(compact=False)
author_names = ', '.join([a.person.name for a in authors])
author_names = ', '.join(a.person.name for a in doc.documentauthor_set.select_related("person"))
context = dict(
doc_info=doc_info,

View file

@ -199,7 +199,7 @@ class Recipient(models.Model):
submission = kwargs['submission']
doc=submission.existing_document()
if doc:
old_authors = [i.author.formatted_email() for i in doc.documentauthor_set.all() if not i.author.invalid_address()]
old_authors = [author.formatted_email() for author in doc.documentauthor_set.all() if author.email]
new_authors = [u'"%s" <%s>' % (author["name"], author["email"]) for author in submission.authors_parsed() if author["email"]]
addrs.extend(old_authors)
if doc.group and set(old_authors)!=set(new_authors):

View file

@ -12,9 +12,6 @@ def insert_initial_formal_language_names(apps, schema_editor):
FormalLanguageName.objects.get_or_create(slug="json", name="JSON", desc="Javascript Object Notation", order=5)
FormalLanguageName.objects.get_or_create(slug="xml", name="XML", desc="Extensible Markup Language", order=6)
def noop(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
@ -22,5 +19,5 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RunPython(insert_initial_formal_language_names, noop)
migrations.RunPython(insert_initial_formal_language_names, migrations.RunPython.noop)
]

View file

@ -14,7 +14,8 @@ from ietf.name.models import (TimeSlotTypeName, GroupStateName, DocTagName, Inte
ConstraintName, MeetingTypeName, DocRelationshipName, RoomResourceName, IprLicenseTypeName,
LiaisonStatementTagName, FeedbackTypeName, LiaisonStatementState, StreamName,
BallotPositionName, DBTemplateTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewTypeName, ReviewResultName)
ReviewRequestStateName, ReviewTypeName, ReviewResultName,
FormalLanguageName)
class TimeSlotTypeNameResource(ModelResource):
@ -456,3 +457,20 @@ class ReviewResultNameResource(ModelResource):
}
api.name.register(ReviewResultNameResource())
class FormalLanguageNameResource(ModelResource):
class Meta:
queryset = FormalLanguageName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'formallanguagename'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(FormalLanguageNameResource())

View file

@ -125,18 +125,18 @@ class PersonInfo(models.Model):
def has_drafts(self):
from ietf.doc.models import Document
return Document.objects.filter(authors__person=self, type='draft').exists()
return Document.objects.filter(documentauthor__person=self, type='draft').exists()
def rfcs(self):
from ietf.doc.models import Document
rfcs = list(Document.objects.filter(authors__person=self, type='draft', states__slug='rfc'))
rfcs = list(Document.objects.filter(documentauthor__person=self, type='draft', states__slug='rfc'))
rfcs.sort(key=lambda d: d.canonical_name() )
return rfcs
def active_drafts(self):
from ietf.doc.models import Document
return Document.objects.filter(authors__person=self, type='draft', states__slug='active').order_by('-time')
return Document.objects.filter(documentauthor__person=self, type='draft', states__slug='active').order_by('-time')
def expired_drafts(self):
from ietf.doc.models import Document
return Document.objects.filter(authors__person=self, type='draft', states__slug__in=['repl', 'expired', 'auth-rm', 'ietf-rm']).order_by('-time')
return Document.objects.filter(documentauthor__person=self, type='draft', states__slug__in=['repl', 'expired', 'auth-rm', 'ietf-rm']).order_by('-time')
class Meta:
abstract = True
@ -231,15 +231,11 @@ class Email(models.Model):
else:
return self.address
def invalid_address(self):
# we have some legacy authors with unknown email addresses
return self.address.startswith("unknown-email") and "@" not in self.address
def email_address(self):
"""Get valid, current email address; in practise, for active,
non-invalid addresses it is just the address field. In other
cases, we default to person's email address."""
if self.invalid_address() or not self.active:
if not self.active:
if self.person:
return self.person.email_address()
return

View file

@ -752,7 +752,7 @@ def make_assignment_choices(email_queryset, review_req):
connections[r.person_id] = "is group {}".format(r.name)
if doc.shepherd:
connections[doc.shepherd.person_id] = "is shepherd of document"
for author in DocumentAuthor.objects.filter(document=doc, author__person__in=possible_person_ids).values_list("author__person", flat=True):
for author in DocumentAuthor.objects.filter(document=doc, person__in=possible_person_ids).values_list("person", flat=True):
connections[author] = "is author of document"
# unavailable periods

View file

@ -48,12 +48,12 @@ def get_authors(draft):
Takes a draft object and returns a list of authors suitable for a tombstone document
"""
authors = []
for a in draft.authors.all():
for a in draft.documentauthor_set.all():
initial = ''
prefix, first, middle, last, suffix = a.person.name_parts()
if first:
initial = first + '. '
entry = '%s%s <%s>' % (initial,last,a.address)
entry = '%s%s <%s>' % (initial,last,a.email.address)
authors.append(entry)
return authors
@ -64,10 +64,10 @@ def get_abbr_authors(draft):
"""
initial = ''
result = ''
authors = DocumentAuthor.objects.filter(document=draft)
authors = DocumentAuthor.objects.filter(document=draft).order_by("order")
if authors:
prefix, first, middle, last, suffix = authors[0].author.person.name_parts()
prefix, first, middle, last, suffix = authors[0].person.name_parts()
if first:
initial = first[0] + '. '
result = '%s%s' % (initial,last)
@ -140,9 +140,9 @@ def get_fullcc_list(draft):
"""
emails = {}
# get authors
for author in draft.authors.all():
if author.address not in emails:
emails[author.address] = '"%s"' % (author.person.name)
for author in draft.documentauthor_set.all():
if author.email and author.email.address not in emails:
emails[author.email.address] = '"%s"' % (author.person.name)
if draft.group.acronym != 'none':
# add chairs

View file

@ -4,6 +4,8 @@ import os
from django import forms
from django_countries.fields import countries
from ietf.doc.models import Document, DocAlias, State
from ietf.name.models import IntendedStdLevelName, DocRelationshipName
from ietf.group.models import Group
@ -104,6 +106,8 @@ class AuthorForm(forms.Form):
'''
person = forms.CharField(max_length=50,widget=forms.TextInput(attrs={'class':'name-autocomplete'}),help_text="To see a list of people type the first name, or last name, or both.")
email = forms.CharField(widget=forms.Select(),help_text="Select an email.")
affiliation = forms.CharField(max_length=100, required=False, help_text="Affiliation")
country = forms.ChoiceField(choices=[('', "(Not specified)")] + list(countries), required=False, help_text="Country")
# check for id within parenthesis to ensure name was selected from the list
def clean_person(self):

View file

@ -541,7 +541,7 @@ def approvals(request):
@role_required('Secretariat')
def author_delete(request, id, oid):
'''
This view deletes the specified author(email) from the draft
This view deletes the specified author from the draft
'''
DocumentAuthor.objects.get(id=oid).delete()
messages.success(request, 'The author was deleted successfully')
@ -574,14 +574,20 @@ def authors(request, id):
return redirect('drafts_view', id=id)
print form.is_valid(), form.errors
if form.is_valid():
author = form.cleaned_data['email']
person = form.cleaned_data['person']
email = form.cleaned_data['email']
affiliation = form.cleaned_data.get('affiliation') or ""
country = form.cleaned_data.get('country') or ""
authors = draft.documentauthor_set.all()
if authors:
order = authors.aggregate(Max('order')).values()[0] + 1
else:
order = 1
DocumentAuthor.objects.create(document=draft,author=author,order=order)
DocumentAuthor.objects.create(document=draft, person=person, email=email, affiliation=affiliation, country=country, order=order)
messages.success(request, 'Author added successfully!')
return redirect('drafts_authors', id=id)

View file

@ -24,6 +24,8 @@
<tr>
<th>Name</th>
<th>Email</th>
<th>Affiliation</th>
<th>Country</th>
<th>Order</th>
<th>Action</th>
</tr>
@ -31,8 +33,10 @@
<tbody>
{% for author in draft.documentauthor_set.all %}
<tr class="{% cycle row1,row2 %}">
<td>{{ author.author.person }}</td>
<td>{{ author.author }}</td>
<td>{{ author.person }}</td>
<td>{{ author.email }}</td>
<td>{{ author.affiliation }}</td>
<td>{{ author.country.name }}</td>
<td>{{ author.order }}</td>
<td><a href="{% url "drafts_author_delete" id=draft.pk oid=author.id %}">Delete</a></td>
</tr>
@ -49,6 +53,10 @@
<tr>
<td>{{ form.person.errors }}{{ form.person }}{% if form.person.help_text %}<br>{{ form.person.help_text }}{% endif %}</td>
<td>{{ form.email.errors }}{{ form.email }}{% if form.email.help_text %}<br>{{ form.email.help_text }}{% endif %}</td>
</tr>
<tr>
<td>{{ form.affiliation.errors }}{{ form.affiliation }}{% if form.affiliation.help_text %}<br>{{ form.affiliation.help_text }}{% endif %}</td>
<td>{{ form.country.errors }}{{ form.country }}{% if form.country.help_text %}<br>{{ form.country.help_text }}{% endif %}</td>
<td><input type="submit" name="submit" value="Add" /></td>
</tr>
</table>

View file

@ -52,7 +52,7 @@
<div class="inline-related">
<h2>Author(s)</h2>
<table>
{% for author in draft.authors.all %}
{% for author in draft.documentauthor_set.all %}
<tr><td><a href="{% url "ietf.secr.rolodex.views.view" id=author.person.pk %}">{{ author.person.name }}</a></td></tr>
{% endfor %}
</table>

View file

@ -73,7 +73,7 @@
<h2>Author(s)</h2>
<table class="full-width">
{% for author in draft.documentauthor_set.all %}
<tr><td><a href="{% url "rolodex_view" id=author.author.person.id %}">{{ author.author.person.name }}</a></td></tr>
<tr><td><a href="{% url "rolodex_view" id=author.person.id %}">{{ author.person.name }}</a></td></tr>
{% endfor %}
</table>
</div> <!-- inline-related -->

View file

@ -293,6 +293,7 @@ INSTALLED_APPS = (
'tastypie',
'widget_tweaks',
'django_markup',
'django_countries',
# IETF apps
'ietf.api',
'ietf.community',

View file

@ -3,7 +3,6 @@ import itertools
import json
import calendar
import os
import re
from collections import defaultdict
from django.shortcuts import render
@ -141,7 +140,7 @@ def document_stats(request, stats_type=None, document_type=None):
bins = defaultdict(list)
for name, author_count in generate_canonical_names(docalias_qs.values_list("name").annotate(Count("document__authors"))):
for name, author_count in generate_canonical_names(docalias_qs.values_list("name").annotate(Count("document__documentauthor"))):
bins[author_count].append(name)
series_data = []

View file

@ -65,7 +65,8 @@ class Submission(models.Model):
if line:
parsed = parse_email_line(line)
if not parsed["email"]:
parsed["email"] = ensure_person_email_info_exists(**parsed).address
person, email = ensure_person_email_info_exists(**parsed)
parsed["email"] = email.address
res.append(parsed)
self._cached_authors_parsed = res
return self._cached_authors_parsed

View file

@ -19,7 +19,7 @@ from ietf.group.utils import setup_default_community_list_for_group
from ietf.meeting.models import Meeting
from ietf.message.models import Message
from ietf.name.models import FormalLanguageName
from ietf.person.models import Person, Email
from ietf.person.models import Person
from ietf.person.factories import UserFactory, PersonFactory
from ietf.submit.models import Submission, Preapproval
from ietf.submit.mail import add_submission_email, process_response_email
@ -249,9 +249,10 @@ class SubmitTests(TestCase):
self.assertEqual(draft.stream_id, "ietf")
self.assertTrue(draft.expires >= datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE - 1))
self.assertEqual(draft.get_state("draft-stream-%s" % draft.stream_id).slug, "wg-doc")
self.assertEqual(draft.authors.count(), 1)
self.assertEqual(draft.authors.all()[0].get_name(), "Author Name")
self.assertEqual(draft.authors.all()[0].address, "author@example.com")
authors = draft.documentauthor_set.all()
self.assertEqual(len(authors), 1)
self.assertEqual(authors[0].person.plain_name(), "Author Name")
self.assertEqual(authors[0].email.address, "author@example.com")
self.assertEqual(set(draft.formal_languages.all()), set(FormalLanguageName.objects.filter(slug="json")))
self.assertEqual(draft.relations_that_doc("replaces").count(), 1)
self.assertTrue(draft.relations_that_doc("replaces").first().target, replaced_alias)
@ -290,12 +291,12 @@ class SubmitTests(TestCase):
draft.save_with_history([DocEvent.objects.create(doc=draft, type="added_comment", by=Person.objects.get(user__username="secretary"), desc="Test")])
if not change_authors:
draft.documentauthor_set.all().delete()
ensure_person_email_info_exists('Author Name','author@example.com')
draft.documentauthor_set.create(author=Email.objects.get(address='author@example.com'))
author_person, author_email = ensure_person_email_info_exists('Author Name','author@example.com')
draft.documentauthor_set.create(person=author_person, email=author_email)
else:
# Make it such that one of the previous authors has an invalid email address
bogus_email = ensure_person_email_info_exists('Bogus Person',None)
DocumentAuthor.objects.create(document=draft,author=bogus_email,order=draft.documentauthor_set.latest('order').order+1)
bogus_person, bogus_email = ensure_person_email_info_exists('Bogus Person',None)
DocumentAuthor.objects.create(document=draft, person=bogus_person, email=bogus_email, order=draft.documentauthor_set.latest('order').order+1)
prev_author = draft.documentauthor_set.all()[0]
@ -342,7 +343,7 @@ class SubmitTests(TestCase):
confirm_email = outbox[-1]
self.assertTrue("Confirm submission" in confirm_email["Subject"])
self.assertTrue(name in confirm_email["Subject"])
self.assertTrue(prev_author.author.address in confirm_email["To"])
self.assertTrue(prev_author.email.address in confirm_email["To"])
if change_authors:
self.assertTrue("author@example.com" not in confirm_email["To"])
self.assertTrue("submitter@example.com" not in confirm_email["To"])
@ -423,9 +424,10 @@ class SubmitTests(TestCase):
self.assertEqual(draft.stream_id, "ietf")
self.assertEqual(draft.get_state_slug("draft-stream-%s" % draft.stream_id), "wg-doc")
self.assertEqual(draft.get_state_slug("draft-iana-review"), "changed")
self.assertEqual(draft.authors.count(), 1)
self.assertEqual(draft.authors.all()[0].get_name(), "Author Name")
self.assertEqual(draft.authors.all()[0].address, "author@example.com")
authors = draft.documentauthor_set.all()
self.assertEqual(len(authors), 1)
self.assertEqual(authors[0].person.plain_name(), "Author Name")
self.assertEqual(authors[0].email.address, "author@example.com")
self.assertEqual(len(outbox), mailbox_before + 3)
self.assertTrue((u"I-D Action: %s" % name) in outbox[-3]["Subject"])
self.assertTrue((u"I-D Action: %s" % name) in draft.message_set.order_by("-time")[0].subject)

View file

@ -125,7 +125,7 @@ def docevent_from_submission(request, submission, desc, who=None):
else:
submitter_parsed = submission.submitter_parsed()
if submitter_parsed["name"] and submitter_parsed["email"]:
by = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"]).person
by, _ = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"])
else:
by = system
@ -179,7 +179,7 @@ def post_submission(request, submission, approvedDesc):
system = Person.objects.get(name="(System)")
submitter_parsed = submission.submitter_parsed()
if submitter_parsed["name"] and submitter_parsed["email"]:
submitter = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"]).person
submitter, _ = ensure_person_email_info_exists(submitter_parsed["name"], submitter_parsed["email"])
submitter_info = u'%s <%s>' % (submitter_parsed["name"], submitter_parsed["email"])
else:
submitter = system
@ -341,10 +341,9 @@ def update_replaces_from_submission(request, submission, draft):
if rdoc == draft:
continue
# TODO - I think the .exists() is in the wrong place below....
if (is_secretariat
or (draft.group in is_chair_of and (rdoc.group.type_id == "individ" or rdoc.group in is_chair_of))
or (submitter_email and rdoc.authors.filter(address__iexact=submitter_email)).exists()):
or (submitter_email and rdoc.documentauthor_set.filter(email__address__iexact=submitter_email).exists())):
approved.append(r)
else:
if r not in existing_suggested:
@ -424,23 +423,24 @@ def ensure_person_email_info_exists(name, email):
email.person = person
email.save()
return email
return person, email
def update_authors(draft, submission):
authors = []
persons = []
for order, author in enumerate(submission.authors_parsed()):
email = ensure_person_email_info_exists(author["name"], author["email"])
person, email = ensure_person_email_info_exists(author["name"], author["email"])
a = DocumentAuthor.objects.filter(document=draft, author=email).first()
a = DocumentAuthor.objects.filter(document=draft, person=person).first()
if not a:
a = DocumentAuthor(document=draft, author=email)
a = DocumentAuthor(document=draft, person=person)
a.email = email
a.order = order
a.save()
authors.append(email)
persons.append(person)
draft.documentauthor_set.exclude(author__in=authors).delete()
draft.documentauthor_set.exclude(person__in=persons).delete()
def cancel_submission(submission):
submission.state = DraftSubmissionStateName.objects.get(slug="cancel")

View file

@ -259,7 +259,7 @@ def submission_status(request, submission_id, access_token=None):
group_authors_changed = False
doc = submission.existing_document()
if doc and doc.group:
old_authors = [ i.author.person for i in doc.documentauthor_set.all() ]
old_authors = [ author.person for author in doc.documentauthor_set.all() ]
new_authors = [ get_person_from_name_email(**p) for p in submission.authors_parsed() ]
group_authors_changed = set(old_authors)!=set(new_authors)

View file

@ -2,11 +2,11 @@
<reference anchor='{{doc_bibtype}}.{{doc.name|slice:"6:"}}'>
<front>
<title>{{doc.title}}</title>
{% for entry in doc.authors.all %}{% with entry.address as email %}{% with entry.person as author %}
<author initials='{{author.initials}}' surname='{{author.last_name}}' fullname='{{author.name}}'>
<organization>{{author.affiliation}}</organization>
{% for author in doc.documentauthor_set.all %}
<author initials='{{ author.person.initials }}' surname='{{ author.person.last_name }}' fullname='{{ author.person.name }}'>
<organization>{{ author.affiliation }}</organization>
</author>
{% endwith %}{% endwith %}{% endfor %}
{% endfor %}
<date month='{{doc.time|date:"F"}}' day='{{doc.time.day}}' year='{{doc.time.year}}' />
<abstract><t>{{doc.abstract}}</t></abstract>
</front>

View file

@ -24,7 +24,7 @@
publisher = {% templatetag openbrace %}Internet Engineering Task Force{% templatetag closebrace %},
note = {% templatetag openbrace %}Work in Progress{% templatetag closebrace %},
url = {% templatetag openbrace %}https://tools.ietf.org/html/{{doc.name}}-{{doc.rev}}{% templatetag closebrace %},{% endif %}
author = {% templatetag openbrace %}{% for entry in doc.authors.all %}{% with entry.person as author %}{{author.name}}{% endwith %}{% if not forloop.last %} and {% endif %}{% endfor %}{% templatetag closebrace %},
author = {% templatetag openbrace %}{% for author in doc.documentauthor_set.all %}{{ author.person.name}}{% if not forloop.last %} and {% endif %}{% endfor %}{% templatetag closebrace %},
title = {% templatetag openbrace %}{% templatetag openbrace %}{{doc.title}}{% templatetag closebrace %}{% templatetag closebrace %},
pagetotal = {{ doc.pages }},
year = {{ doc.pub_date.year }},

View file

@ -570,13 +570,13 @@
<h4>Authors</h4>
<p>
{% for author in doc.documentauthor_set.all %}
{% if not author.author.invalid_address %}
{% if author.email %}
<span class="fa fa-envelope-o"></span>
<a href="mailto:{{ author.author.address }}">
<a href="mailto:{{ author.email.address }}">
{% endif %}
{{ author.author.person }}
{% if not author.author.invalid_address %}
({{ author.author.address }})</a>
{{ author.person }}
{% if author.email %}
({{ author.email.address }})</a>
{% endif %}
{% if not forloop.last %}<br>{% endif %}
{% endfor %}

View file

@ -91,7 +91,7 @@ def make_immutable_base_data():
# one area
area = create_group(name="Far Future", acronym="farfut", type_id="area", parent=ietf)
create_person(area, "ad", name="Areað Irector", username="ad", email_address="aread@ietf.org")
create_person(area, "ad", name=u"Areað Irector", username="ad", email_address="aread@ietf.org")
# second area
opsarea = create_group(name="Operations", acronym="ops", type_id="area", parent=ietf)
@ -276,7 +276,8 @@ def make_test_data():
DocumentAuthor.objects.create(
document=draft,
author=Email.objects.get(address="aread@ietf.org"),
person=Person.objects.get(email__address="aread@ietf.org"),
email=Email.objects.get(address="aread@ietf.org"),
order=1
)

View file

@ -9,6 +9,7 @@ decorator>=3.4.0
defusedxml>=0.4.1 # for TastyPie when ussing xml; not a declared dependency
Django>=1.8.16,<1.9
django-bootstrap3>=5.1.1,<7.0.0 # django-bootstrap 7.0 requires django 1.8
django-countries>=4.0
django-formtools>=1.0 # instead of django.contrib.formtools in 1.8
django-markup>=1.1
django-tastypie>=0.13.1