Merged in ^/branch/iola/author-stats-r13145 from olau@iola.com, with additional features from ^/personal/henrik/6.52.1-authorstats.

- Legacy-Id: 13550
This commit is contained in:
Henrik Levkowetz 2017-06-06 18:36:59 +00:00
commit 023a32715d
110 changed files with 7672 additions and 423 deletions

View file

@ -34,3 +34,7 @@ $DTDIR/ietf/bin/expire-last-calls
# Enable when removed from /a/www/ietf-datatracker/scripts/Cron-runner:
$DTDIR/ietf/bin/rfc-editor-index-updates -d 1969-01-01
# Fetch meeting attendance data from ietf.org/registration/attendees
$DTDIR/ietf/manage.py fetch_meeting_attendance --latest

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

@ -35,7 +35,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)
@ -117,7 +117,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

@ -90,7 +90,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":
@ -123,7 +123,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

@ -26,7 +26,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):
@ -100,7 +100,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
@ -123,7 +123,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()
@ -175,8 +175,8 @@ class BallotPositionDocEventAdmin(DocEventAdmin):
admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin)
class DocumentAuthorAdmin(admin.ModelAdmin):
list_display = ['id', 'document', 'author', 'order']
search_fields = [ 'document__name', 'author__address', ]
raw_id_fields = ['document', 'author', ]
list_display = ['id', 'document', 'person', 'email', 'affiliation', 'country', 'order']
search_fields = ['document__docalias__name', 'person__name', 'email__address', 'affiliation', 'country']
raw_id_fields = ["document", "person", "email"]
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

@ -0,0 +1,115 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('name', '0020_formallanguagename'),
('doc', '0029_update_rfc_authors'),
]
operations = [
migrations.AddField(
model_name='dochistory',
name='words',
field=models.IntegerField(null=True, blank=True),
),
migrations.AddField(
model_name='document',
name='words',
field=models.IntegerField(null=True, blank=True),
),
migrations.AddField(
model_name='dochistory',
name='formal_languages',
field=models.ManyToManyField(help_text=b'Formal languages used in document', to='name.FormalLanguageName', blank=True),
),
migrations.AddField(
model_name='document',
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=models.CharField(blank=True, help_text=b'Country used by author for submission', max_length=255),
),
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=models.CharField(blank=True, help_text=b'Country used by author for submission', max_length=255),
),
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,58 @@
# -*- 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")
DocumentAuthor = apps.get_model("doc", "DocumentAuthor")
DocHistoryAuthor = apps.get_model("doc", "DocHistoryAuthor")
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()
e = Email.objects.filter(address="unknown-email-Greg-<gregimirsky@gmail.com>>").first()
if e:
# current email
new_e = Email.objects.get(address="gregimirsky@gmail.com")
DocumentAuthor.objects.filter(email=e).update(email=new_e)
DocHistoryAuthor.objects.filter(email=e).update(email=new_e)
e.delete()
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()
assert not Email.objects.filter(address__startswith="unknown-email-")
class Migration(migrations.Migration):
dependencies = [
('doc', '0030_author_revamp_and_extra_attributes'),
('person', '0014_auto_20160613_0751'),
('group', '0009_auto_20150930_0758'),
]
operations = [
migrations.RunPython(fix_invalid_emails, migrations.RunPython.noop),
]

View file

