From 9308948195d526abd09df525307711bf531360f4 Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Thu, 26 Jan 2017 17:10:08 +0000 Subject: [PATCH] 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 --- ietf/api/__init__.py | 4 +- ...bmission-confirmation-email-in-postfix-log | 2 +- ietf/bin/generate-draft-aliases | 2 +- .../community/migrations/0004_cleanup_data.py | 2 +- ietf/community/tests.py | 4 +- ietf/community/utils.py | 4 +- ietf/doc/admin.py | 10 +-- ietf/doc/factories.py | 4 +- .../doc/migrations/0020_auto_20170112_0753.py | 81 +++++++++++++++++++ .../0021_remove_fake_email_adresses.py | 50 ++++++++++++ ietf/doc/models.py | 45 +++++++---- ietf/doc/resources.py | 24 +++--- ietf/doc/tests.py | 4 +- ietf/doc/tests_draft.py | 21 +++-- ietf/doc/tests_review.py | 5 +- ietf/doc/views_doc.py | 13 ++- ietf/doc/views_search.py | 2 +- ietf/doc/views_stats.py | 2 +- ietf/idindex/index.py | 12 +-- ietf/idindex/tests.py | 2 +- ietf/ipr/views.py | 6 +- ietf/mailtrigger/models.py | 2 +- .../migrations/0018_add_formlang_names.py | 5 +- ietf/name/resources.py | 20 ++++- ietf/person/models.py | 14 ++-- ietf/review/utils.py | 2 +- ietf/secr/drafts/email.py | 14 ++-- ietf/secr/drafts/forms.py | 4 + ietf/secr/drafts/views.py | 12 ++- ietf/secr/templates/drafts/authors.html | 12 ++- ietf/secr/templates/drafts/makerfc.html | 2 +- ietf/secr/templates/drafts/view.html | 2 +- ietf/settings.py | 1 + ietf/stats/views.py | 3 +- ietf/submit/models.py | 3 +- ietf/submit/tests.py | 26 +++--- ietf/submit/utils.py | 22 ++--- ietf/submit/views.py | 2 +- ietf/templates/doc/bibxml.xml | 8 +- ietf/templates/doc/document_bibtex.bib | 2 +- ietf/templates/doc/document_draft.html | 10 +-- ietf/utils/test_data.py | 5 +- requirements.txt | 1 + 43 files changed, 329 insertions(+), 142 deletions(-) create mode 100644 ietf/doc/migrations/0021_remove_fake_email_adresses.py diff --git a/ietf/api/__init__.py b/ietf/api/__init__.py index 9a6cdf868..0d50d103d 100644 --- a/ietf/api/__init__.py +++ b/ietf/api/__init__.py @@ -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) diff --git a/ietf/bin/find-submission-confirmation-email-in-postfix-log b/ietf/bin/find-submission-confirmation-email-in-postfix-log index 3df3c78be..69bdcc331 100755 --- a/ietf/bin/find-submission-confirmation-email-in-postfix-log +++ b/ietf/bin/find-submission-confirmation-email-in-postfix-log @@ -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)), diff --git a/ietf/bin/generate-draft-aliases b/ietf/bin/generate-draft-aliases index fd3d409af..64b97a846 100755 --- a/ietf/bin/generate-draft-aliases +++ b/ietf/bin/generate-draft-aliases @@ -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 diff --git a/ietf/community/migrations/0004_cleanup_data.py b/ietf/community/migrations/0004_cleanup_data.py index 6ee14c951..ab1c24f4e 100644 --- a/ietf/community/migrations/0004_cleanup_data.py +++ b/ietf/community/migrations/0004_cleanup_data.py @@ -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] diff --git a/ietf/community/tests.py b/ietf/community/tests.py index b712ee821..01d1f1ece 100644 --- a/ietf/community/tests.py +++ b/ietf/community/tests.py @@ -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) diff --git a/ietf/community/utils.py b/ietf/community/utils.py index ee8c87042..37fce87ac 100644 --- a/ietf/community/utils.py +++ b/ietf/community/utils.py @@ -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: diff --git a/ietf/doc/admin.py b/ietf/doc/admin.py index a9e81a749..c5db20e37 100644 --- a/ietf/doc/admin.py +++ b/ietf/doc/admin.py @@ -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) diff --git a/ietf/doc/factories.py b/ietf/doc/factories.py index 9e29c1a19..919c9a520 100644 --- a/ietf/doc/factories.py +++ b/ietf/doc/factories.py @@ -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 diff --git a/ietf/doc/migrations/0020_auto_20170112_0753.py b/ietf/doc/migrations/0020_auto_20170112_0753.py index 9ca0cafa6..9404b8aab 100644 --- a/ietf/doc/migrations/0020_auto_20170112_0753.py +++ b/ietf/doc/migrations/0020_auto_20170112_0753.py @@ -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'), + ), ] diff --git a/ietf/doc/migrations/0021_remove_fake_email_adresses.py b/ietf/doc/migrations/0021_remove_fake_email_adresses.py new file mode 100644 index 000000000..d317ed03d --- /dev/null +++ b/ietf/doc/migrations/0021_remove_fake_email_adresses.py @@ -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), + ] diff --git a/ietf/doc/models.py b/ietf/doc/models.py index e4c776004..3592b4578 100644 --- a/ietf/doc/models.py +++ b/ietf/doc/models.py @@ -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) diff --git a/ietf/doc/resources.py b/ietf/doc/resources.py index 945700daa..dd97516e0 100644 --- a/ietf/doc/resources.py +++ b/ietf/doc/resources.py @@ -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()) diff --git a/ietf/doc/tests.py b/ietf/doc/tests.py index e4a171813..d025b5de9 100644 --- a/ietf/doc/tests.py +++ b/ietf/doc/tests.py @@ -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)) diff --git a/ietf/doc/tests_draft.py b/ietf/doc/tests_draft.py index 1f024b0ed..3c2c6a32a 100644 --- a/ietf/doc/tests_draft.py +++ b/ietf/doc/tests_draft.py @@ -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")) diff --git a/ietf/doc/tests_review.py b/ietf/doc/tests_review.py index f477f3323..58a9ef010 100644 --- a/ietf/doc/tests_review.py +++ b/ietf/doc/tests_review.py @@ -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")]) diff --git a/ietf/doc/views_doc.py b/ietf/doc/views_doc.py index ab950d406..7f81d2802 100644 --- a/ietf/doc/views_doc.py +++ b/ietf/doc/views_doc.py @@ -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 diff --git a/ietf/doc/views_search.py b/ietf/doc/views_search.py index e1ae950e8..0b22a2b72 100644 --- a/ietf/doc/views_search.py +++ b/ietf/doc/views_search.py @@ -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": diff --git a/ietf/doc/views_stats.py b/ietf/doc/views_stats.py index 6fa1a094b..9d1d02a3e 100644 --- a/ietf/doc/views_stats.py +++ b/ietf/doc/views_stats.py @@ -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) diff --git a/ietf/idindex/index.py b/ietf/idindex/index.py index ec4e111ef..d02ceee5f 100644 --- a/ietf/idindex/index.py +++ b/ietf/idindex/index.py @@ -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(): diff --git a/ietf/idindex/tests.py b/ietf/idindex/tests.py index 5f7ff9d9c..4d7aaa699 100644 --- a/ietf/idindex/tests.py +++ b/ietf/idindex/tests.py @@ -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())) diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 3ec7a0a3d..61da03c23 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -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, diff --git a/ietf/mailtrigger/models.py b/ietf/mailtrigger/models.py index 526b5f5dd..40392e0e4 100644 --- a/ietf/mailtrigger/models.py +++ b/ietf/mailtrigger/models.py @@ -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): diff --git a/ietf/name/migrations/0018_add_formlang_names.py b/ietf/name/migrations/0018_add_formlang_names.py index 542cb6cf5..a0dd3ff19 100644 --- a/ietf/name/migrations/0018_add_formlang_names.py +++ b/ietf/name/migrations/0018_add_formlang_names.py @@ -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) ] diff --git a/ietf/name/resources.py b/ietf/name/resources.py index 15bc6d378..f6a74387d 100644 --- a/ietf/name/resources.py +++ b/ietf/name/resources.py @@ -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()) + diff --git a/ietf/person/models.py b/ietf/person/models.py index 9713ccb25..61fa6b2c0 100644 --- a/ietf/person/models.py +++ b/ietf/person/models.py @@ -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 diff --git a/ietf/review/utils.py b/ietf/review/utils.py index ae7124fa8..f1270ef7e 100644 --- a/ietf/review/utils.py +++ b/ietf/review/utils.py @@ -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 diff --git a/ietf/secr/drafts/email.py b/ietf/secr/drafts/email.py index cd8889efc..78d83d474 100644 --- a/ietf/secr/drafts/email.py +++ b/ietf/secr/drafts/email.py @@ -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 diff --git a/ietf/secr/drafts/forms.py b/ietf/secr/drafts/forms.py index 228f98bfb..956c87109 100644 --- a/ietf/secr/drafts/forms.py +++ b/ietf/secr/drafts/forms.py @@ -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): diff --git a/ietf/secr/drafts/views.py b/ietf/secr/drafts/views.py index e14c408a4..eb77abd64 100644 --- a/ietf/secr/drafts/views.py +++ b/ietf/secr/drafts/views.py @@ -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) diff --git a/ietf/secr/templates/drafts/authors.html b/ietf/secr/templates/drafts/authors.html index 2620db1fb..33e818d82 100644 --- a/ietf/secr/templates/drafts/authors.html +++ b/ietf/secr/templates/drafts/authors.html @@ -24,6 +24,8 @@ Name Email + Affiliation + Country Order Action @@ -31,8 +33,10 @@ {% for author in draft.documentauthor_set.all %} - {{ author.author.person }} - {{ author.author }} + {{ author.person }} + {{ author.email }} + {{ author.affiliation }} + {{ author.country.name }} {{ author.order }} Delete @@ -49,6 +53,10 @@ {{ form.person.errors }}{{ form.person }}{% if form.person.help_text %}
{{ form.person.help_text }}{% endif %} {{ form.email.errors }}{{ form.email }}{% if form.email.help_text %}
{{ form.email.help_text }}{% endif %} + + + {{ form.affiliation.errors }}{{ form.affiliation }}{% if form.affiliation.help_text %}
{{ form.affiliation.help_text }}{% endif %} + {{ form.country.errors }}{{ form.country }}{% if form.country.help_text %}
{{ form.country.help_text }}{% endif %} diff --git a/ietf/secr/templates/drafts/makerfc.html b/ietf/secr/templates/drafts/makerfc.html index 962f76093..f5945d558 100644 --- a/ietf/secr/templates/drafts/makerfc.html +++ b/ietf/secr/templates/drafts/makerfc.html @@ -52,7 +52,7 @@ diff --git a/ietf/settings.py b/ietf/settings.py index 38ab48e83..70ba8dda5 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -293,6 +293,7 @@ INSTALLED_APPS = ( 'tastypie', 'widget_tweaks', 'django_markup', + 'django_countries', # IETF apps 'ietf.api', 'ietf.community', diff --git a/ietf/stats/views.py b/ietf/stats/views.py index a580bb277..047b90c66 100644 --- a/ietf/stats/views.py +++ b/ietf/stats/views.py @@ -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 = [] diff --git a/ietf/submit/models.py b/ietf/submit/models.py index 1e9e29e59..260494360 100644 --- a/ietf/submit/models.py +++ b/ietf/submit/models.py @@ -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 diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py index 6fb3859f9..62ab8c3f2 100644 --- a/ietf/submit/tests.py +++ b/ietf/submit/tests.py @@ -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) diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py index 957b5eff2..ad5f02c6a 100644 --- a/ietf/submit/utils.py +++ b/ietf/submit/utils.py @@ -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") diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 95b9c2ae8..05a1aa379 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -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) diff --git a/ietf/templates/doc/bibxml.xml b/ietf/templates/doc/bibxml.xml index 9813c3a00..fdb57c132 100644 --- a/ietf/templates/doc/bibxml.xml +++ b/ietf/templates/doc/bibxml.xml @@ -2,11 +2,11 @@ {{doc.title}} - {% for entry in doc.authors.all %}{% with entry.address as email %}{% with entry.person as author %} - - {{author.affiliation}} + {% for author in doc.documentauthor_set.all %} + + {{ author.affiliation }} - {% endwith %}{% endwith %}{% endfor %} + {% endfor %} {{doc.abstract}} diff --git a/ietf/templates/doc/document_bibtex.bib b/ietf/templates/doc/document_bibtex.bib index 489abf3ec..0e10add29 100644 --- a/ietf/templates/doc/document_bibtex.bib +++ b/ietf/templates/doc/document_bibtex.bib @@ -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 }}, diff --git a/ietf/templates/doc/document_draft.html b/ietf/templates/doc/document_draft.html index bed55b798..7ebd65f90 100644 --- a/ietf/templates/doc/document_draft.html +++ b/ietf/templates/doc/document_draft.html @@ -570,13 +570,13 @@

Authors

{% for author in doc.documentauthor_set.all %} - {% if not author.author.invalid_address %} + {% if author.email %} - + {% endif %} - {{ author.author.person }} - {% if not author.author.invalid_address %} - ({{ author.author.address }}) + {{ author.person }} + {% if author.email %} + ({{ author.email.address }}) {% endif %} {% if not forloop.last %}
{% endif %} {% endfor %} diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py index 938fcbc5c..80229fdb7 100644 --- a/ietf/utils/test_data.py +++ b/ietf/utils/test_data.py @@ -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 ) diff --git a/requirements.txt b/requirements.txt index 16aba8935..d5eb29170 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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