@ -18,12 +18,13 @@ import debug # pyflakes:ignore
from ietf.group.models import Group
from ietf.name.models import ( DocTypeName, DocTagName, StreamName, IntendedStdLevelName, StdLevelName,
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName )
DocRelationshipName, DocReminderTypeName, BallotPositionName, ReviewRequestStateName, FormalLanguageName )
from ietf.person.models import Email, Person
from ietf.utils import log
from ietf.utils.admin import admin_link
from ietf.utils.rfcmarkup import markup
from ietf.utils.validators import validate_no_control_chars
from ietf.utils.mail import formataddr
logger = logging.getLogger('django')
@ -82,6 +83,8 @@ class DocumentInfo(models.Model):
abstract = models.TextField(blank=True)
rev = models.CharField(verbose_name="revision", max_length=16, blank=True)
pages = models.IntegerField(blank=True, null=True)
words = models.IntegerField(blank=True, null=True)
formal_languages = models.ManyToManyField(FormalLanguageName, blank=True, help_text="Formal languages used in document")
order = models.IntegerField(default=1, blank=True) # This is probably obviated by SessionPresentaion.order
intended_std_level = models.ForeignKey(IntendedStdLevelName, verbose_name="Intended standardization level", blank=True, null=True)
std_level = models.ForeignKey(StdLevelName, verbose_name="Standardization level", blank=True, null=True)
@ -319,7 +322,7 @@ class DocumentInfo(models.Model):
return self._cached_is_rfc
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
@ -509,20 +512,33 @@ class RelatedDocument(models.Model):
return False
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 = models.CharField(max_length=255, 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 formataddr((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
@ -757,16 +773,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")
@ -775,7 +788,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())
@ -209,7 +211,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()
@ -239,7 +240,6 @@ class DocHistoryResource(ModelResource):
"doc": ALL_WITH_RELATIONS,
"states": ALL_WITH_RELATIONS,
"tags": ALL_WITH_RELATIONS,
"authors": ALL_WITH_RELATIONS,
}
api.doc.register(DocHistoryResource())
@ -412,10 +412,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()
@ -423,9 +424,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

@ -192,6 +192,11 @@ def rfcnospace(string):
else:
return string
@register.filter
def prettystdname(string):
from ietf.doc.utils import prettify_std_name
return prettify_std_name(unicode(string or ""))
@register.filter(name='rfcurl')
def rfclink(string):
"""

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))
@ -1237,7 +1237,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

@ -13,6 +13,11 @@ from ietf.utils.test_data import make_test_data, make_downref_test_data
from ietf.utils.test_utils import login_testing_unauthorized, unicontent
class Downref(TestCase):
def setUp(self):
make_test_data()
make_downref_test_data()
def test_downref_registry(self):
url = urlreverse('ietf.doc.views_downref.downref_registry')
@ -103,7 +108,3 @@ class Downref(TestCase):
self.assertTrue(RelatedDocument.objects.filter(source=draft, target=rfc, relationship_id='downref-approval'))
self.assertEqual(draft.docevent_set.count(), draft_de_count_before + 1)
self.assertEqual(rfc.document.docevent_set.count(), rfc_de_count_before + 1)
def setUp(self):
make_test_data()
make_downref_test_data()

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
)
@ -1348,7 +1351,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",
@ -1359,7 +1364,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",
@ -1370,7 +1377,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",
@ -1381,7 +1390,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

@ -260,10 +260,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, rev=doc.rev, type="changed_document", by=Person.objects.get(user__username="secretary"), desc="Test")])
@ -572,7 +569,7 @@ class ReviewTests(TestCase):
self.assertTrue(settings.MAILING_LIST_ARCHIVE_URL in review_req.review.external_url)
# Check that the review has the reviewer as author
self.assertEqual(review_req.reviewer, review_req.review.authors.first())
self.assertEqual(review_req.reviewer, review_req.review.documentauthor_set.first().email)
# Check that we have a copy of the outgoing message
msgid = outbox[0]["Message-ID"]

View file

@ -56,7 +56,6 @@ from ietf.group.models import Role
from ietf.group.utils import can_manage_group_type, 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
@ -994,11 +993,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

@ -567,7 +567,7 @@ def complete_review(request, name, request_id):
else:
author_email = request.user.person.email()
frm = request.user.person.formatted_email()
author, created = DocumentAuthor.objects.get_or_create(document=review, author=author_email)
author, created = DocumentAuthor.objects.get_or_create(document=review, email=author_email, person=request.user.person)
if need_to_email_review:
# email the review

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

@ -171,7 +171,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

@ -14,6 +14,7 @@ from ietf.group.colors import fg_group_colors, bg_group_colors
from ietf.name.models import GroupStateName, GroupTypeName, DocTagName, GroupMilestoneStateName, RoleName
from ietf.person.models import Email, Person
from ietf.utils.mail import formataddr
from ietf.utils.log import unreachable
class GroupInfo(models.Model):
@ -278,7 +279,12 @@ class Role(models.Model):
return u"%s is %s in %s" % (self.person.plain_name(), self.name.name, self.group.acronym or self.group.name)
def name_and_email(self):
"Returns name and email, e.g.: u'Ano Nymous <ano@nymous.org>' "
"""
Returns name and email, e.g.: u'Ano Nymous <ano@nymous.org>'
Is intended for display use, not in email context.
Use self.formatted_email() for that.
"""
unreachable()
if self.person:
return u"%s <%s>" % (self.person.plain_name(), self.email.address)
else:

View file

@ -117,15 +117,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_ascii_email().replace('"', ''))
for e in Email.objects.filter(shepherd_document_set__type="draft").select_related("person").distinct())
@ -236,12 +236,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

@ -97,7 +97,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

@ -64,15 +64,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

@ -6,7 +6,6 @@ from django.template import Template, Context
from email.utils import parseaddr
from ietf.utils.mail import formataddr
import debug # pyflakes:ignore
from ietf.group.models import Role
@ -178,10 +177,14 @@ class Recipient(models.Model):
return addrs
def gather_submission_authors(self, **kwargs):
"""
Returns a list of name and email, e.g.: [ 'Ano Nymous <ano@nymous.org>' ]
Is intended for display use, not in email context.
"""
addrs = []
if 'submission' in kwargs:
submission = kwargs['submission']
addrs.extend(["%s <%s>" % (author["name"], author["email"]) for author in submission.authors_parsed() if author["email"]])
addrs.extend(["%s <%s>" % (author["name"], author["email"]) for author in submission.authors if author.get("email")])
return addrs
def gather_submission_group_chairs(self, **kwargs):
@ -203,10 +206,14 @@ 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()]
new_authors = [ formataddr((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):
old_authors = [ author for author in doc.documentauthor_set.all() if author.email ]
addrs.extend([ author.formatted_email() for author in old_authors])
old_author_email_set = set(author.email.address for author in old_authors)
new_author_email_set = set(author["email"] for author in submission.authors if author.get("email"))
if doc.group and old_author_email_set != new_author_email_set:
if doc.group.type_id in ['wg','rg','ag']:
addrs.extend(Recipient.objects.get(slug='group_chairs').gather(**{'group':doc.group}))
elif doc.group.type_id in ['area']:
@ -216,8 +223,8 @@ class Recipient(models.Model):
if doc.stream_id and doc.stream_id not in ['ietf']:
addrs.extend(Recipient.objects.get(slug='stream_managers').gather(**{'streams':[doc.stream_id]}))
else:
addrs.extend([formataddr((author["name"], author["email"])) for author in submission.authors_parsed() if author["email"]])
if submission.submitter_parsed()["email"]:
addrs.extend([formataddr((author["name"], author["email"])) for author in submission.authors if author.get("email")])
if submission.submitter_parsed()["email"]:
addrs.append(submission.submitter)
return addrs

View file

@ -6,6 +6,8 @@ from django import forms
from django.db.models import Q
from django.forms import BaseInlineFormSet
import debug # pyflakes:ignore
from ietf.doc.models import Document, DocAlias, State, NewRevisionDocEvent
from ietf.doc.utils import get_document_content
from ietf.group.models import Group

View file

@ -297,7 +297,7 @@ class MeetingTests(TestCase):
r = self.client.get(urlreverse("ietf.meeting.views.materials", kwargs=dict(num=meeting.number)))
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
row = q('#content td div:contains("%s")' % str(session.group.acronym)).closest("tr")
row = q('#content #%s' % str(session.group.acronym)).closest("tr")
self.assertTrue(row.find('a:contains("Agenda")'))
self.assertTrue(row.find('a:contains("Minutes")'))
self.assertTrue(row.find('a:contains("Slideshow")'))
@ -987,6 +987,7 @@ class InterimTests(TestCase):
'session_set-INITIAL_FORMS':0}
r = self.client.post(urlreverse("ietf.meeting.views.interim_request"),data)
self.assertRedirects(r,urlreverse('ietf.meeting.views.upcoming'))
meeting = Meeting.objects.order_by('id').last()
self.assertEqual(meeting.type_id,'interim')

View file

@ -1,17 +1,21 @@
from django.contrib import admin
from ietf.name.models import (
BallotPositionName, ConstraintName, DBTemplateTypeName, DocRelationshipName,
BallotPositionName, ConstraintName, ContinentName, CountryName,
DBTemplateTypeName, DocRelationshipName,
DocReminderTypeName, DocTagName, DocTypeName, DraftSubmissionStateName,
FeedbackTypeName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
FeedbackTypeName, FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName, IprLicenseTypeName,
LiaisonStatementEventTypeName, LiaisonStatementPurposeName, LiaisonStatementState,
LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, )
from ietf.stats.models import CountryAlias
class NameAdmin(admin.ModelAdmin):
list_display = ["slug", "name", "desc", "used"]
search_fields = ["slug", "name"]
prepopulate_from = { "slug": ("name",) }
class DocRelationshipNameAdmin(NameAdmin):
@ -26,12 +30,24 @@ class GroupTypeNameAdmin(NameAdmin):
list_display = ["slug", "name", "verbose_name", "desc", "used"]
admin.site.register(GroupTypeName, GroupTypeNameAdmin)
class CountryAliasInline(admin.TabularInline):
model = CountryAlias
extra = 1
class CountryNameAdmin(NameAdmin):
list_display = ["slug", "name", "continent", "in_eu"]
list_filter = ["continent", "in_eu"]
inlines = [CountryAliasInline]
admin.site.register(CountryName, CountryNameAdmin)
admin.site.register(BallotPositionName, NameAdmin)
admin.site.register(ConstraintName, NameAdmin)
admin.site.register(ContinentName, NameAdmin)
admin.site.register(DBTemplateTypeName, NameAdmin)
admin.site.register(DocReminderTypeName, NameAdmin)
admin.site.register(DocTagName, NameAdmin)
admin.site.register(DraftSubmissionStateName, NameAdmin)
admin.site.register(FormalLanguageName, NameAdmin)
admin.site.register(FeedbackTypeName, NameAdmin)
admin.site.register(GroupMilestoneStateName, NameAdmin)
admin.site.register(GroupStateName, NameAdmin)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('name', '0019_add_docrelationshipname_downref_approval'),
]
operations = [
migrations.CreateModel(
name='FormalLanguageName',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order', 'name'],
'abstract': False,
},
),
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def insert_initial_formal_language_names(apps, schema_editor):
FormalLanguageName = apps.get_model("name", "FormalLanguageName")
FormalLanguageName.objects.get_or_create(slug="abnf", name="ABNF", desc="Augmented Backus-Naur Form", order=1)
FormalLanguageName.objects.get_or_create(slug="asn1", name="ASN.1", desc="Abstract Syntax Notation One", order=2)
FormalLanguageName.objects.get_or_create(slug="cbor", name="CBOR", desc="Concise Binary Object Representation", order=3)
FormalLanguageName.objects.get_or_create(slug="ccode", name="C Code", desc="Code in the C Programming Language", order=4)
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)
class Migration(migrations.Migration):
dependencies = [
('name', '0020_formallanguagename'),
]
operations = [
migrations.RunPython(insert_initial_formal_language_names, migrations.RunPython.noop)
]

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('name', '0021_add_formlang_names'),
]
operations = [
migrations.CreateModel(
name='ContinentName',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
],
options={
'ordering': ['order', 'name'],
'abstract': False,
},
),
migrations.CreateModel(
name='CountryName',
fields=[
('slug', models.CharField(max_length=32, serialize=False, primary_key=True)),
('name', models.CharField(max_length=255)),
('desc', models.TextField(blank=True)),
('used', models.BooleanField(default=True)),
('order', models.IntegerField(default=0)),
('in_eu', models.BooleanField(default=False, verbose_name='In EU')),
('continent', models.ForeignKey(to='name.ContinentName')),
],
options={
'ordering': ['order', 'name'],
'abstract': False,
},
),
]

View file

@ -0,0 +1,275 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def insert_initial_country_continent_names(apps, schema_editor):
ContinentName = apps.get_model("name", "ContinentName")
africa, _ = ContinentName.objects.get_or_create(slug="africa", name="Africa")
antarctica, _ = ContinentName.objects.get_or_create(slug="antarctica", name="Antarctica")
asia, _ = ContinentName.objects.get_or_create(slug="asia", name="Asia")
europe, _ = ContinentName.objects.get_or_create(slug="europe", name="Europe")
north_america, _ = ContinentName.objects.get_or_create(slug="north-america", name="North America")
oceania, _ = ContinentName.objects.get_or_create(slug="oceania", name="Oceania")
south_america, _ = ContinentName.objects.get_or_create(slug="south-america", name="South America")
CountryName = apps.get_model("name", "CountryName")
CountryName.objects.get_or_create(slug="AD", name=u"Andorra", continent=europe)
CountryName.objects.get_or_create(slug="AE", name=u"United Arab Emirates", continent=asia)
CountryName.objects.get_or_create(slug="AF", name=u"Afghanistan", continent=asia)
CountryName.objects.get_or_create(slug="AG", name=u"Antigua and Barbuda", continent=north_america)
CountryName.objects.get_or_create(slug="AI", name=u"Anguilla", continent=north_america)
CountryName.objects.get_or_create(slug="AL", name=u"Albania", continent=europe)
CountryName.objects.get_or_create(slug="AM", name=u"Armenia", continent=asia)
CountryName.objects.get_or_create(slug="AO", name=u"Angola", continent=africa)
CountryName.objects.get_or_create(slug="AQ", name=u"Antarctica", continent=antarctica)
CountryName.objects.get_or_create(slug="AR", name=u"Argentina", continent=south_america)
CountryName.objects.get_or_create(slug="AS", name=u"American Samoa", continent=oceania)
CountryName.objects.get_or_create(slug="AT", name=u"Austria", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="AU", name=u"Australia", continent=oceania)
CountryName.objects.get_or_create(slug="AW", name=u"Aruba", continent=north_america)
CountryName.objects.get_or_create(slug="AX", name=u"Åland Islands", continent=europe)
CountryName.objects.get_or_create(slug="AZ", name=u"Azerbaijan", continent=asia)
CountryName.objects.get_or_create(slug="BA", name=u"Bosnia and Herzegovina", continent=europe)
CountryName.objects.get_or_create(slug="BB", name=u"Barbados", continent=north_america)
CountryName.objects.get_or_create(slug="BD", name=u"Bangladesh", continent=asia)
CountryName.objects.get_or_create(slug="BE", name=u"Belgium", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="BF", name=u"Burkina Faso", continent=africa)
CountryName.objects.get_or_create(slug="BG", name=u"Bulgaria", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="BH", name=u"Bahrain", continent=asia)
CountryName.objects.get_or_create(slug="BI", name=u"Burundi", continent=africa)
CountryName.objects.get_or_create(slug="BJ", name=u"Benin", continent=africa)
CountryName.objects.get_or_create(slug="BL", name=u"Saint Barthélemy", continent=north_america)
CountryName.objects.get_or_create(slug="BM", name=u"Bermuda", continent=north_america)
CountryName.objects.get_or_create(slug="BN", name=u"Brunei", continent=asia)
CountryName.objects.get_or_create(slug="BO", name=u"Bolivia", continent=south_america)
CountryName.objects.get_or_create(slug="BQ", name=u"Bonaire, Sint Eustatius and Saba", continent=north_america)
CountryName.objects.get_or_create(slug="BR", name=u"Brazil", continent=south_america)
CountryName.objects.get_or_create(slug="BS", name=u"Bahamas", continent=north_america)
CountryName.objects.get_or_create(slug="BT", name=u"Bhutan", continent=asia)
CountryName.objects.get_or_create(slug="BV", name=u"Bouvet Island", continent=antarctica)
CountryName.objects.get_or_create(slug="BW", name=u"Botswana", continent=africa)
CountryName.objects.get_or_create(slug="BY", name=u"Belarus", continent=europe)
CountryName.objects.get_or_create(slug="BZ", name=u"Belize", continent=north_america)
CountryName.objects.get_or_create(slug="CA", name=u"Canada", continent=north_america)
CountryName.objects.get_or_create(slug="CC", name=u"Cocos (Keeling) Islands", continent=asia)
CountryName.objects.get_or_create(slug="CD", name=u"Congo (the Democratic Republic of the)", continent=africa)
CountryName.objects.get_or_create(slug="CF", name=u"Central African Republic", continent=africa)
CountryName.objects.get_or_create(slug="CG", name=u"Congo", continent=africa)
CountryName.objects.get_or_create(slug="CH", name=u"Switzerland", continent=europe)
CountryName.objects.get_or_create(slug="CI", name=u"Côte d'Ivoire", continent=africa)
CountryName.objects.get_or_create(slug="CK", name=u"Cook Islands", continent=oceania)
CountryName.objects.get_or_create(slug="CL", name=u"Chile", continent=south_america)
CountryName.objects.get_or_create(slug="CM", name=u"Cameroon", continent=africa)
CountryName.objects.get_or_create(slug="CN", name=u"China", continent=asia)
CountryName.objects.get_or_create(slug="CO", name=u"Colombia", continent=south_america)
CountryName.objects.get_or_create(slug="CR", name=u"Costa Rica", continent=north_america)
CountryName.objects.get_or_create(slug="CU", name=u"Cuba", continent=north_america)
CountryName.objects.get_or_create(slug="CV", name=u"Cabo Verde", continent=africa)
CountryName.objects.get_or_create(slug="CW", name=u"Curaçao", continent=north_america)
CountryName.objects.get_or_create(slug="CX", name=u"Christmas Island", continent=asia)
CountryName.objects.get_or_create(slug="CY", name=u"Cyprus", continent=asia, in_eu=True)
CountryName.objects.get_or_create(slug="CZ", name=u"Czech Republic", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="DE", name=u"Germany", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="DJ", name=u"Djibouti", continent=africa)
CountryName.objects.get_or_create(slug="DK", name=u"Denmark", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="DM", name=u"Dominica", continent=north_america)
CountryName.objects.get_or_create(slug="DO", name=u"Dominican Republic", continent=north_america)
CountryName.objects.get_or_create(slug="DZ", name=u"Algeria", continent=africa)
CountryName.objects.get_or_create(slug="EC", name=u"Ecuador", continent=south_america)
CountryName.objects.get_or_create(slug="EE", name=u"Estonia", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="EG", name=u"Egypt", continent=africa)
CountryName.objects.get_or_create(slug="EH", name=u"Western Sahara", continent=africa)
CountryName.objects.get_or_create(slug="ER", name=u"Eritrea", continent=africa)
CountryName.objects.get_or_create(slug="ES", name=u"Spain", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="ET", name=u"Ethiopia", continent=africa)
CountryName.objects.get_or_create(slug="FI", name=u"Finland", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="FJ", name=u"Fiji", continent=oceania)
CountryName.objects.get_or_create(slug="FK", name=u"Falkland Islands [Malvinas]", continent=south_america)
CountryName.objects.get_or_create(slug="FM", name=u"Micronesia (Federated States of)", continent=oceania)
CountryName.objects.get_or_create(slug="FO", name=u"Faroe Islands", continent=europe)
CountryName.objects.get_or_create(slug="FR", name=u"France", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="GA", name=u"Gabon", continent=africa)
CountryName.objects.get_or_create(slug="GB", name=u"United Kingdom", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="GD", name=u"Grenada", continent=north_america)
CountryName.objects.get_or_create(slug="GE", name=u"Georgia", continent=asia)
CountryName.objects.get_or_create(slug="GF", name=u"French Guiana", continent=south_america)
CountryName.objects.get_or_create(slug="GG", name=u"Guernsey", continent=europe)
CountryName.objects.get_or_create(slug="GH", name=u"Ghana", continent=africa)
CountryName.objects.get_or_create(slug="GI", name=u"Gibraltar", continent=europe)
CountryName.objects.get_or_create(slug="GL", name=u"Greenland", continent=north_america)
CountryName.objects.get_or_create(slug="GM", name=u"Gambia", continent=africa)
CountryName.objects.get_or_create(slug="GN", name=u"Guinea", continent=africa)
CountryName.objects.get_or_create(slug="GP", name=u"Guadeloupe", continent=north_america)
CountryName.objects.get_or_create(slug="GQ", name=u"Equatorial Guinea", continent=africa)
CountryName.objects.get_or_create(slug="GR", name=u"Greece", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="GS", name=u"South Georgia and the South Sandwich Islands", continent=antarctica)
CountryName.objects.get_or_create(slug="GT", name=u"Guatemala", continent=north_america)
CountryName.objects.get_or_create(slug="GU", name=u"Guam", continent=oceania)
CountryName.objects.get_or_create(slug="GW", name=u"Guinea-Bissau", continent=africa)
CountryName.objects.get_or_create(slug="GY", name=u"Guyana", continent=south_america)
CountryName.objects.get_or_create(slug="HK", name=u"Hong Kong", continent=asia)
CountryName.objects.get_or_create(slug="HM", name=u"Heard Island and McDonald Islands", continent=antarctica)
CountryName.objects.get_or_create(slug="HN", name=u"Honduras", continent=north_america)
CountryName.objects.get_or_create(slug="HR", name=u"Croatia", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="HT", name=u"Haiti", continent=north_america)
CountryName.objects.get_or_create(slug="HU", name=u"Hungary", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="ID", name=u"Indonesia", continent=asia)
CountryName.objects.get_or_create(slug="IE", name=u"Ireland", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="IL", name=u"Israel", continent=asia)
CountryName.objects.get_or_create(slug="IM", name=u"Isle of Man", continent=europe)
CountryName.objects.get_or_create(slug="IN", name=u"India", continent=asia)
CountryName.objects.get_or_create(slug="IO", name=u"British Indian Ocean Territory", continent=asia)
CountryName.objects.get_or_create(slug="IQ", name=u"Iraq", continent=asia)
CountryName.objects.get_or_create(slug="IR", name=u"Iran", continent=asia)
CountryName.objects.get_or_create(slug="IS", name=u"Iceland", continent=europe)
CountryName.objects.get_or_create(slug="IT", name=u"Italy", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="JE", name=u"Jersey", continent=europe)
CountryName.objects.get_or_create(slug="JM", name=u"Jamaica", continent=north_america)
CountryName.objects.get_or_create(slug="JO", name=u"Jordan", continent=asia)
CountryName.objects.get_or_create(slug="JP", name=u"Japan", continent=asia)
CountryName.objects.get_or_create(slug="KE", name=u"Kenya", continent=africa)
CountryName.objects.get_or_create(slug="KG", name=u"Kyrgyzstan", continent=asia)
CountryName.objects.get_or_create(slug="KH", name=u"Cambodia", continent=asia)
CountryName.objects.get_or_create(slug="KI", name=u"Kiribati", continent=oceania)
CountryName.objects.get_or_create(slug="KM", name=u"Comoros", continent=africa)
CountryName.objects.get_or_create(slug="KN", name=u"Saint Kitts and Nevis", continent=north_america)
CountryName.objects.get_or_create(slug="KP", name=u"North Korea", continent=asia)
CountryName.objects.get_or_create(slug="KR", name=u"South Korea", continent=asia)
CountryName.objects.get_or_create(slug="KW", name=u"Kuwait", continent=asia)
CountryName.objects.get_or_create(slug="KY", name=u"Cayman Islands", continent=north_america)
CountryName.objects.get_or_create(slug="KZ", name=u"Kazakhstan", continent=asia)
CountryName.objects.get_or_create(slug="LA", name=u"Laos", continent=asia)
CountryName.objects.get_or_create(slug="LB", name=u"Lebanon", continent=asia)
CountryName.objects.get_or_create(slug="LC", name=u"Saint Lucia", continent=north_america)
CountryName.objects.get_or_create(slug="LI", name=u"Liechtenstein", continent=europe)
CountryName.objects.get_or_create(slug="LK", name=u"Sri Lanka", continent=asia)
CountryName.objects.get_or_create(slug="LR", name=u"Liberia", continent=africa)
CountryName.objects.get_or_create(slug="LS", name=u"Lesotho", continent=africa)
CountryName.objects.get_or_create(slug="LT", name=u"Lithuania", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="LU", name=u"Luxembourg", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="LV", name=u"Latvia", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="LY", name=u"Libya", continent=africa)
CountryName.objects.get_or_create(slug="MA", name=u"Morocco", continent=africa)
CountryName.objects.get_or_create(slug="MC", name=u"Monaco", continent=europe)
CountryName.objects.get_or_create(slug="MD", name=u"Moldova", continent=europe)
CountryName.objects.get_or_create(slug="ME", name=u"Montenegro", continent=europe)
CountryName.objects.get_or_create(slug="MF", name=u"Saint Martin (French part)", continent=north_america)
CountryName.objects.get_or_create(slug="MG", name=u"Madagascar", continent=africa)
CountryName.objects.get_or_create(slug="MH", name=u"Marshall Islands", continent=oceania)
CountryName.objects.get_or_create(slug="MK", name=u"Macedonia", continent=europe)
CountryName.objects.get_or_create(slug="ML", name=u"Mali", continent=africa)
CountryName.objects.get_or_create(slug="MM", name=u"Myanmar", continent=asia)
CountryName.objects.get_or_create(slug="MN", name=u"Mongolia", continent=asia)
CountryName.objects.get_or_create(slug="MO", name=u"Macao", continent=asia)
CountryName.objects.get_or_create(slug="MP", name=u"Northern Mariana Islands", continent=oceania)
CountryName.objects.get_or_create(slug="MQ", name=u"Martinique", continent=north_america)
CountryName.objects.get_or_create(slug="MR", name=u"Mauritania", continent=africa)
CountryName.objects.get_or_create(slug="MS", name=u"Montserrat", continent=north_america)
CountryName.objects.get_or_create(slug="MT", name=u"Malta", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="MU", name=u"Mauritius", continent=africa)
CountryName.objects.get_or_create(slug="MV", name=u"Maldives", continent=asia)
CountryName.objects.get_or_create(slug="MW", name=u"Malawi", continent=africa)
CountryName.objects.get_or_create(slug="MX", name=u"Mexico", continent=north_america)
CountryName.objects.get_or_create(slug="MY", name=u"Malaysia", continent=asia)
CountryName.objects.get_or_create(slug="MZ", name=u"Mozambique", continent=africa)
CountryName.objects.get_or_create(slug="NA", name=u"Namibia", continent=africa)
CountryName.objects.get_or_create(slug="NC", name=u"New Caledonia", continent=oceania)
CountryName.objects.get_or_create(slug="NE", name=u"Niger", continent=africa)
CountryName.objects.get_or_create(slug="NF", name=u"Norfolk Island", continent=oceania)
CountryName.objects.get_or_create(slug="NG", name=u"Nigeria", continent=africa)
CountryName.objects.get_or_create(slug="NI", name=u"Nicaragua", continent=north_america)
CountryName.objects.get_or_create(slug="NL", name=u"Netherlands", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="NO", name=u"Norway", continent=europe)
CountryName.objects.get_or_create(slug="NP", name=u"Nepal", continent=asia)
CountryName.objects.get_or_create(slug="NR", name=u"Nauru", continent=oceania)
CountryName.objects.get_or_create(slug="NU", name=u"Niue", continent=oceania)
CountryName.objects.get_or_create(slug="NZ", name=u"New Zealand", continent=oceania)
CountryName.objects.get_or_create(slug="OM", name=u"Oman", continent=asia)
CountryName.objects.get_or_create(slug="PA", name=u"Panama", continent=north_america)
CountryName.objects.get_or_create(slug="PE", name=u"Peru", continent=south_america)
CountryName.objects.get_or_create(slug="PF", name=u"French Polynesia", continent=oceania)
CountryName.objects.get_or_create(slug="PG", name=u"Papua New Guinea", continent=oceania)
CountryName.objects.get_or_create(slug="PH", name=u"Philippines", continent=asia)
CountryName.objects.get_or_create(slug="PK", name=u"Pakistan", continent=asia)
CountryName.objects.get_or_create(slug="PL", name=u"Poland", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="PM", name=u"Saint Pierre and Miquelon", continent=north_america)
CountryName.objects.get_or_create(slug="PN", name=u"Pitcairn", continent=oceania)
CountryName.objects.get_or_create(slug="PR", name=u"Puerto Rico", continent=north_america)
CountryName.objects.get_or_create(slug="PS", name=u"Palestine, State of", continent=asia)
CountryName.objects.get_or_create(slug="PT", name=u"Portugal", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="PW", name=u"Palau", continent=oceania)
CountryName.objects.get_or_create(slug="PY", name=u"Paraguay", continent=south_america)
CountryName.objects.get_or_create(slug="QA", name=u"Qatar", continent=asia)
CountryName.objects.get_or_create(slug="RE", name=u"Réunion", continent=africa)
CountryName.objects.get_or_create(slug="RO", name=u"Romania", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="RS", name=u"Serbia", continent=europe)
CountryName.objects.get_or_create(slug="RU", name=u"Russia", continent=europe)
CountryName.objects.get_or_create(slug="RW", name=u"Rwanda", continent=africa)
CountryName.objects.get_or_create(slug="SA", name=u"Saudi Arabia", continent=asia)
CountryName.objects.get_or_create(slug="SB", name=u"Solomon Islands", continent=oceania)
CountryName.objects.get_or_create(slug="SC", name=u"Seychelles", continent=africa)
CountryName.objects.get_or_create(slug="SD", name=u"Sudan", continent=africa)
CountryName.objects.get_or_create(slug="SE", name=u"Sweden", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="SG", name=u"Singapore", continent=asia)
CountryName.objects.get_or_create(slug="SH", name=u"Saint Helena, Ascension and Tristan da Cunha", continent=africa)
CountryName.objects.get_or_create(slug="SI", name=u"Slovenia", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="SJ", name=u"Svalbard and Jan Mayen", continent=europe)
CountryName.objects.get_or_create(slug="SK", name=u"Slovakia", continent=europe, in_eu=True)
CountryName.objects.get_or_create(slug="SL", name=u"Sierra Leone", continent=africa)
CountryName.objects.get_or_create(slug="SM", name=u"San Marino", continent=europe)
CountryName.objects.get_or_create(slug="SN", name=u"Senegal", continent=africa)
CountryName.objects.get_or_create(slug="SO", name=u"Somalia", continent=africa)
CountryName.objects.get_or_create(slug="SR", name=u"Suriname", continent=south_america)
CountryName.objects.get_or_create(slug="SS", name=u"South Sudan", continent=africa)
CountryName.objects.get_or_create(slug="ST", name=u"Sao Tome and Principe", continent=africa)
CountryName.objects.get_or_create(slug="SV", name=u"El Salvador", continent=north_america)
CountryName.objects.get_or_create(slug="SX", name=u"Sint Maarten (Dutch part)", continent=north_america)
CountryName.objects.get_or_create(slug="SY", name=u"Syria", continent=asia)
CountryName.objects.get_or_create(slug="SZ", name=u"Swaziland", continent=africa)
CountryName.objects.get_or_create(slug="TC", name=u"Turks and Caicos Islands", continent=north_america)
CountryName.objects.get_or_create(slug="TD", name=u"Chad", continent=africa)
CountryName.objects.get_or_create(slug="TF", name=u"French Southern Territories", continent=antarctica)
CountryName.objects.get_or_create(slug="TG", name=u"Togo", continent=africa)
CountryName.objects.get_or_create(slug="TH", name=u"Thailand", continent=asia)
CountryName.objects.get_or_create(slug="TJ", name=u"Tajikistan", continent=asia)
CountryName.objects.get_or_create(slug="TK", name=u"Tokelau", continent=oceania)
CountryName.objects.get_or_create(slug="TL", name=u"Timor-Leste", continent=asia)
CountryName.objects.get_or_create(slug="TM", name=u"Turkmenistan", continent=asia)
CountryName.objects.get_or_create(slug="TN", name=u"Tunisia", continent=africa)
CountryName.objects.get_or_create(slug="TO", name=u"Tonga", continent=oceania)
CountryName.objects.get_or_create(slug="TR", name=u"Turkey", continent=europe)
CountryName.objects.get_or_create(slug="TT", name=u"Trinidad and Tobago", continent=north_america)
CountryName.objects.get_or_create(slug="TV", name=u"Tuvalu", continent=oceania)
CountryName.objects.get_or_create(slug="TW", name=u"Taiwan", continent=asia)
CountryName.objects.get_or_create(slug="TZ", name=u"Tanzania", continent=africa)
CountryName.objects.get_or_create(slug="UA", name=u"Ukraine", continent=europe)
CountryName.objects.get_or_create(slug="UG", name=u"Uganda", continent=africa)
CountryName.objects.get_or_create(slug="UM", name=u"United States Minor Outlying Islands", continent=oceania)
CountryName.objects.get_or_create(slug="US", name=u"United States of America", continent=north_america)
CountryName.objects.get_or_create(slug="UY", name=u"Uruguay", continent=south_america)
CountryName.objects.get_or_create(slug="UZ", name=u"Uzbekistan", continent=asia)
CountryName.objects.get_or_create(slug="VA", name=u"Holy See", continent=europe)
CountryName.objects.get_or_create(slug="VC", name=u"Saint Vincent and the Grenadines", continent=north_america)
CountryName.objects.get_or_create(slug="VE", name=u"Venezuela", continent=south_america)
CountryName.objects.get_or_create(slug="VG", name=u"Virgin Islands (British)", continent=north_america)
CountryName.objects.get_or_create(slug="VI", name=u"Virgin Islands (U.S.)", continent=north_america)
CountryName.objects.get_or_create(slug="VN", name=u"Vietnam", continent=asia)
CountryName.objects.get_or_create(slug="VU", name=u"Vanuatu", continent=oceania)
CountryName.objects.get_or_create(slug="WF", name=u"Wallis and Futuna", continent=oceania)
CountryName.objects.get_or_create(slug="WS", name=u"Samoa", continent=oceania)
CountryName.objects.get_or_create(slug="YE", name=u"Yemen", continent=asia)
CountryName.objects.get_or_create(slug="YT", name=u"Mayotte", continent=africa)
CountryName.objects.get_or_create(slug="ZA", name=u"South Africa", continent=africa)
CountryName.objects.get_or_create(slug="ZM", name=u"Zambia", continent=africa)
CountryName.objects.get_or_create(slug="ZW", name=u"Zimbabwe", continent=africa)
class Migration(migrations.Migration):
dependencies = [
('name', '0022_continentname_countryname'),
]
operations = [
migrations.RunPython(insert_initial_country_continent_names, migrations.RunPython.noop)
]

View file

@ -46,6 +46,8 @@ class StdLevelName(NameModel):
class IntendedStdLevelName(NameModel):
"""Proposed Standard, (Draft Standard), Internet Standard, Experimental,
Informational, Best Current Practice, Historic, ..."""
class FormalLanguageName(NameModel):
"""ABNF, ASN.1, C code, CBOR, JSON, XML, ..."""
class DocReminderTypeName(NameModel):
"Stream state"
class BallotPositionName(NameModel):
@ -99,3 +101,10 @@ class ReviewResultName(NameModel):
Ready with nits, Serious Issues"""
class TopicAudienceName(NameModel):
"""General, Nominee, Nomcom Member"""
class ContinentName(NameModel):
"Africa, Antarctica, Asia, ..."
class CountryName(NameModel):
"Afghanistan, Aaland Islands, Albania, ..."
continent = models.ForeignKey(ContinentName)
in_eu = models.BooleanField(verbose_name="In EU", default=False)

View file

@ -15,7 +15,7 @@ from ietf.name.models import (TimeSlotTypeName, GroupStateName, DocTagName, Inte
LiaisonStatementTagName, FeedbackTypeName, LiaisonStatementState, StreamName,
BallotPositionName, DBTemplateTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewTypeName, ReviewResultName,
TopicAudienceName, )
TopicAudienceName, FormalLanguageName, ContinentName, CountryName)
class TimeSlotTypeNameResource(ModelResource):
@ -471,3 +471,51 @@ class TopicAudienceNameResource(ModelResource):
}
api.name.register(TopicAudienceNameResource())
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())
class ContinentNameResource(ModelResource):
class Meta:
queryset = ContinentName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'continentname'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(ContinentNameResource())
class CountryNameResource(ModelResource):
continent = ToOneField(ContinentNameResource, 'continent')
class Meta:
queryset = CountryName.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'countryname'
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
"in_eu": ALL,
"continent": ALL_WITH_RELATIONS,
}
api.name.register(CountryNameResource())

View file

@ -32,4 +32,3 @@ class PersonAdmin(admin.ModelAdmin):
inlines = [ EmailInline, AliasInline, ]
# actions = None
admin.site.register(Person, PersonAdmin)

View file

@ -16,7 +16,7 @@ from django.utils.text import slugify
import debug # pyflakes:ignore
from ietf.person.name import name_parts, initials
from ietf.person.name import name_parts, initials, plain_name
from ietf.utils.mail import send_mail_preformatted
from ietf.utils.storage import NoLocationMigrationFileSystemStorage
from ietf.utils.mail import formataddr
@ -51,8 +51,7 @@ class PersonInfo(models.Model):
return (first and first[0]+"." or "")+(middle or "")+" "+last+(suffix and " "+suffix or "")
def plain_name(self):
if not hasattr(self, '_cached_plain_name'):
prefix, first, middle, last, suffix = name_parts(self.name)
self._cached_plain_name = u" ".join([first, last])
self._cached_plain_name = plain_name(self.name)
return self._cached_plain_name
def ascii_name(self):
if not hasattr(self, '_cached_ascii_name'):
@ -137,18 +136,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
@ -244,7 +243,11 @@ class Email(models.Model):
return self.address
def name_and_email(self):
"Returns name and email, e.g.: u'Ano Nymous <ano@nymous.org>' "
"""
Returns name and email, e.g.: u'Ano Nymous <ano@nymous.org>'
Is intended for display use, not in email context.
Use self.formatted_email() for that.
"""
if self.person:
return u"%s <%s>" % (self.person.plain_name(), self.address)
else:
@ -260,15 +263,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

@ -54,6 +54,10 @@ def initials(name):
initials = u" ".join([ n[0]+'.' for n in given.split() ])
return initials
def plain_name(name):
prefix, first, middle, last, suffix = name_parts(name)
return u" ".join([first, last])
if __name__ == "__main__":
import sys
name = u" ".join(sys.argv[1:])

View file

@ -6,7 +6,7 @@ from tastypie.cache import SimpleCache
from ietf import api
from ietf.person.models import Person, Email, Alias, PersonHistory
from ietf.person.models import (Person, Email, Alias, PersonHistory)
from ietf.utils.resources import UserResource
@ -82,4 +82,3 @@ class PersonHistoryResource(ModelResource):
"user": ALL_WITH_RELATIONS,
}
api.person.register(PersonHistoryResource())

View file

@ -754,7 +754,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

@ -104,6 +104,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.CharField(max_length=255, required=False, help_text="Country")
# check for id within parenthesis to ensure name was selected from the list
def clean_person(self):

View file

@ -540,7 +540,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,13 +574,17 @@ def authors(request, id):
return redirect('ietf.secr.drafts.views.view', id=id)
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('ietf.secr.drafts.views.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 'ietf.secr.drafts.views.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 'ietf.secr.rolodex.views.view' id=author.author.person.id %}">{{ author.author.person.name }}</a></td></tr>
<tr><td><a href="{% url 'ietf.secr.rolodex.views.view' id=author.person.id %}">{{ author.person.name }}</a></td></tr>
{% endfor %}
</table>
</div> <!-- inline-related -->

View file

@ -398,6 +398,7 @@ INSTALLED_APPS = (
'ietf.redirects',
'ietf.release',
'ietf.review',
'ietf.stats',
'ietf.submit',
'ietf.sync',
'ietf.utils',
@ -487,8 +488,9 @@ TEST_CODE_COVERAGE_EXCLUDE = [
"ietf/settings*",
"ietf/utils/templatetags/debug_filters.py",
"ietf/utils/test_runner.py",
"name/generate_fixtures.py",
"review/import_from_review_tool.py",
"ietf/name/generate_fixtures.py",
"ietf/review/import_from_review_tool.py",
"ietf/stats/backfill_data.py",
]
# These are filename globs. They are used by test_parse_templates() and
@ -557,6 +559,8 @@ INTERNET_DRAFT_ARCHIVE_DIR = '/a/www/www6s/draft-archive'
INTERNET_ALL_DRAFTS_ARCHIVE_DIR = '/a/www/www6s/archive/id'
MEETING_RECORDINGS_DIR = '/a/www/audio'
DOCUMENT_FORMAT_WHITELIST = ["txt", "ps", "pdf", "xml", "html", ]
# Mailing list info URL for lists hosted on the IETF servers
MAILING_LIST_INFO_URL = "https://www.ietf.org/mailman/listinfo/%(list_addr)s"
MAILING_LIST_ARCHIVE_URL = "https://mailarchive.ietf.org"
@ -619,6 +623,8 @@ AUDIO_IMPORT_EMAIL = ['agenda@ietf.org','ietf@meetecho.com']
IANA_EVAL_EMAIL = "drafts-eval@icann.org"
SESSION_REQUEST_FROM_EMAIL = 'IETF Meeting Session Request Tool <session-request@ietf.org>'
SECRETARIAT_TICKET_EMAIL = "ietf-action@ietf.org"
# Put real password in settings_local.py
IANA_SYNC_PASSWORD = "secret"
IANA_SYNC_CHANGES_URL = "https://datatracker.iana.org:4443/data-tracker/changes"
@ -883,6 +889,8 @@ SILENCED_SYSTEM_CHECKS = [
"fields.W342", # Setting unique=True on a ForeignKey has the same effect as using a OneToOneField.
]
STATS_NAMES_LIMIT = 25
# Put the production SECRET_KEY in settings_local.py, and also any other
# sensitive or site-specific changes. DO NOT commit settings_local.py to svn.
from settings_local import * # pyflakes:ignore pylint: disable=wildcard-import

View file

@ -580,6 +580,15 @@ table.simple-table td:last-child {
width: 7em;
}
.document-stats .popover .element {
padding-left: 1em;
text-indent: -1em;
}
.document-stats #chart {
height: 25em;
}
.stats-time-graph {
height: 15em;
}

View file

@ -0,0 +1,60 @@
$(document).ready(function () {
if (window.chartConf) {
window.chartConf.credits = {
enabled: false
};
window.chartConf.exporting = {
fallbackToExportServer: false
};
if (!window.chartConf.legend)
window.chartConf.legend = {
enabled: false
};
var chart = Highcharts.chart('chart', window.chartConf);
}
if (window.pieChartConf) {
window.pieChartConf.credits = {
enabled: false
};
var pieChart = Highcharts.chart('pie-chart', window.pieChartConf);
}
/*
$(".popover-details").each(function () {
var stdNameRegExp = new RegExp("^(rfc|bcp|fyi|std)[0-9]+$", 'i');
var draftRegExp = new RegExp("^draft-", 'i');
var html = [];
$.each(($(this).data("elements") || "").split("|"), function (i, element) {
if (!$.trim(element))
return;
if (draftRegExp.test(element) || stdNameRegExp.test(element)) {
var displayName = element;
if (stdNameRegExp.test(element))
displayName = element.slice(0, 3).toUpperCase() + " " + element.slice(3);
html.push('<div class="element"><a href="/doc/' + element + '/">' + displayName + '</a></div>');
}
else {
html.push('<div class="element">' + element + '</div>');
}
});
if ($(this).data("sliced"))
html.push('<div class="text-center">&hellip;</div>');
$(this).popover({
trigger: "focus",
template: '<div class="popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>',
content: html.join(""),
placement: "top",
html: true
}).on("click", function (e) {
e.preventDefault();
});
});*/
});

22
ietf/stats/admin.py Normal file
View file

@ -0,0 +1,22 @@
from django.contrib import admin
from ietf.stats.models import AffiliationAlias, AffiliationIgnoredEnding, CountryAlias
class AffiliationAliasAdmin(admin.ModelAdmin):
list_filter = ["name"]
list_display = ["alias", "name"]
search_fields = ["alias", "name"]
admin.site.register(AffiliationAlias, AffiliationAliasAdmin)
class AffiliationIgnoredEndingAdmin(admin.ModelAdmin):
list_display = ["ending"]
search_fields = ["ending"]
admin.site.register(AffiliationIgnoredEnding, AffiliationIgnoredEndingAdmin)
class CountryAliasAdmin(admin.ModelAdmin):
list_filter = ["country"]
list_display = ["alias", "country"]
search_fields = ["alias", "country__name"]
admin.site.register(CountryAlias, CountryAliasAdmin)

175
ietf/stats/backfill_data.py Executable file
View file

@ -0,0 +1,175 @@
#!/usr/bin/env python
from __future__ import print_function, unicode_literals
import sys
import os
import os.path
import argparse
import time
basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../.."))
sys.path = [ basedir ] + sys.path
os.environ["DJANGO_SETTINGS_MODULE"] = "ietf.settings"
virtualenv_activation = os.path.join(basedir, "env", "bin", "activate_this.py")
if os.path.exists(virtualenv_activation):
execfile(virtualenv_activation, dict(__file__=virtualenv_activation))
import django
django.setup()
from django.conf import settings
import debug # pyflakes:ignore
from ietf.doc.models import Document
from ietf.name.models import FormalLanguageName
from ietf.utils.draft import Draft
parser = argparse.ArgumentParser()
parser.add_argument("--document", help="specific document name")
parser.add_argument("--words", action="store_true", help="fill in word count")
parser.add_argument("--formlang", action="store_true", help="fill in formal languages")
parser.add_argument("--authors", action="store_true", help="fill in author info")
args = parser.parse_args()
formal_language_dict = { l.pk: l for l in FormalLanguageName.objects.all() }
docs_qs = Document.objects.filter(type="draft")
if args.document:
docs_qs = docs_qs.filter(docalias__name=args.document)
ts = time.strftime("%Y-%m-%d_%H:%M%z")
logfile = open('backfill-authorstats-%s.log'%ts, 'w')
print("Writing log to %s" % os.path.abspath(logfile.name))
def say(msg):
msg = msg.encode('utf8')
sys.stderr.write(msg)
sys.stderr.write('\n')
logfile.write(msg)
logfile.write('\n')
def unicode(text):
if text is None:
return text
# order matters here:
for encoding in ['ascii', 'utf8', 'latin1', ]:
try:
utext = text.decode(encoding)
if encoding == 'latin1':
say("Warning: falling back to latin1 decoding for %s" % utext)
return utext
except UnicodeDecodeError:
pass
start = time.time()
for doc in docs_qs.prefetch_related("docalias_set", "formal_languages", "documentauthor_set", "documentauthor_set__person", "documentauthor_set__person__alias_set"):
canonical_name = doc.name
for n in doc.docalias_set.all():
if n.name.startswith("rfc"):
canonical_name = n.name
if canonical_name.startswith("rfc"):
path = os.path.join(settings.RFC_PATH, canonical_name + ".txt")
else:
path = os.path.join(settings.INTERNET_ALL_DRAFTS_ARCHIVE_DIR, canonical_name + "-" + doc.rev + ".txt")
if not os.path.exists(path):
say("Skipping %s, no txt file found at %s" % (doc.name, path))
continue
with open(path, 'r') as f:
say("\nProcessing %s" % doc.name)
sys.stdout.flush()
d = Draft(f.read(), path)
updated = False
updates = {}
if args.words:
words = d.get_wordcount()
if words != doc.words:
updates["words"] = words
if args.formlang:
langs = d.get_formal_languages()
new_formal_languages = set(formal_language_dict[l] for l in langs)
old_formal_languages = set(doc.formal_languages.all())
if new_formal_languages != old_formal_languages:
for l in new_formal_languages - old_formal_languages:
doc.formal_languages.add(l)
updated = True
for l in old_formal_languages - new_formal_languages:
doc.formal_languages.remove(l)
updated = True
if args.authors:
old_authors = doc.documentauthor_set.all()
old_authors_by_name = {}
old_authors_by_email = {}
for author in old_authors:
for alias in author.person.alias_set.all():
old_authors_by_name[alias.name] = author
old_authors_by_name[author.person.plain_name()] = author
if author.email_id:
old_authors_by_email[author.email_id] = author
# the draft parser sometimes has a problem when
# affiliation isn't in the second line and it then thinks
# it's an extra author - skip those extra authors
seen = set()
for full, _, _, _, _, email, country, company in d.get_author_list():
full, email, country, company = [ unicode(s) for s in [full, email, country, company, ] ]
if email in seen:
continue
seen.add(email)
old_author = None
if email:
old_author = old_authors_by_email.get(email)
if not old_author:
old_author = old_authors_by_name.get(full)
if not old_author:
say("UNKNOWN AUTHOR: %s, %s, %s, %s, %s" % (doc.name, full, email, country, company))
continue
if old_author.affiliation != company:
say("new affiliation: %s [ %s <%s> ] %s -> %s" % (canonical_name, full, email, old_author.affiliation, company))
old_author.affiliation = company
old_author.save(update_fields=["affiliation"])
updated = True
if country is None:
country = ""
if old_author.country != country:
say("new country: %s [ %s <%s> ] %s -> %s" % (canonical_name , full, email, old_author.country, country))
old_author.country = country
old_author.save(update_fields=["country"])
updated = True
if updates:
Document.objects.filter(pk=doc.pk).update(**updates)
updated = True
if updated:
say("updated: %s" % canonical_name)
stop = time.time()
dur = stop-start
sec = dur%60
min = dur//60
say("Processing time %d:%02d" % (min, sec))
print("\n\nWrote log to %s" % os.path.abspath(logfile.name))
logfile.close()

View file

View file

@ -0,0 +1,44 @@
# Copyright 2016 IETF Trust
import syslog
from django.core.management.base import BaseCommand, CommandError
import debug # pyflakes:ignore
from ietf.meeting.models import Meeting
from ietf.stats.utils import get_meeting_registration_data
logtag = __name__.split('.')[-1]
logname = "user.log"
syslog.openlog(logtag, syslog.LOG_PID, syslog.LOG_USER)
class Command(BaseCommand):
help = "Fetch meeting attendee figures from ietf.org/registration/attendees."
def add_arguments(self, parser):
parser.add_argument("--meeting", help="meeting to fetch data for")
parser.add_argument("--all", action="store_true", help="fetch data for all meetings")
parser.add_argument("--latest", type=int, help="fetch data for latest N meetings")
def handle(self, *args, **options):
self.verbosity = options['verbosity']
meetings = Meeting.objects.none()
if options['meeting']:
meetings = Meeting.objects.filter(number=options['meeting'], type="ietf")
elif options['all']:
meetings = Meeting.objects.filter(type="ietf").order_by("date")
elif options['latest']:
meetings = Meeting.objects.filter(type="ietf").order_by("-date")[:options['latest']]
else:
raise CommandError("Please use one of --meeting, --all or --latest")
for meeting in meetings:
added, processed, total = get_meeting_registration_data(meeting)
msg = "Fetched data for meeting %3s: %4d processed, %4d added, %4d in table" % (meeting.number, processed, added, total)
if self.stdout.isatty():
self.stdout.write(msg+'\n') # make debugging a bit easier
else:
syslog.syslog(msg)

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('name', '0023_add_country_continent_names'),
]
operations = [
migrations.CreateModel(
name='AffiliationAlias',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('alias', models.CharField(help_text=b"Note that aliases will be matched case-insensitive and both before and after some clean-up.", max_length=255, unique=True)),
('name', models.CharField(max_length=255)),
],
options={'verbose_name_plural': 'affiliation aliases'},
),
migrations.CreateModel(
name='AffiliationIgnoredEnding',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('ending', models.CharField(help_text=b"Regexp with ending, e.g. 'Inc\\.?' - remember to escape .!", max_length=255)),
],
),
migrations.CreateModel(
name='CountryAlias',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('alias', models.CharField(help_text=b"Note that lower-case aliases are matched case-insensitive while aliases with at least one uppercase letter is matched case-sensitive. So 'United States' is best entered as 'united states' so it both matches 'United States' and 'United states' and 'UNITED STATES', whereas 'US' is best entered as 'US' so it doesn't accidentally match an ordinary word like 'us'.", max_length=255)),
('country', models.ForeignKey(to='name.CountryName', max_length=255)),
],
options={'verbose_name_plural': 'country aliases'},
),
]

View file

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
def add_affiliation_info(apps, schema_editor):
AffiliationAlias = apps.get_model("stats", "AffiliationAlias")
AffiliationAlias.objects.get_or_create(alias="cisco", name="Cisco Systems")
AffiliationAlias.objects.get_or_create(alias="cisco system", name="Cisco Systems")
AffiliationAlias.objects.get_or_create(alias="cisco systems (india) private limited", name="Cisco Systems")
AffiliationAlias.objects.get_or_create(alias="cisco systems india pvt", name="Cisco Systems")
AffiliationIgnoredEnding = apps.get_model("stats", "AffiliationIgnoredEnding")
AffiliationIgnoredEnding.objects.get_or_create(ending="LLC\.?")
AffiliationIgnoredEnding.objects.get_or_create(ending="Ltd\.?")
AffiliationIgnoredEnding.objects.get_or_create(ending="Inc\.?")
AffiliationIgnoredEnding.objects.get_or_create(ending="GmbH\.?")
CountryAlias = apps.get_model("stats", "CountryAlias")
for iso_country_code in ['AD', 'AE', 'AF', 'AG', 'AI', 'AL', 'AM', 'AO', 'AQ', 'AR', 'AS', 'AT', 'AU', 'AW',
'AX', 'AZ', 'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BL', 'BM', 'BN',
'BO', 'BQ', 'BR', 'BS', 'BT', 'BV', 'BW', 'BY', 'BZ', 'CA', 'CC', 'CD', 'CF', 'CG',
'CH', 'CI', 'CK', 'CL', 'CM', 'CN', 'CO', 'CR', 'CU', 'CV', 'CW', 'CX', 'CY', 'CZ',
'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'EH', 'ER', 'ES', 'ET', 'FI',
'FJ', 'FK', 'FM', 'FO', 'FR', 'GA', 'GB', 'GD', 'GE', 'GF', 'GG', 'GH', 'GI', 'GL',
'GM', 'GN', 'GP', 'GQ', 'GR', 'GS', 'GT', 'GU', 'GW', 'GY', 'HK', 'HM', 'HN', 'HR',
'HT', 'HU', 'ID', 'IE', 'IL', 'IM', 'IN', 'IO', 'IQ', 'IR', 'IS', 'IT', 'JE', 'JM',
'JO', 'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KP', 'KR', 'KW', 'KY', 'KZ', 'LA',
'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV', 'LY', 'MA', 'MC', 'MD', 'ME',
'MF', 'MG', 'MH', 'MK', 'ML', 'MM', 'MN', 'MO', 'MP', 'MQ', 'MR', 'MS', 'MT', 'MU',
'MV', 'MW', 'MX', 'MY', 'MZ', 'NA', 'NC', 'NE', 'NF', 'NG', 'NI', 'NL', 'NO', 'NP',
'NR', 'NU', 'NZ', 'OM', 'PA', 'PE', 'PF', 'PG', 'PH', 'PK', 'PL', 'PM', 'PN', 'PR',
'PS', 'PT', 'PW', 'PY', 'QA', 'RE', 'RO', 'RS', 'RU', 'RW', 'SA', 'SB', 'SC', 'SD',
'SE', 'SG', 'SH', 'SI', 'SJ', 'SK', 'SL', 'SM', 'SN', 'SO', 'SR', 'SS', 'ST', 'SV',
'SX', 'SY', 'SZ', 'TC', 'TD', 'TF', 'TG', 'TH', 'TJ', 'TK', 'TL', 'TM', 'TN', 'TO',
'TR', 'TT', 'TV', 'TW', 'TZ', 'UA', 'UG', 'UM', 'US', 'UY', 'UZ', 'VA', 'VC', 'VE',
'VG', 'VI', 'VN', 'VU', 'WF', 'WS', 'YE', 'YT', 'ZA', 'ZM', 'ZW']:
CountryAlias.objects.get_or_create(alias=iso_country_code, country_id=iso_country_code)
CountryAlias.objects.get_or_create(alias="russian federation", country_id="RU")
CountryAlias.objects.get_or_create(alias="p. r. china", country_id="CN")
CountryAlias.objects.get_or_create(alias="p.r. china", country_id="CN")
CountryAlias.objects.get_or_create(alias="p.r.china", country_id="CN")
CountryAlias.objects.get_or_create(alias="p.r china", country_id="CN")
CountryAlias.objects.get_or_create(alias="p.r. of china", country_id="CN")
CountryAlias.objects.get_or_create(alias="PRC", country_id="CN")
CountryAlias.objects.get_or_create(alias="P.R.C", country_id="CN")
CountryAlias.objects.get_or_create(alias="P.R.C.", country_id="CN")
CountryAlias.objects.get_or_create(alias="beijing", country_id="CN")
CountryAlias.objects.get_or_create(alias="shenzhen", country_id="CN")
CountryAlias.objects.get_or_create(alias="R.O.C.", country_id="TW")
CountryAlias.objects.get_or_create(alias="usa", country_id="US")
CountryAlias.objects.get_or_create(alias="UAS", country_id="US")
CountryAlias.objects.get_or_create(alias="USA.", country_id="US")
CountryAlias.objects.get_or_create(alias="u.s.a.", country_id="US")
CountryAlias.objects.get_or_create(alias="u. s. a.", country_id="US")
CountryAlias.objects.get_or_create(alias="u.s.a", country_id="US")
CountryAlias.objects.get_or_create(alias="u.s.", country_id="US")
CountryAlias.objects.get_or_create(alias="U.S", country_id="GB")
CountryAlias.objects.get_or_create(alias="US of A", country_id="US")
CountryAlias.objects.get_or_create(alias="united sates", country_id="US")
CountryAlias.objects.get_or_create(alias="united state", country_id="US")
CountryAlias.objects.get_or_create(alias="united states", country_id="US")
CountryAlias.objects.get_or_create(alias="unites states", country_id="US")
CountryAlias.objects.get_or_create(alias="texas", country_id="US")
CountryAlias.objects.get_or_create(alias="UK", country_id="GB")
CountryAlias.objects.get_or_create(alias="united kingcom", country_id="GB")
CountryAlias.objects.get_or_create(alias="great britain", country_id="GB")
CountryAlias.objects.get_or_create(alias="england", country_id="GB")
CountryAlias.objects.get_or_create(alias="U.K.", country_id="GB")
CountryAlias.objects.get_or_create(alias="U.K", country_id="GB")
CountryAlias.objects.get_or_create(alias="Uk", country_id="GB")
CountryAlias.objects.get_or_create(alias="scotland", country_id="GB")
CountryAlias.objects.get_or_create(alias="republic of korea", country_id="KR")
CountryAlias.objects.get_or_create(alias="korea", country_id="KR")
CountryAlias.objects.get_or_create(alias="korea rep", country_id="KR")
CountryAlias.objects.get_or_create(alias="korea (the republic of)", country_id="KR")
CountryAlias.objects.get_or_create(alias="the netherlands", country_id="NL")
CountryAlias.objects.get_or_create(alias="netherland", country_id="NL")
CountryAlias.objects.get_or_create(alias="danmark", country_id="DK")
CountryAlias.objects.get_or_create(alias="sweeden", country_id="SE")
CountryAlias.objects.get_or_create(alias="swede", country_id="SE")
CountryAlias.objects.get_or_create(alias="belgique", country_id="BE")
CountryAlias.objects.get_or_create(alias="madrid", country_id="ES")
CountryAlias.objects.get_or_create(alias="espana", country_id="ES")
CountryAlias.objects.get_or_create(alias="hellas", country_id="GR")
CountryAlias.objects.get_or_create(alias="gemany", country_id="DE")
CountryAlias.objects.get_or_create(alias="deutschland", country_id="DE")
CountryAlias.objects.get_or_create(alias="italia", country_id="IT")
CountryAlias.objects.get_or_create(alias="isreal", country_id="IL")
CountryAlias.objects.get_or_create(alias="tel aviv", country_id="IL")
CountryAlias.objects.get_or_create(alias="UAE", country_id="AE")
CountryAlias.objects.get_or_create(alias="grand-duchy of luxembourg", country_id="LU")
CountryAlias.objects.get_or_create(alias="brasil", country_id="BR")
class Migration(migrations.Migration):
dependencies = [
('stats', '0001_initial'),
]
operations = [
migrations.RunPython(add_affiliation_info, migrations.RunPython.noop)
]

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-30 14:38
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('person', '0015_clean_primary'),
('meeting', '0047_import_shared_audio_files'),
('stats', '0002_add_initial_aliases'),
]
operations = [
migrations.CreateModel(
name='MeetingRegistration',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
('affiliation', models.CharField(blank=True, max_length=255)),
('country_code', models.CharField(max_length=2)),
('meeting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='meeting.Meeting')),
('person', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='person.Person')),
],
),
]

View file

56
ietf/stats/models.py Normal file
View file

@ -0,0 +1,56 @@
from django.db import models
from ietf.meeting.models import Meeting
from ietf.name.models import CountryName
from ietf.person.models import Person
class AffiliationAlias(models.Model):
"""Records that alias should be treated as name for statistical
purposes."""
alias = models.CharField(max_length=255, help_text="Note that aliases will be matched case-insensitive and both before and after some clean-up.", unique=True)
name = models.CharField(max_length=255)
def __unicode__(self):
return u"{} -> {}".format(self.alias, self.name)
def save(self, *args, **kwargs):
self.alias = self.alias.lower()
super(AffiliationAlias, self).save(*args, **kwargs)
class Meta:
verbose_name_plural = "affiliation aliases"
class AffiliationIgnoredEnding(models.Model):
"""Records that ending should be stripped from the affiliation for statistical purposes."""
ending = models.CharField(max_length=255, help_text="Regexp with ending, e.g. 'Inc\\.?' - remember to escape .!")
def __unicode__(self):
return self.ending
class CountryAlias(models.Model):
"""Records that alias should be treated as country for statistical
purposes."""
alias = models.CharField(max_length=255, help_text="Note that lower-case aliases are matched case-insensitive while aliases with at least one uppercase letter is matched case-sensitive. So 'United States' is best entered as 'united states' so it both matches 'United States' and 'United states' and 'UNITED STATES', whereas 'US' is best entered as 'US' so it doesn't accidentally match an ordinary word like 'us'.")
country = models.ForeignKey(CountryName, max_length=255)
def __unicode__(self):
return u"{} -> {}".format(self.alias, self.country.name)
class Meta:
verbose_name_plural = "country aliases"
class MeetingRegistration(models.Model):
"""Registration attendee records from the IETF registration system"""
meeting = models.ForeignKey(Meeting)
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
affiliation = models.CharField(blank=True, max_length=255)
country_code = models.CharField(max_length=2) # ISO 3166
person = models.ForeignKey(Person, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
def __unicode__(self):
return u"{} {}".format(self.first_name, self.last_name)

70
ietf/stats/resources.py Normal file
View file

@ -0,0 +1,70 @@
# Autogenerated by the makeresources management command 2017-02-15 10:10 PST
from tastypie.resources import ModelResource
from tastypie.fields import ToManyField # pyflakes:ignore
from tastypie.constants import ALL, ALL_WITH_RELATIONS # pyflakes:ignore
from tastypie.cache import SimpleCache
from ietf import api
from ietf.api import ToOneField # pyflakes:ignore
from ietf.stats.models import CountryAlias, AffiliationIgnoredEnding, AffiliationAlias, MeetingRegistration
from ietf.name.resources import CountryNameResource
class CountryAliasResource(ModelResource):
country = ToOneField(CountryNameResource, 'country')
class Meta:
queryset = CountryAlias.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'countryalias'
filtering = {
"id": ALL,
"alias": ALL,
"country": ALL_WITH_RELATIONS,
}
api.stats.register(CountryAliasResource())
class AffiliationIgnoredEndingResource(ModelResource):
class Meta:
queryset = AffiliationIgnoredEnding.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'affiliationignoredending'
filtering = {
"id": ALL,
"ending": ALL,
}
api.stats.register(AffiliationIgnoredEndingResource())
class AffiliationAliasResource(ModelResource):
class Meta:
queryset = AffiliationAlias.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'affiliationalias'
filtering = {
"id": ALL,
"alias": ALL,
"name": ALL,
}
api.stats.register(AffiliationAliasResource())
class MeetingRegistrationResource(ModelResource):
class Meta:
queryset = MeetingRegistration.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'meetingregistration'
filtering = {
"id": ALL,
"meeting": ALL_WITH_RELATIONS,
"first_name": ALL,
"last_name": ALL,
"affiliation": ALL,
"country_code": ALL,
"email": ALL,
"person": ALL_WITH_RELATIONS
}
api.stats.register(MeetingRegistrationResource())

View file

@ -1,17 +1,149 @@
import datetime
from mock import patch
from pyquery import PyQuery
from requests import Response
from django.urls import reverse as urlreverse
from ietf.utils.test_data import make_test_data, make_review_data
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
from ietf.utils.test_utils import login_testing_unauthorized, TestCase, unicontent
import ietf.stats.views
from ietf.submit.models import Submission
from ietf.doc.models import Document, DocAlias, State, RelatedDocument, NewRevisionDocEvent
from ietf.meeting.factories import MeetingFactory
from ietf.person.models import Person
from ietf.name.models import FormalLanguageName, DocRelationshipName, CountryName
from ietf.stats.models import MeetingRegistration, CountryAlias
from ietf.stats.utils import get_meeting_registration_data
class StatisticsTests(TestCase):
def test_stats_index(self):
url = urlreverse(ietf.stats.views.stats_index)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
def test_document_stats(self):
draft = make_test_data()
# create some data for the statistics
Submission.objects.create(
authors=[ { "name": "Some Body", "email": "somebody@example.com", "affiliation": "Some Inc.", "country": "US" }],
pages=30,
rev=draft.rev,
words=4000,
draft=draft,
file_types=".txt",
state_id="posted",
)
draft.formal_languages.add(FormalLanguageName.objects.get(slug="xml"))
Document.objects.filter(pk=draft.pk).update(words=4000)
# move it back so it shows up in the yearly summaries
NewRevisionDocEvent.objects.filter(doc=draft, rev=draft.rev).update(
time=datetime.datetime.now() - datetime.timedelta(days=500))
referencing_draft = Document.objects.create(
name="draft-ietf-mars-referencing",
type_id="draft",
title="Referencing",
stream_id="ietf",
abstract="Test",
rev="00",
pages=2,
words=100
)
referencing_draft.set_state(State.objects.get(used=True, type="draft", slug="active"))
DocAlias.objects.create(document=referencing_draft, name=referencing_draft.name)
RelatedDocument.objects.create(
source=referencing_draft,
target=draft.docalias_set.first(),
relationship=DocRelationshipName.objects.get(slug="refinfo")
)
NewRevisionDocEvent.objects.create(
type="new_revision",
by=Person.objects.get(name="(System)"),
doc=referencing_draft,
desc="New revision available",
rev=referencing_draft.rev,
time=datetime.datetime.now() - datetime.timedelta(days=1000)
)
# check redirect
url = urlreverse(ietf.stats.views.document_stats)
authors_url = urlreverse(ietf.stats.views.document_stats, kwargs={ "stats_type": "authors" })
r = self.client.get(url)
self.assertEqual(r.status_code, 302)
self.assertTrue(authors_url in r["Location"])
# check various stats types
for stats_type in ["authors", "pages", "words", "format", "formlang",
"author/documents", "author/affiliation", "author/country",
"author/continent", "author/citations", "author/hindex",
"yearly/affiliation", "yearly/country", "yearly/continent"]:
for document_type in ["", "rfc", "draft"]:
for time_choice in ["", "5y"]:
url = urlreverse(ietf.stats.views.document_stats, kwargs={ "stats_type": stats_type })
r = self.client.get(url, {
"type": document_type,
"time": time_choice,
})
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('#chart'))
if not stats_type.startswith("yearly"):
self.assertTrue(q('table.stats-data'))
def test_meeting_stats(self):
# create some data for the statistics
make_test_data()
meeting = MeetingFactory(type_id='ietf', date=datetime.date.today(), number="96")
MeetingRegistration.objects.create(first_name='John', last_name='Smith', country_code='US', email="john.smith@example.us", meeting=meeting)
CountryAlias.objects.get_or_create(alias="US", country=CountryName.objects.get(slug="US"))
MeetingRegistration.objects.create(first_name='Jaume', last_name='Guillaume', country_code='FR', email="jaume.guillaume@example.fr", meeting=meeting)
CountryAlias.objects.get_or_create(alias="FR", country=CountryName.objects.get(slug="FR"))
# check redirect
url = urlreverse(ietf.stats.views.meeting_stats)
authors_url = urlreverse(ietf.stats.views.meeting_stats, kwargs={ "stats_type": "overview" })
r = self.client.get(url)
self.assertEqual(r.status_code, 302)
self.assertTrue(authors_url in r["Location"])
# check various stats types
for stats_type in ["overview", "country", "continent"]:
url = urlreverse(ietf.stats.views.meeting_stats, kwargs={ "stats_type": stats_type })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('#chart'))
if stats_type == "overview":
self.assertTrue(q('table.stats-data'))
for stats_type in ["country", "continent"]:
url = urlreverse(ietf.stats.views.meeting_stats, kwargs={ "stats_type": stats_type, "num": meeting.number })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('#chart'))
self.assertTrue(q('table.stats-data'))
def test_known_country_list(self):
make_test_data()
# check redirect
url = urlreverse(ietf.stats.views.known_countries_list)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue("United States" in unicontent(r))
def test_review_stats(self):
doc = make_test_data()
review_req = make_review_data(doc)
@ -56,3 +188,14 @@ class StatisticsTests(TestCase):
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(q('.review-stats td:contains("1")'))
@patch('requests.get')
def test_get_meeting_registration_data(self, mock_get):
response = Response()
response.status_code = 200
response._content = '[{"LastName":"Smith","FirstName":"John","Company":"ABC","Country":"US","Email":"john.doe@example.us"}]'
mock_get.return_value = response
meeting = MeetingFactory(type_id='ietf', date=datetime.date(2016,7,14), number="96")
get_meeting_registration_data(meeting)
query = MeetingRegistration.objects.filter(first_name='John',last_name='Smith',country_code='US')
self.assertTrue(query.count(), 1)

View file

@ -5,5 +5,9 @@ from ietf.utils.urls import url
urlpatterns = [
url("^$", views.stats_index),
url("^document/(?:(?P<stats_type>authors|pages|words|format|formlang|author/(?:documents|affiliation|country|continent|citations|hindex)|yearly/(?:affiliation|country|continent))/)?$", views.document_stats),
url("^knowncountries/$", views.known_countries_list),
url("^meeting/(?P<num>\d+)/(?P<stats_type>country|continent)/$", views.meeting_stats),
url("^meeting/(?:(?P<stats_type>overview|country|continent)/)?$", views.meeting_stats),
url("^review/(?:(?P<stats_type>completion|results|states|time)/)?(?:%(acronym)s/)?$" % settings.URL_REGEXPS, views.review_stats),
]

248
ietf/stats/utils.py Normal file
View file

@ -0,0 +1,248 @@
import re
import requests
from collections import defaultdict
from django.conf import settings
from ietf.stats.models import AffiliationAlias, AffiliationIgnoredEnding, CountryAlias, MeetingRegistration
from ietf.name.models import CountryName
def compile_affiliation_ending_stripping_regexp():
parts = []
for ending_re in AffiliationIgnoredEnding.objects.values_list("ending", flat=True):
try:
re.compile(ending_re)
except re.error:
pass
parts.append(ending_re)
re_str = ",? *({}) *$".format("|".join(parts))
return re.compile(re_str, re.IGNORECASE)
def get_aliased_affiliations(affiliations):
"""Given non-unique sequence of affiliations, returns dictionary with
aliases needed.
We employ the following strategies, interleaved:
- Stripping company endings like Inc., GmbH etc. from database
- Looking up aliases stored directly in the database, like
"Examplar International" -> "Examplar"
- Case-folding so Examplar and EXAMPLAR is merged with the
winner being the one with most occurrences (so input should not
be made unique) or most upper case letters in case of ties.
Case folding can be overridden by the aliases in the database."""
res = {}
ending_re = compile_affiliation_ending_stripping_regexp()
known_aliases = { alias.lower(): name for alias, name in AffiliationAlias.objects.values_list("alias", "name") }
affiliations_with_case_spellings = defaultdict(set)
case_spelling_count = defaultdict(int)
for affiliation in affiliations:
original_affiliation = affiliation
# check aliases from DB
name = known_aliases.get(affiliation.lower())
if name is not None:
affiliation = name
res[original_affiliation] = affiliation
# strip ending
name = ending_re.sub("", affiliation)
if name != affiliation:
affiliation = name
res[original_affiliation] = affiliation
# check aliases from DB
name = known_aliases.get(affiliation.lower())
if name is not None:
affiliation = name
res[original_affiliation] = affiliation
affiliations_with_case_spellings[affiliation.lower()].add(original_affiliation)
case_spelling_count[affiliation] += 1
def affiliation_sort_key(affiliation):
count = case_spelling_count[affiliation]
uppercase_letters = sum(1 for c in affiliation if c.isupper())
return (count, uppercase_letters)
# now we just need to pick the most popular uppercase/lowercase
# spelling for each affiliation with more than one
for similar_affiliations in affiliations_with_case_spellings.itervalues():
if len(similar_affiliations) > 1:
most_popular = sorted(similar_affiliations, key=affiliation_sort_key, reverse=True)[0]
for affiliation in similar_affiliations:
if affiliation != most_popular:
res[affiliation] = most_popular
return res
def get_aliased_countries(countries):
known_aliases = dict(CountryAlias.objects.values_list("alias", "country__name"))
# add aliases for known countries
for slug, name in CountryName.objects.values_list("slug", "name"):
known_aliases[name.lower()] = name
def lookup_alias(possible_alias):
name = known_aliases.get(possible_alias)
if name is not None:
return name
name = known_aliases.get(possible_alias.lower())
if name is not None:
return name
return possible_alias
known_re_aliases = {
re.compile(u"\\b{}\\b".format(re.escape(alias))): name
for alias, name in known_aliases.iteritems()
}
# specific hack: check for zip codes from the US since in the
# early days, the addresses often didn't include the country
us_zipcode_re = re.compile(r"\b(AL|AK|AZ|AR|CA|CO|CT|DE|DC|FL|GA|HI|ID|IL|IN|IA|KS|KY|LA|ME|MD|MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ|NM|NY|NC|ND|OH|OK|OR|PA|RI|SC|SD|TN|TX|UT|VT|VA|WA|WV|WI|WY|AS|GU|MP|PR|VI|UM|FM|MH|PW|Ca|Cal.|California|CALIFORNIA|Colorado|Georgia|Illinois|Ill|Maryland|Ma|Ma.|Mass|Massachuss?etts|Michigan|Minnesota|New Jersey|New York|Ny|N.Y.|North Carolina|NORTH CAROLINA|Ohio|Oregon|Pennsylvania|Tx|Texas|Tennessee|Utah|Vermont|Virginia|Va.|Washington)[., -]*[0-9]{5}\b")
us_country_name = CountryName.objects.get(slug="US").name
def last_text_part_stripped(split):
for t in reversed(split):
t = t.strip()
if t:
return t
return u""
known_countries = set(CountryName.objects.values_list("name", flat=True))
res = {}
for country in countries:
if country in res or country in known_countries:
continue
original_country = country
# aliased name
country = lookup_alias(country)
if country in known_countries:
res[original_country] = country
continue
# contains US zipcode
if us_zipcode_re.search(country):
res[original_country] = us_country_name
continue
# do a little bit of cleanup
if len(country) > 1 and country[-1] == "." and not country[-2].isupper():
country = country.rstrip(".")
country = country.strip("-,").strip()
# aliased name
country = lookup_alias(country)
if country in known_countries:
res[original_country] = country
continue
# country name at end, separated by comma
last_part = lookup_alias(last_text_part_stripped(country.split(",")))
if last_part in known_countries:
res[original_country] = last_part
continue
# country name at end, separated by whitespace
last_part = lookup_alias(last_text_part_stripped(country.split()))
if last_part in known_countries:
res[original_country] = last_part
continue
# country name anywhere
country_lower = country.lower()
found = False
for alias_re, name in known_re_aliases.iteritems():
if alias_re.search(country) or alias_re.search(country_lower):
res[original_country] = name
found = True
break
if found:
continue
# unknown country
res[original_country] = ""
return res
def clean_country_name(country_name):
if country_name:
country_name = get_aliased_countries([country_name]).get(country_name, country_name)
if country_name and CountryName.objects.filter(name=country_name).exists():
return country_name
return ""
def compute_hirsch_index(citation_counts):
"""Computes the h-index given a sequence containing the number of
citations for each document."""
i = 0
for count in sorted(citation_counts, reverse=True):
if i + 1 > count:
break
i += 1
return i
def get_meeting_registration_data(meeting):
""""Retrieve registration attendee data and summary statistics. Returns number
of Registration records created."""
num_created = 0
num_processed = 0
response = requests.get(settings.REGISTRATION_ATTENDEES_BASE_URL + meeting.number)
if response.status_code == 200:
decoded = []
try:
decoded = response.json()
except ValueError:
if response.content.strip() == 'Invalid meeting':
pass
else:
raise RuntimeError("Could not decode response from registrations API: '%s...'" % (response.content[:64], ))
for registration in decoded:
object, created = MeetingRegistration.objects.get_or_create(
meeting_id=meeting.pk,
first_name=registration['FirstName'],
last_name=registration['LastName'],
affiliation=registration['Company'],
country_code=registration['Country'],
email=registration['Email'],
)
if created:
num_created += 1
num_processed += 1
else:
raise RuntimeError("Bad response from registrations API: %s, '%s'" % (response.status_code, response.content))
num_total = MeetingRegistration.objects.filter(meeting_id=meeting.pk).count()
return num_created, num_processed, num_total

File diff suppressed because it is too large Load diff

View file

@ -5,6 +5,7 @@ import email
import pytz
import xml2rfc
import tempfile
from email.utils import formataddr
from unidecode import unidecode
from django import forms
@ -21,6 +22,7 @@ from ietf.doc.fields import SearchableDocAliasesField
from ietf.ipr.mail import utc_from_string
from ietf.meeting.models import Meeting
from ietf.message.models import Message
from ietf.name.models import FormalLanguageName
from ietf.submit.models import Submission, Preapproval
from ietf.submit.utils import validate_submission_rev, validate_submission_document_date
from ietf.submit.parsers.pdf_parser import PDFParser
@ -29,7 +31,6 @@ from ietf.submit.parsers.ps_parser import PSParser
from ietf.submit.parsers.xml_parser import XMLParser
from ietf.utils.draft import Draft
class SubmissionUploadForm(forms.Form):
txt = forms.FileField(label=u'.txt format', required=False)
xml = forms.FileField(label=u'.xml format', required=False)
@ -177,18 +178,14 @@ class SubmissionUploadForm(forms.Form):
self.abstract = self.xmlroot.findtext('front/abstract').strip()
if type(self.abstract) is unicode:
self.abstract = unidecode(self.abstract)
self.author_list = []
author_info = self.xmlroot.findall('front/author')
for author in author_info:
author_dict = dict(
company = author.findtext('organization').strip(),
last_name = author.attrib.get('surname').strip(),
full_name = author.attrib.get('fullname').strip(),
email = author.findtext('address/email').strip(),
)
self.author_list.append(author_dict)
line = email.utils.formataddr((author_dict['full_name'], author_dict['email']))
self.authors.append(line)
self.authors.append({
"name": author.attrib.get('fullname').strip(),
"email": author.findtext('address/email').strip(),
"affiliation": author.findtext('organization').strip(),
"country": author.findtext('address/postal/country').strip(),
})
except forms.ValidationError:
raise
except Exception as e:
@ -324,18 +321,12 @@ class SubmissionUploadForm(forms.Form):
return None
class NameEmailForm(forms.Form):
"""For validating supplied submitter and author information."""
name = forms.CharField(required=True)
email = forms.EmailField(label=u'Email address')
#Fields for secretariat only
approvals_received = forms.BooleanField(label=u'Approvals received', required=False, initial=False)
email = forms.EmailField(label=u'Email address', required=True)
def __init__(self, *args, **kwargs):
email_required = kwargs.pop("email_required", True)
super(NameEmailForm, self).__init__(*args, **kwargs)
self.fields["email"].required = email_required
self.fields["name"].widget.attrs["class"] = "name"
self.fields["email"].widget.attrs["class"] = "email"
@ -345,11 +336,23 @@ class NameEmailForm(forms.Form):
def clean_email(self):
return self.cleaned_data["email"].replace("\n", "").replace("\r", "").replace("<", "").replace(">", "").strip()
class AuthorForm(NameEmailForm):
affiliation = forms.CharField(max_length=100, required=False)
country = forms.CharField(max_length=255, required=False)
def __init__(self, *args, **kwargs):
super(AuthorForm, self).__init__(*args, **kwargs)
self.fields["email"].required = False
class SubmitterForm(NameEmailForm):
#Fields for secretariat only
approvals_received = forms.BooleanField(label=u'Approvals received', required=False, initial=False)
def cleaned_line(self):
line = self.cleaned_data["name"]
email = self.cleaned_data.get("email")
if email:
line += u" <%s>" % email
line = formataddr((line, email))
return line
class ReplacesForm(forms.Form):
@ -376,13 +379,14 @@ class EditSubmissionForm(forms.ModelForm):
rev = forms.CharField(label=u'Revision', max_length=2, required=True)
document_date = forms.DateField(required=True)
pages = forms.IntegerField(required=True)
formal_languages = forms.ModelMultipleChoiceField(queryset=FormalLanguageName.objects.filter(used=True), widget=forms.CheckboxSelectMultiple, required=False)
abstract = forms.CharField(widget=forms.Textarea, required=True, strip=False)
note = forms.CharField(label=mark_safe(u'Comment to the Secretariat'), widget=forms.Textarea, required=False, strip=False)
class Meta:
model = Submission
fields = ['title', 'rev', 'document_date', 'pages', 'abstract', 'note']
fields = ['title', 'rev', 'document_date', 'pages', 'formal_languages', 'abstract', 'note']
def clean_rev(self):
rev = self.cleaned_data["rev"]

View file

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('name', '0021_add_formlang_names'),
('submit', '0018_fix_more_bad_submission_docevents'),
]
operations = [
migrations.AddField(
model_name='submission',
name='formal_languages',
field=models.ManyToManyField(help_text=b'Formal languages used in document', to='name.FormalLanguageName', blank=True),
),
migrations.AddField(
model_name='submission',
name='words',
field=models.IntegerField(null=True, blank=True),
),
]

View file

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
import jsonfield.fields
def parse_email_line(line):
"""Split line on the form 'Some Name <email@example.com>'"""
import re
m = re.match("([^<]+) <([^>]+)>$", line)
if m:
return dict(name=m.group(1), email=m.group(2))
else:
return dict(name=line, email="")
def parse_authors(author_lines):
res = []
for line in author_lines.replace("\r", "").split("\n"):
line = line.strip()
if line:
res.append(parse_email_line(line))
return res
def convert_author_lines_to_json(apps, schema_editor):
import json
Submission = apps.get_model("submit", "Submission")
for s in Submission.objects.all().iterator():
Submission.objects.filter(pk=s.pk).update(authors=json.dumps(parse_authors(s.authors)))
class Migration(migrations.Migration):
dependencies = [
('submit', '0019_add_formal_languages_and_words'),
]
operations = [
migrations.RunPython(convert_author_lines_to_json, migrations.RunPython.noop),
migrations.AlterField(
model_name='submission',
name='authors',
field=jsonfield.fields.JSONField(default=list, help_text=b'List of authors with name, email, affiliation and country.'),
),
]

View file

@ -10,7 +10,7 @@ from ietf.doc.models import Document
from ietf.person.models import Person
from ietf.group.models import Group
from ietf.message.models import Message
from ietf.name.models import DraftSubmissionStateName
from ietf.name.models import DraftSubmissionStateName, FormalLanguageName
from ietf.utils.accesstoken import generate_random_key, generate_access_token
@ -36,7 +36,10 @@ class Submission(models.Model):
abstract = models.TextField(blank=True)
rev = models.CharField(max_length=3, blank=True)
pages = models.IntegerField(null=True, blank=True)
authors = models.TextField(blank=True, help_text="List of author names and emails, one author per line, e.g. \"John Doe &lt;john@example.org&gt;\".")
words = models.IntegerField(null=True, blank=True)
formal_languages = models.ManyToManyField(FormalLanguageName, blank=True, help_text="Formal languages used in document")
authors = jsonfield.JSONField(default=list, help_text="List of authors with name, email, affiliation and country.")
note = models.TextField(blank=True)
replaces = models.CharField(max_length=1000, blank=True)
@ -53,20 +56,6 @@ class Submission(models.Model):
def __unicode__(self):
return u"%s-%s" % (self.name, self.rev)
def authors_parsed(self):
if not hasattr(self, '_cached_authors_parsed'):
from ietf.submit.utils import ensure_person_email_info_exists
res = []
for line in self.authors.replace("\r", "").split("\n"):
line = line.strip()
if line:
parsed = parse_email_line(line)
if not parsed["email"]:
parsed["email"] = ensure_person_email_info_exists(**parsed).address
res.append(parsed)
self._cached_authors_parsed = res
return self._cached_authors_parsed
def submitter_parsed(self):
return parse_email_line(self.submitter)

View file

@ -61,8 +61,10 @@ Internet-Draft Testing Tests %(month)s %(year)s
Table of Contents
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 2
2. Security Considerations . . . . . . . . . . . . . . . . . . . 2
3. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 2
2. Yang . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
3. JSON example . . . . . . . . . . . . . . . . . . . . . . . . 2
4. Security Considerations . . . . . . . . . . . . . . . . . . . 2
5. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 2
Author's Address . . . . . . . . . . . . . . . . . . . . . . . . 2
1. Introduction
@ -169,11 +171,19 @@ Table of Contents
<CODE ENDS>
3. Security Considerations
3. JSON example
The JSON object should look like this:
{
"test": 1234
}
4. Security Considerations
There are none.
4. IANA Considerations
5. IANA Considerations
No new registrations for IANA.

View file

@ -137,6 +137,15 @@ module ietf-mpls {
</figure>
</section>
<section anchor="JSON" title="JSON example">
<t>
The JSON object should look like this:
{
"test": 1234
}
</t>
</section>
<section anchor="Security" title="Security Considerations">
<t>
There are none.

View file

@ -18,7 +18,8 @@ from ietf.group.models import Group
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.person.models import Person, Email
from ietf.name.models import FormalLanguageName
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
@ -48,7 +49,6 @@ def submission_file(name, rev, group, format, templatename, author=None):
surname=author.last_name(),
email=author.email().address.lower(),
)
file = StringIO(submission_text)
file.name = "%s-%s.%s" % (name, rev, format)
return file
@ -118,7 +118,7 @@ class SubmitTests(TestCase):
q = PyQuery(r.content)
print(q('div.has-error div.alert').text())
self.assertEqual(r.status_code, 302)
self.assertNoFormPostErrors(r, ".has-error,.alert-danger")
status_url = r["Location"]
for format in formats:
@ -126,10 +126,12 @@ class SubmitTests(TestCase):
self.assertEqual(Submission.objects.filter(name=name).count(), 1)
submission = Submission.objects.get(name=name)
self.assertTrue(all([ c.passed!=False for c in submission.checks.all() ]))
self.assertEqual(len(submission.authors_parsed()), 1)
author = submission.authors_parsed()[0]
self.assertEqual(len(submission.authors), 1)
author = submission.authors[0]
self.assertEqual(author["name"], "Author Name")
self.assertEqual(author["email"], "author@example.com")
self.assertEqual(author["affiliation"], "Test Centre Inc.")
self.assertEqual(author["country"], "UK")
return status_url
@ -151,7 +153,7 @@ class SubmitTests(TestCase):
if r.status_code == 302:
submission = Submission.objects.get(name=name)
self.assertEqual(submission.submitter, u"%s <%s>" % (submitter_name, submitter_email))
self.assertEqual(submission.submitter, email.utils.formataddr((submitter_name, submitter_email)))
self.assertEqual(submission.replaces, ",".join(d.name for d in DocAlias.objects.filter(pk__in=replaces.split(",") if replaces else [])))
return r
@ -184,6 +186,7 @@ class SubmitTests(TestCase):
abstract="Blahblahblah.",
rev="01",
pages=2,
words=100,
intended_std_level_id="ps",
ad=draft.ad,
expires=datetime.datetime.now() + datetime.timedelta(days=settings.INTERNET_DRAFT_DAYS_TO_EXPIRE),
@ -241,9 +244,11 @@ 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)
self.assertEqual(draft.relations_that_doc("possibly-replaces").count(), 1)
@ -281,12 +286,12 @@ class SubmitTests(TestCase):
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, 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]
@ -334,7 +339,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"])
@ -415,9 +420,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)
@ -654,7 +660,7 @@ class SubmitTests(TestCase):
"authors-prefix": ["authors-", "authors-0", "authors-1", "authors-2"],
})
self.assertEqual(r.status_code, 302)
self.assertNoFormPostErrors(r, ".has-error,.alert-danger")
submission = Submission.objects.get(name=name)
self.assertEqual(submission.title, "some title")
@ -666,14 +672,14 @@ class SubmitTests(TestCase):
self.assertEqual(submission.replaces, draft.docalias_set.all().first().name)
self.assertEqual(submission.state_id, "manual")
authors = submission.authors_parsed()
authors = submission.authors
self.assertEqual(len(authors), 3)
self.assertEqual(authors[0]["name"], "Person 1")
self.assertEqual(authors[0]["email"], "person1@example.com")
self.assertEqual(authors[1]["name"], "Person 2")
self.assertEqual(authors[1]["email"], "person2@example.com")
self.assertEqual(authors[2]["name"], "Person 3")
self.assertEqual(authors[2]["email"], "unknown-email-Person-3")
self.assertEqual(authors[2]["email"], "")
self.assertEqual(len(outbox), mailbox_before + 1)
self.assertTrue("Manual Post Requested" in outbox[-1]["Subject"])
@ -929,7 +935,6 @@ class SubmitTests(TestCase):
files = {"txt": submission_file(name, rev, group, "txt", "test_submission.nonascii", author=author) }
r = self.client.post(url, files)
self.assertEqual(r.status_code, 302)
status_url = r["Location"]
r = self.client.get(status_url)
@ -1433,8 +1438,8 @@ Subject: test
self.assertEqual(Submission.objects.filter(name=name).count(), 1)
submission = Submission.objects.get(name=name)
self.assertTrue(all([ c.passed!=False for c in submission.checks.all() ]))
self.assertEqual(len(submission.authors_parsed()), 1)
author = submission.authors_parsed()[0]
self.assertEqual(len(submission.authors), 1)
author = submission.authors[0]
self.assertEqual(author["name"], "Author Name")
self.assertEqual(author["email"], "author@example.com")
@ -1459,6 +1464,6 @@ Subject: test
if r.status_code == 302:
submission = Submission.objects.get(name=name)
self.assertEqual(submission.submitter, u"%s <%s>" % (submitter_name, submitter_email))
self.assertEqual(submission.submitter, email.utils.formataddr((submitter_name, submitter_email)))
return r

View file

@ -47,7 +47,7 @@ def validate_submission(submission):
if not submission.abstract:
errors['abstract'] = 'Abstract is empty or was not found'
if not submission.authors_parsed():
if not submission.authors:
errors['authors'] = 'No authors found'
# revision
@ -130,7 +130,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
@ -184,7 +184,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
@ -258,6 +258,8 @@ def post_submission(request, submission, approvedDesc):
update_authors(draft, submission)
draft.formal_languages = submission.formal_languages.all()
trouble = rebuild_reference_relations(draft, filename=os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (submission.name, submission.rev)))
if trouble:
log.log('Rebuild_reference_relations trouble: %s'%trouble)
@ -344,10 +346,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:
@ -434,24 +435,27 @@ def ensure_person_email_info_exists(name, email):
email.time = datetime.datetime.now()
email.save()
return email
return person, email
def update_authors(draft, submission):
authors = []
for order, author in enumerate(submission.authors_parsed()):
email = ensure_person_email_info_exists(author["name"], author["email"])
persons = []
for order, author in enumerate(submission.authors):
person, email = ensure_person_email_info_exists(author["name"], author.get("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.affiliation = author.get("affiliation") or ""
a.country = author.get("country") or ""
a.order = order
a.save()
log.assertion('a.author_id != "none"')
log.assertion('a.email_id != "none"')
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

@ -3,6 +3,7 @@ import base64
import datetime
import os
import xml2rfc
import optparse
from django.conf import settings
from django.contrib import messages
@ -20,7 +21,8 @@ from ietf.group.models import Group
from ietf.ietfauth.utils import has_role, role_required
from ietf.mailtrigger.utils import gather_address_lists
from ietf.message.models import Message, MessageAttachment
from ietf.submit.forms import ( SubmissionUploadForm, NameEmailForm, EditSubmissionForm,
from ietf.name.models import FormalLanguageName
from ietf.submit.forms import ( SubmissionUploadForm, AuthorForm, SubmitterForm, EditSubmissionForm,
PreapprovalForm, ReplacesForm, SubmissionEmailForm, MessageModelForm )
from ietf.submit.mail import ( send_full_url, send_approval_request_to_group,
send_submission_confirmation, send_manual_post_request, add_submission_email, get_reply_to )
@ -30,6 +32,7 @@ from ietf.submit.utils import ( approvable_submissions_for_user, preapprovals_fo
recently_approved_by_user, validate_submission, create_submission_event,
docevent_from_submission, post_submission, cancel_submission, rename_submission_files,
get_person_from_name_email )
from ietf.stats.utils import clean_country_name
from ietf.utils.accesstoken import generate_random_key, generate_access_token
from ietf.utils.draft import Draft
from ietf.utils.log import log
@ -60,7 +63,7 @@ def upload_submission(request):
if not form.cleaned_data['txt']:
file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (form.filename, form.revision))
try:
pagedwriter = xml2rfc.PaginatedTextRfcWriter(form.xmltree, quiet=True)
pagedwriter = xml2rfc.PaginatedTextRfcWriter(form.xmltree, options=optparse.Values(defaults=dict(quiet=True, verbose=False, utf8=False)))
pagedwriter.write(file_name['txt'])
except Exception as e:
raise ValidationError("Error from xml2rfc: %s" % e)
@ -80,10 +83,9 @@ def upload_submission(request):
# If we don't have an xml file, try to extract the
# relevant information from the text file
for author in form.parsed_draft.get_author_list():
full_name, first_name, middle_initial, last_name, name_suffix, email, company = author
full_name, first_name, middle_initial, last_name, name_suffix, email, country, company = author
line = full_name.replace("\n", "").replace("\r", "").replace("<", "").replace(">", "").strip()
email = (email or "").strip()
name = full_name.replace("\n", "").replace("\r", "").replace("<", "").replace(">", "").strip()
if email:
try:
@ -91,29 +93,31 @@ def upload_submission(request):
except ValidationError:
email = ""
if email:
# Try various ways of handling name and email, in order to avoid
# triggering a 500 error here. If the document contains non-ascii
# characters, it will be flagged later by the idnits check.
try:
line += u" <%s>" % email
except UnicodeDecodeError:
def turn_into_unicode(s):
if s is None:
return u""
if isinstance(s, unicode):
return s
else:
try:
line = line.decode('utf-8')
email = email.decode('utf-8')
line += u" <%s>" % email
return s.decode("utf-8")
except UnicodeDecodeError:
try:
line = line.decode('latin-1')
email = email.decode('latin-1')
line += u" <%s>" % email
return s.decode("latin-1")
except UnicodeDecodeError:
try:
line += " <%s>" % email
except UnicodeDecodeError:
pass
return ""
authors.append(line)
name = turn_into_unicode(name)
email = turn_into_unicode(email)
company = turn_into_unicode(company)
authors.append({
"name": name,
"email": email,
"affiliation": company,
"country": country
})
if form.abstract:
abstract = form.abstract
@ -124,55 +128,39 @@ def upload_submission(request):
# for this revision.
# If so - we're going to update it otherwise we create a new object
submission = Submission.objects.filter(name=form.filename,
rev=form.revision,
state_id = "waiting-for-draft").distinct()
if (len(submission) == 0):
submission = None
elif (len(submission) == 1):
submission = submission[0]
submission.state = DraftSubmissionStateName.objects.get(slug="uploaded")
submission.remote_ip=form.remote_ip
submission.title=form.title
submission.abstract=abstract
submission.rev=form.revision
submission.pages=form.parsed_draft.get_pagecount()
submission.authors="\n".join(authors)
submission.first_two_pages=''.join(form.parsed_draft.pages[:2])
submission.file_size=file_size
submission.file_types=','.join(form.file_types)
submission.submission_date=datetime.date.today()
submission.document_date=form.parsed_draft.get_creation_date()
submission.replaces=""
submission.save()
submissions = Submission.objects.filter(name=form.filename,
rev=form.revision,
state_id = "waiting-for-draft").distinct()
if not submissions:
submission = Submission(name=form.filename, rev=form.revision, group=form.group)
elif len(submissions) == 1:
submission = submissions[0]
else:
raise Exception("Multiple submissions found waiting for upload")
if (submission == None):
try:
submission = Submission.objects.create(
state=DraftSubmissionStateName.objects.get(slug="uploaded"),
remote_ip=form.remote_ip,
name=form.filename,
group=form.group,
title=form.title,
abstract=abstract,
rev=form.revision,
pages=form.parsed_draft.get_pagecount(),
authors="\n".join(authors),
note="",
first_two_pages=''.join(form.parsed_draft.pages[:2]),
file_size=file_size,
file_types=','.join(form.file_types),
submission_date=datetime.date.today(),
document_date=form.parsed_draft.get_creation_date(),
replaces="",
)
except Exception as e:
log("Exception: %s\n" % e)
raise
try:
submission.state = DraftSubmissionStateName.objects.get(slug="uploaded")
submission.remote_ip = form.remote_ip
submission.title = form.title
submission.abstract = abstract
submission.pages = form.parsed_draft.get_pagecount()
submission.words = form.parsed_draft.get_wordcount()
submission.authors = authors
submission.first_two_pages = ''.join(form.parsed_draft.pages[:2])
submission.file_size = file_size
submission.file_types = ','.join(form.file_types)
submission.submission_date = datetime.date.today()
submission.document_date = form.parsed_draft.get_creation_date()
submission.replaces = ""
submission.save()
submission.formal_languages = FormalLanguageName.objects.filter(slug__in=form.parsed_draft.get_formal_languages())
except Exception as e:
log("Exception: %s\n" % e)
raise
# run submission checkers
def apply_check(submission, checker, method, fn):
@ -274,8 +262,8 @@ 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() ]
new_authors = [ get_person_from_name_email(**p) for p in submission.authors_parsed() ]
old_authors = [ author.person for author in doc.documentauthor_set.all() ]
new_authors = [ get_person_from_name_email(author["name"], author.get("email")) for author in submission.authors ]
group_authors_changed = set(old_authors)!=set(new_authors)
message = None
@ -290,7 +278,7 @@ def submission_status(request, submission_id, access_token=None):
message = ('success', 'The submission is pending approval by the authors of the previous version. An email has been sent to: %s' % ", ".join(confirmation_list))
submitter_form = NameEmailForm(initial=submission.submitter_parsed(), prefix="submitter")
submitter_form = SubmitterForm(initial=submission.submitter_parsed(), prefix="submitter")
replaces_form = ReplacesForm(name=submission.name,initial=DocAlias.objects.filter(name__in=submission.replaces.split(",")))
if request.method == 'POST':
@ -299,7 +287,7 @@ def submission_status(request, submission_id, access_token=None):
if not can_edit:
return HttpResponseForbidden("You do not have permission to perform this action")
submitter_form = NameEmailForm(request.POST, prefix="submitter")
submitter_form = SubmitterForm(request.POST, prefix="submitter")
replaces_form = ReplacesForm(request.POST, name=submission.name)
validations = [submitter_form.is_valid(), replaces_form.is_valid()]
if all(validations):
@ -415,6 +403,9 @@ def submission_status(request, submission_id, access_token=None):
# something went wrong, turn this into a GET and let the user deal with it
return HttpResponseRedirect("")
for author in submission.authors:
author["cleaned_country"] = clean_country_name(author.get("country"))
return render(request, 'submit/submission_status.html', {
'selected': 'status',
'submission': submission,
@ -447,7 +438,7 @@ def edit_submission(request, submission_id, access_token=None):
# submission itself, one for the submitter, and a list of forms
# for the authors
empty_author_form = NameEmailForm(email_required=False)
empty_author_form = AuthorForm()
if request.method == 'POST':
# get a backup submission now, the model form may change some
@ -455,21 +446,23 @@ def edit_submission(request, submission_id, access_token=None):
prev_submission = Submission.objects.get(pk=submission.pk)
edit_form = EditSubmissionForm(request.POST, instance=submission, prefix="edit")
submitter_form = NameEmailForm(request.POST, prefix="submitter")
submitter_form = SubmitterForm(request.POST, prefix="submitter")
replaces_form = ReplacesForm(request.POST,name=submission.name)
author_forms = [ NameEmailForm(request.POST, email_required=False, prefix=prefix)
author_forms = [ AuthorForm(request.POST, prefix=prefix)
for prefix in request.POST.getlist("authors-prefix")
if prefix != "authors-" ]
# trigger validation of all forms
validations = [edit_form.is_valid(), submitter_form.is_valid(), replaces_form.is_valid()] + [ f.is_valid() for f in author_forms ]
if all(validations):
changed_fields = []
submission.submitter = submitter_form.cleaned_line()
replaces = replaces_form.cleaned_data.get("replaces", [])
submission.replaces = ",".join(o.name for o in replaces)
submission.authors = "\n".join(f.cleaned_line() for f in author_forms)
if hasattr(submission, '_cached_authors_parsed'):
del submission._cached_authors_parsed
submission.authors = [ { attr: f.cleaned_data.get(attr) or ""
for attr in ["name", "email", "affiliation", "country"] }
for f in author_forms ]
edit_form.save(commit=False) # transfer changes
if submission.rev != prev_submission.rev:
@ -478,12 +471,18 @@ def edit_submission(request, submission_id, access_token=None):
submission.state = DraftSubmissionStateName.objects.get(slug="manual")
submission.save()
formal_languages_changed = False
if set(submission.formal_languages.all()) != set(edit_form.cleaned_data["formal_languages"]):
submission.formal_languages = edit_form.cleaned_data["formal_languages"]
formal_languages_changed = True
send_manual_post_request(request, submission, errors)
changed_fields = [
changed_fields += [
submission._meta.get_field(f).verbose_name
for f in list(edit_form.fields.keys()) + ["submitter", "authors"]
if getattr(submission, f) != getattr(prev_submission, f)
if (f == "formal_languages" and formal_languages_changed)
or getattr(submission, f) != getattr(prev_submission, f)
]
if changed_fields:
@ -498,10 +497,10 @@ def edit_submission(request, submission_id, access_token=None):
form_errors = True
else:
edit_form = EditSubmissionForm(instance=submission, prefix="edit")
submitter_form = NameEmailForm(initial=submission.submitter_parsed(), prefix="submitter")
submitter_form = SubmitterForm(initial=submission.submitter_parsed(), prefix="submitter")
replaces_form = ReplacesForm(name=submission.name,initial=DocAlias.objects.filter(name__in=submission.replaces.split(",")))
author_forms = [ NameEmailForm(initial=author, email_required=False, prefix="authors-%s" % i)
for i, author in enumerate(submission.authors_parsed()) ]
author_forms = [ AuthorForm(initial=author, prefix="authors-%s" % i)
for i, author in enumerate(submission.authors) ]
return render(request, 'submit/edit_submission.html',
{'selected': 'status',

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://datatracker.ietf.org/doc/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

@ -572,13 +572,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

@ -1,6 +1,6 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin staticfiles %}
{% load origin staticfiles ietf_filters %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
@ -44,8 +44,8 @@
{% with ref.source.canonical_name as name %}
<tr>
<td>
<a href="{% url 'ietf.doc.views_doc.document_main' name=name %}">{{name}}</a>
{% if ref.target.name != alias_name %}
<a href="{% url 'ietf.doc.views_doc.document_main' name=name %}">{{ name|prettystdname }}</a>
{% if ref.target.name != alias_name %}
<br><span class="label label-info">As {{ref.target.name}}</span>
{% endif %}
</td>

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{# Copyright The IETF Trust 2015, All Rights Reserved #}
{% load origin staticfiles %}
{% load origin staticfiles ietf_filters %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "jquery.tablesorter/css/theme.bootstrap.min.css" %}">
@ -35,7 +35,7 @@
{% for ref in refs %}
{% with ref.target.name as name %}
<tr>
<td><a href="{% url 'ietf.doc.views_doc.document_main' name=name %}">{{name}}</a></td>
<td><a href="{% url 'ietf.doc.views_doc.document_main' name=name %}">{{ name|prettystdname }}</a></td>
<td>
<b>{{ref.target.document.title}}</b><br>
<a class="btn btn-default btn-xs" href="{% url 'ietf.doc.views_doc.document_references' name %}">Refs</a>

View file

@ -0,0 +1,78 @@
{% extends "base.html" %}
{% load origin %}
{% load ietf_filters staticfiles bootstrap3 %}
{% block title %}{{ stats_title }}{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>Draft/RFC statistics</h1>
<div class="stats-options well">
<div>
Documents:
<div class="btn-group">
{% for slug, label, url in possible_document_stats_types %}
<a class="btn btn-default {% if slug == stats_type %}active{% endif %}" href="{{ url }}">{{ label }}</a>
{% endfor %}
</div>
</div>
<div>
Authors:
<div class="btn-group">
{% for slug, label, url in possible_author_stats_types %}
<a class="btn btn-default {% if slug == stats_type %}active{% endif %}" href="{{ url }}">{{ label }}</a>
{% endfor %}
</div>
</div>
<div>
Yearly:
<div class="btn-group">
{% for slug, label, url in possible_yearly_stats_types %}
<a class="btn btn-default {% if slug == stats_type %}active{% endif %}" href="{{ url }}">{{ label }}</a>
{% endfor %}
</div>
</div>
<h5>Options</h5>
<div>
Document type:
<div style="margin-right:3em;" class="btn-group">
{% for slug, label, url in possible_document_types %}
<a class="btn btn-default {% if slug == document_type %}active{% endif %}" href="{{ url }}">{{ label }}</a>
{% endfor %}
</div>
Time:
<div class="btn-group">
{% for slug, label, url in possible_time_choices %}
<a class="btn btn-default {% if slug == time_choice %}active{% endif %}" href="{{ url }}">{{ label }}</a>
{% endfor %}
</div>
</div>
</div>
<div class="document-stats">
{% include content_template %}
</div>
{% endblock %}
{% block js %}
<script src="{% static 'highcharts/highcharts.js' %}"></script>
<script src="{% static 'highcharts/modules/exporting.js' %}"></script>
<script src="{% static 'highcharts/modules/offline-exporting.js' %}"></script>
<script src="{% static 'ietf/js/stats.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,107 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
type: "category",
title: {
text: 'Affiliation'
}
},
yAxis: {
title: {
text: 'Number of authors'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.points[0].key + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Affiliation</th>
<th>Percentage of authors</th>
<th>Authors</th>
</tr>
</thead>
<tbody>
{% for affiliation, percentage, count, names in table_data %}
<tr>
<td>{{ affiliation|default:"(unknown)" }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>The statistics are based entirely on the author affiliation
provided with each draft. Since this may vary across documents, an
author may be counted with more than one affiliation, making the
total sum more than 100%.</p>
<h3>Affiliation Aliases</h3>
<p>In generating the above statistics, some heuristics have been
applied to determine the affiliations of each author.</p>
{% if request.GET.showaliases %}
<p><a href="{{ hide_aliases_url }}" class="btn btn-default">Hide generated aliases</a></p>
{% if request.user.is_staff %}
<p>Note: since you're an admin, you can <a href="{% url "admin:stats_affiliationalias_add" %}">add an extra known alias</a> or see the <a href="{% url "admin:stats_affiliationalias_changelist" %}">existing known aliases</a> and <a href="{% url "admin:stats_affiliationignoredending_changelist" %}">generally ignored endings</a>.</p>
{% endif %}
{% if alias_data %}
<table class="table table-condensed">
<thead>
<th>Affiliation</th>
<th>Alias</th>
</thead>
{% for name, alias in alias_data %}
<tr>
<td>
{% ifchanged %}
{{ name|default:"(unknown)" }}
{% endifchanged %}
</td>
<td>{{ alias }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% else %}
<p><a href="{{ show_aliases_url }}" class="btn btn-default">Show generated aliases</a></p>
{% endif %}

View file

@ -0,0 +1,71 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'area'
},
plotOptions: {
area: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
title: {
text: 'Number of citations of {{ doc_label }}s by author'
},
max: 500
},
yAxis: {
title: {
text: 'Percentage of authors'
},
labels: {
formatter: function () {
return this.value + '%';
}
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.x + ' ' + (this.x == 1 ? "citation" : 'citations') + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y.toFixed(1) + '%';
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Citations</th>
<th>Percentage of authors</th>
<th>Authors</th>
</tr>
</thead>
<tbody>
{% for citations, percentage, count, names in table_data %}
<tr>
<td>{{ citations }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" with content_limit=10 %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Note that the citation counts do not exclude self-references.</p>

View file

@ -0,0 +1,69 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
type: "category",
title: {
text: 'Continent'
}
},
yAxis: {
title: {
text: 'Number of authors'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.points[0].key + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Continent</th>
<th>Percentage of authors</th>
<th>Authors</th>
</tr>
</thead>
<tbody>
{% for continent, percentage, count, names in table_data %}
<tr>
<td>{{ continent|default:"(unknown)" }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>The statistics are based entirely on the author addresses provided
with each draft. Since this varies across documents, a travelling
author may be counted in more than country, making the total sum
more than 100%.</p>

View file

@ -0,0 +1,130 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
type: "category",
title: {
text: 'Country'
}
},
yAxis: {
title: {
text: 'Number of authors'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.points[0].key + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Country</th>
<th>Percentage of authors</th>
<th>Authors</th>
</tr>
</thead>
<tbody>
{% for country, percentage, count, names in table_data %}
<tr>
<td>{{ country|default:"(unknown)" }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>The statistics are based entirely on the author addresses provided
with each draft. Since this varies across documents, a travelling
author may be counted in more than country, making the total sum
more than 100%.</p>
<p>In case no country information is found for an author in the time
period, the author is counted as (unknown).</p>
<p>EU (European Union) is not a country, but has been added for reference, as the sum of
all current EU member countries:
{% for c in eu_countries %}{{ c.name }}{% if not forloop.last %}, {% endif %}{% endfor %}.</p>
<h3>Country Aliases</h3>
<p>In generating the above statistics, some heuristics have been
applied to figure out which country each author is from.</p>
{% if request.GET.showaliases %}
<p><a href="{{ hide_aliases_url }}" class="btn btn-default">Hide generated aliases</a></p>
{% if request.user.is_staff %}
<p>Note: since you're an admin, some extra links are visible. You
can either correct a document author entry directly in case the
information is obviously missing or add an alias if an unknown
<a href="{% url "admin:name_countryname_changelist" %}">country name</a>
is being used.
</p>
{% endif %}
{% if alias_data %}
<table class="table table-condensed">
<thead>
<th>Country</th>
<th>Alias</th>
<th></th>
</thead>
{% for name, alias, country in alias_data %}
<tr>
<td>
{% ifchanged %}
{% if country and request.user.is_staff %}
<a href="{% url "admin:name_countryname_change" country.pk %}">
{% endif %}
{{ name|default:"(unknown)" }}
{% if country and request.user.is_staff %}
</a>
{% endif %}
{% endifchanged %}
</td>
<td>{{ alias }}</td>
<td>
{% if request.user.is_staff and name != "EU" %}
<a href="{% url "admin:doc_documentauthor_changelist" %}?country={{ alias|urlencode }}">Matching authors</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% endif %}
{% else %}
<p><a href="{{ show_aliases_url }}" class="btn btn-default">Show generated aliases</a></p>
{% endif %}

View file

@ -0,0 +1,70 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
tickInterval: 1,
title: {
text: 'Author of number of {{ doc_label }}s'
},
max: 20
},
yAxis: {
title: {
text: 'Percentage of authors'
},
labels: {
formatter: function () {
return this.value + '%';
}
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.x + ' ' + (this.x == 1 ? "{{ doc_label }}" : '{{ doc_label }}s') + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y.toFixed(1) + '%';
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Documents</th>
<th>Percentage of authors</th>
<th>Authors</th>
</tr>
</thead>
<tbody>
{% for document_count, percentage, count, names in table_data %}
<tr>
<td>{{ document_count }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" with content_limit=10 %}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,79 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
tickInterval: 1,
title: {
text: 'h-index of {{ doc_label }}s by author'
}
},
yAxis: {
title: {
text: 'Percentage of authors'
},
labels: {
formatter: function () {
return this.value + '%';
}
}
},
tooltip: {
formatter: function () {
var s = '<b>' + ' h-index ' + this.x + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y.toFixed(1) + '%';
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>h-index</th>
<th>Percentage of authors</th>
<th>Authors</th>
</tr>
</thead>
<tbody>
{% for h_index, percentage, count, names in table_data %}
<tr>
<td>{{ h_index }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" with content_limit=25 %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>Hirsch index or h-index is a
<a href="https://www.wikipedia.org/wiki/H-index">measure of the
productivity and impact of the publications of an author</a>. An
author with an h-index of 5 has had 5 publications each cited at
least 5 times - to increase the index to 6, the 5 publications plus
1 more would have to have been cited at least 6 times, each. Thus a
high h-index requires many highly-cited publications.</p>
<p>Note that the h-index calculations do not exclude self-references.</p>

View file

@ -0,0 +1,69 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
tickInterval: 1,
title: {
text: 'Number of authors'
}
},
yAxis: {
title: {
text: 'Percentage of {{ doc_label }}s'
},
labels: {
formatter: function () {
return this.value + '%';
}
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.x + ' ' + (this.x == 1 ? "author" : 'authors') + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y.toFixed(1) + '%';
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Authors</th>
<th>Percentage of {{ doc_label }}s</th>
<th>{{ doc_label|capfirst }}s</th>
</tr>
</thead>
<tbody>
{% for author_count, percentage, count, names in table_data %}
<tr>
<td>{{ author_count }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,65 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
type: "category",
title: {
text: 'Format'
}
},
yAxis: {
title: {
text: 'Number of {{ doc_label }}s'
}
},
tooltip: {
formatter: function () {
console.log(this);
var s = '<b>' + this.points[0].key + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Format</th>
<th>Percentage of {{ doc_label }}s</th>
<th>{{ doc_label|capfirst }}s</th>
</tr>
</thead>
<tbody>
{% for pages, percentage, count, names in table_data %}
<tr>
<td>{{ pages }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,65 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
type: "category",
title: {
text: 'Formal language'
}
},
yAxis: {
title: {
text: 'Number of {{ doc_label }}s'
}
},
tooltip: {
formatter: function () {
console.log(this);
var s = '<b>' + this.points[0].key + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Formal language</th>
<th>Percentage of {{ doc_label }}s</th>
<th>{{ doc_label|capfirst }}s</th>
</tr>
</thead>
<tbody>
{% for formal_language, percentage, count, names in table_data %}
<tr>
<td>{{ formal_language }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,63 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'line'
},
plotOptions: {
line: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
title: {
text: 'Number of pages'
}
},
yAxis: {
title: {
text: 'Number of {{ doc_label }}s'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.x + ' ' + (this.x == 1 ? "page" : 'pages') + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Pages</th>
<th>Percentage of {{ doc_label }}s</th>
<th>{{ doc_label|capfirst }}s</th>
</tr>
</thead>
<tbody>
{% for pages, percentage, count, names in table_data %}
<tr>
<td>{{ pages }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,63 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'line'
},
plotOptions: {
line: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
title: {
text: 'Number of words'
}
},
yAxis: {
title: {
text: 'Number of {{ doc_label }}s'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.x + ' - ' + (this.x + {{ bin_size }} - 1) + ' ' + (this.x == 1 ? "word" : 'words') + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Words</th>
<th>Percentage of {{ doc_label }}s</th>
<th>{{ doc_label|capfirst }}s</th>
</tr>
</thead>
<tbody>
{% for pages, percentage, count, names in table_data %}
<tr>
<td>{{ pages }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,53 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'line',
},
plotOptions: {
line: {
marker: {
enabled: false
},
animation: false
}
},
legend: {
align: "right",
verticalAlign: "middle",
layout: "vertical",
enabled: true
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
tickInterval: 1,
title: {
text: 'Year'
}
},
yAxis: {
min: 0,
title: {
text: 'Authors active in year'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.x + '</b>';
$.each(this.points, function () {
s += '<br/>' + this.series.name + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>

View file

@ -0,0 +1,8 @@
{% if content_limit and count <= content_limit %}
{% for n in names %}
{{ n }}<br>
{% endfor %}
{% else %}
{# <a class="popover-details" href="" data-elements="{% for n in names|slice:":20" %}{{ n }}{% if not forloop.last %}|{% endif %}{% endfor %}" data-sliced="{% if count > 20 %}1{% endif %}">{{ count }}</a> #}
{{ count }}
{% endif %}

View file

@ -1,6 +1,6 @@
{% extends "base.html" %}
{% load origin %}{% origin %}
{% load origin %}
{% load ietf_filters staticfiles bootstrap3 %}
@ -9,10 +9,12 @@
<h1>{% block title %}Statistics{% endblock %}</h1>
<p>Currently, there are statistics for:</p>
<p>Statistics on...</p>
<ul>
<li><a rel="nofollow" href="{% url "ietf.stats.views.review_stats" %}">Reviews in review teams</a> (requires login)</li>
<li><a href="{% url "ietf.stats.views.document_stats" %}">Drafts/RFCs (authors, countries, formats, ...)</a></li>
<li><a href="{% url "ietf.stats.views.meeting_stats" %}">Meeting attendance</a></li>
<li><a rel="nofollow" href="{% url "ietf.stats.views.review_stats" %}">Reviews of drafts in review teams</a> (requires login)</li>
</ul>
{% endblock %}

View file

@ -0,0 +1,47 @@
{% extends "base.html" %}
{% load origin %}
{% load ietf_filters staticfiles bootstrap3 %}
{% block content %}
{% origin %}
<h1>{% block title %}Countries known to the Datatracker{% endblock %}</h1>
<p>In case you think a country or an alias is missing from the list, you can <a href="mailto:{{ ticket_email_address }}">file a ticket</a>.</p>
{% if request.user.is_staff %}
<p>Note: since you're an admin, the country names are linked to their corresponding admin page.</p>
{% endif %}
<table class="table table-condensed table-striped">
<thead>
<tr>
<th>Name</th>
<th>Aliases (lowercase aliases are matched case-insensitive)</th>
</tr>
</thead>
<tbody>
{% for c in countries %}
<tr>
<td>
{% if request.user.is_staff %}
<a href="{% url "admin:name_countryname_change" c.pk %}">
{% endif %}
{{ c.name }}
{% if request.user.is_staff %}
</a>
{% endif %}
</td>
<td>
{% for a in c.aliases %}
{{ a.alias }}{% if not forloop.last %},{% endif %}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View file

@ -0,0 +1,46 @@
{% extends "base.html" %}
{% load origin %}
{% load ietf_filters staticfiles bootstrap3 %}
{% block title %}{{ stats_title }}{% endblock %}
{% block pagehead %}
<link rel="stylesheet" href="{% static 'bootstrap-datepicker/css/bootstrap-datepicker3.min.css' %}">
{% endblock %}
{% block content %}
{% origin %}
<h1>Meeting Statistics</h1>
{% if meeting %}
<p>
<a href="{% url "ietf.stats.views.meeting_stats" %}">&laquo; Back to overview</a>
</p>
{% endif %}
<div class="stats-options well">
<div>
Attendees:
<div class="btn-group">
{% for slug, label, url in possible_stats_types %}
<a class="btn btn-default {% if slug == stats_type %}active{% endif %}" href="{{ url }}">{{ label }}</a>
{% endfor %}
</div>
</div>
</div>
<div class="document-stats">
{% include content_template %}
</div>
{% endblock %}
{% block js %}
<script src="{% static 'highcharts/highcharts.js' %}"></script>
<script src="{% static 'highcharts/modules/exporting.js' %}"></script>
<script src="{% static 'highcharts/modules/offline-exporting.js' %}"></script>
<script src="{% static 'ietf/js/stats.js' %}"></script>
{% endblock %}

View file

@ -0,0 +1,64 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
type: "category",
title: {
text: 'Continent'
}
},
yAxis: {
title: {
text: 'Number of attendees'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.points[0].key + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Continent</th>
<th>Percentage of attendees</th>
<th>Attendees</th>
</tr>
</thead>
<tbody>
{% for continent, percentage, count, names in table_data %}
<tr>
<td>{{ continent|default:"(unknown)" }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>

View file

@ -0,0 +1,99 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'column'
},
plotOptions: {
column: {
animation: false
}
},
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
type: "category",
title: {
text: 'Country'
}
},
yAxis: {
title: {
text: 'Number of attendees'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + this.points[0].key + '</b>';
$.each(this.points, function () {
s += '<br/>' + chartConf.yAxis.title.text + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
<div id="pie-chart"></div>
<script>
var pieChartConf = {
chart: {
type: 'pie'
},
plotOptions: {
pie: {
animation: false,
dataLabels: {
enabled: true,
format: "{point.name}: {point.percentage:.1f}%"
},
enableMouseTracking: false
}
},
title: {
text: "Countries at IETF {{ meeting.number }}"
},
tooltip: {
},
series: [ {
name: "Countries",
colorByPoint: true,
data: {{ piechart_data }}
}]
};
</script>
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Country</th>
<th>Percentage of attendees</th>
<th>Attendees</th>
</tr>
</thead>
<tbody>
{% for country, percentage, count, names in table_data %}
<tr>
<td>{{ country|default:"(unknown)" }}</td>
<td>{{ percentage|floatformat:2 }}%</td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p>EU (European Union) is not a country, but has been added for reference, as the sum of
all current EU member countries:
{% for c in eu_countries %}{{ c.name }}{% if not forloop.last %}, {% endif %}{% endfor %}.
</p>

View file

@ -0,0 +1,77 @@
<h3>{{ stats_title }}</h3>
<div id="chart"></div>
<script>
var chartConf = {
chart: {
type: 'line',
},
plotOptions: {
line: {
marker: {
enabled: false
},
animation: false
}
},
{% if stats_type != "overview" %}
legend: {
align: "right",
verticalAlign: "middle",
layout: "vertical",
enabled: true
},
{% endif %}
title: {
text: '{{ stats_title|escapejs }}'
},
xAxis: {
tickInterval: 1,
title: {
text: 'Meeting'
}
},
yAxis: {
min: 0,
title: {
text: 'Attendees at meeting'
}
},
tooltip: {
formatter: function () {
var s = '<b>' + "IETF " + this.x + '</b>';
$.each(this.points, function () {
s += '<br/>' + this.series.name + ': ' + this.y;
});
return s;
},
shared: true
},
series: {{ chart_data }}
};
</script>
{% if table_data %}
<h3>Data</h3>
<table class="table table-condensed stats-data">
<thead>
<tr>
<th>Meeting</th>
<th>Attendees</th>
</tr>
</thead>
<tbody>
{% for meeting_number, label, url, count, names in table_data %}
<tr>
<td><a href="{{ url }}">{{ label }}</a></td>
<td>{% include "stats/includes/number_with_details_cell.html" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}

Some files were not shown because too many files have changed in this diff Show more