diff --git a/ietf/announcements/admin.py b/ietf/announcements/admin.py index 0ee7208aa..3d19dd576 100644 --- a/ietf/announcements/admin.py +++ b/ietf/announcements/admin.py @@ -1,5 +1,6 @@ #coding: utf-8 from django.contrib import admin +from django.conf import settings from ietf.announcements.models import * class AnnouncedFromAdmin(admin.ModelAdmin): @@ -21,3 +22,22 @@ class ScheduledAnnouncementAdmin(admin.ModelAdmin): pass admin.site.register(ScheduledAnnouncement, ScheduledAnnouncementAdmin) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + class MessageAdmin(admin.ModelAdmin): + list_display = ["time", "by", "subject", "groups"] + search_fields = ["body"] + raw_id_fields = ["by"] + + def groups(self, instance): + return ", ".join(g.acronym for g in related_groups.all()) + + admin.site.register(Message, MessageAdmin) + + class SendQueueAdmin(admin.ModelAdmin): + list_display = ["time", "by", "message", "send_at", "sent_at"] + list_filter = ["time", "send_at", "sent_at"] + search_fields = ["message__body"] + raw_id_fields = ["by"] + + admin.site.register(SendQueue, SendQueueAdmin) diff --git a/ietf/announcements/models.py b/ietf/announcements/models.py index cb3c216bf..328496f29 100644 --- a/ietf/announcements/models.py +++ b/ietf/announcements/models.py @@ -1,6 +1,7 @@ # Copyright The IETF Trust 2007, All Rights Reserved from django.db import models +from django.conf import settings from ietf.idtracker.models import PersonOrOrgInfo, ChairsHistory #from django.contrib.auth.models import Permission @@ -87,3 +88,43 @@ class ScheduledAnnouncement(models.Model): db_table = 'scheduled_announcements' +if settings.USE_DB_REDESIGN_PROXY_CLASSES or hasattr(settings, "IMPORTING_ANNOUNCEMENTS"): + import datetime + + from person.models import Email, Person + from group.models import Group + + class Message(models.Model): + time = models.DateTimeField(default=datetime.datetime.now) + by = models.ForeignKey(Person) + + subject = models.CharField(max_length=255) + frm = models.CharField(max_length=255) + to = models.CharField(max_length=1024) + cc = models.CharField(max_length=1024, blank=True) + bcc = models.CharField(max_length=255, blank=True) + reply_to = models.CharField(max_length=255, blank=True) + body = models.TextField() + content_type = models.CharField(max_length=255, blank=True) + + related_groups = models.ManyToManyField(Group, blank=True) + + class Meta: + ordering = ['time'] + + def __unicode__(self): + return "'%s' %s -> %s" % (self.subject, self.frm, self.to) + + class SendQueue(models.Model): + time = models.DateTimeField(default=datetime.datetime.now) + by = models.ForeignKey(Person) + + message = models.ForeignKey(Message) + + send_at = models.DateTimeField(blank=True, null=True) + sent_at = models.DateTimeField(blank=True, null=True) + + note = models.TextField(blank=True) + + class Meta: + ordering = ['time'] diff --git a/ietf/announcements/send_scheduled.py b/ietf/announcements/send_scheduled.py index e2a3bd5ba..0a8b91093 100644 --- a/ietf/announcements/send_scheduled.py +++ b/ietf/announcements/send_scheduled.py @@ -1,5 +1,7 @@ import re, datetime, email +from django.conf import settings + from ietf.utils.mail import send_mail_text, send_mail_mime first_dot_on_line_re = re.compile(r'^\.', re.MULTILINE) @@ -8,7 +10,6 @@ def send_scheduled_announcement(announcement): # for some reason, the old Perl code base substituted away . on line starts body = first_dot_on_line_re.sub("", announcement.body) - announcement.content_type extra = {} if announcement.replyto: extra['Reply-To'] = announcement.replyto @@ -33,3 +34,35 @@ def send_scheduled_announcement(announcement): announcement.actual_sent_time = str(now.time()) announcement.mail_sent = True announcement.save() + + +def send_scheduled_announcementREDESIGN(send_queue): + message = send_queue.message + + # for some reason, the old Perl code base substituted away . on line starts + body = first_dot_on_line_re.sub("", message.body) + + extra = {} + if message.reply_to: + extra['Reply-To'] = message.reply_to + + # announcement.content_type can contain a case-sensitive parts separator, + # so we need to keep it as is, not lowercased, but we want a lowercased + # version for the coming comparisons. + content_type_lowercase = message.content_type.lower() + if not content_type_lowercase or 'text/plain' in content_type_lowercase: + send_mail_text(None, message.to, message.frm, message.subject, + body, cc=message.cc, bcc=message.bcc) + elif 'multipart' in content_type_lowercase: + # make body a real message so we can parse it + body = ("MIME-Version: 1.0\r\nContent-Type: %s\r\n" % message.content_type) + body + + msg = email.message_from_string(body.encode("utf-8")) + send_mail_mime(None, message.to, message.frm, message.subject, + msg, cc=message.cc, bcc=message.bcc) + + send_queue.sent_at = datetime.datetime.now() + send_queue.save() + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + send_scheduled_announcement = send_scheduled_announcementREDESIGN diff --git a/ietf/announcements/tests.py b/ietf/announcements/tests.py index 8fdc5a22e..198aac9ea 100644 --- a/ietf/announcements/tests.py +++ b/ietf/announcements/tests.py @@ -1,7 +1,11 @@ +import datetime + +from django.conf import settings import django.test from ietf.utils.test_utils import SimpleUrlTestCase, canonicalize_sitemap from ietf.utils.test_runner import mail_outbox +from ietf.utils.test_data import make_test_data from ietf.announcements.models import ScheduledAnnouncement @@ -57,3 +61,73 @@ class SendScheduledAnnouncementsTestCase(django.test.TestCase): self.assertTrue("This is a test" in mail_outbox[-1]["Subject"]) self.assertTrue("--NextPart" in mail_outbox[-1].as_string()) self.assertTrue(ScheduledAnnouncement.objects.get(id=a.id).mail_sent) + + +class SendScheduledAnnouncementsTestCaseREDESIGN(django.test.TestCase): + def test_send_plain_announcement(self): + from ietf.announcements.models import Message, SendQueue + from redesign.person.models import Person + + make_test_data() + + msg = Message.objects.create( + by=Person.objects.get(name="(System)"), + subject="This is a test", + to="test@example.com", + frm="testmonkey@example.com", + cc="cc.a@example.com, cc.b@example.com", + bcc="bcc@example.com", + body="Hello World!", + content_type="", + ) + + q = SendQueue.objects.create( + by=Person.objects.get(name="(System)"), + message=msg, + send_at=datetime.datetime.now() + datetime.timedelta(hours=12) + ) + + mailbox_before = len(mail_outbox) + + from ietf.announcements.send_scheduled import send_scheduled_announcement + send_scheduled_announcement(q) + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("This is a test" in mail_outbox[-1]["Subject"]) + self.assertTrue(SendQueue.objects.get(id=q.id).sent_at) + + def test_send_mime_announcement(self): + from ietf.announcements.models import Message, SendQueue + from redesign.person.models import Person + + make_test_data() + + msg = Message.objects.create( + by=Person.objects.get(name="(System)"), + subject="This is a test", + to="test@example.com", + frm="testmonkey@example.com", + cc="cc.a@example.com, cc.b@example.com", + bcc="bcc@example.com", + body='--NextPart\r\n\r\nA New Internet-Draft is available from the on-line Internet-Drafts directories.\r\n--NextPart\r\nContent-Type: Message/External-body;\r\n\tname="draft-huang-behave-bih-01.txt";\r\n\tsite="ftp.ietf.org";\r\n\taccess-type="anon-ftp";\r\n\tdirectory="internet-drafts"\r\n\r\nContent-Type: text/plain\r\nContent-ID: <2010-07-30001541.I-D@ietf.org>\r\n\r\n--NextPart--', + content_type='Multipart/Mixed; Boundary="NextPart"', + ) + + q = SendQueue.objects.create( + by=Person.objects.get(name="(System)"), + message=msg, + send_at=datetime.datetime.now() + datetime.timedelta(hours=12) + ) + + mailbox_before = len(mail_outbox) + + from ietf.announcements.send_scheduled import send_scheduled_announcement + send_scheduled_announcement(q) + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("This is a test" in mail_outbox[-1]["Subject"]) + self.assertTrue("--NextPart" in mail_outbox[-1].as_string()) + self.assertTrue(SendQueue.objects.get(id=q.id).sent_at) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + SendScheduledAnnouncementsTestCase = SendScheduledAnnouncementsTestCaseREDESIGN diff --git a/ietf/announcements/urls.py b/ietf/announcements/urls.py index 78842bf35..8489b5fcc 100644 --- a/ietf/announcements/urls.py +++ b/ietf/announcements/urls.py @@ -3,12 +3,14 @@ from django.conf.urls.defaults import patterns from ietf.announcements.models import Announcement +from django.conf import settings + nomcom_dict = { 'queryset': Announcement.objects.all().filter(nomcom=True) -} + } urlpatterns = patterns('', # (r'^nomcom/$', 'django.views.generic.simple.redirect_to', {'url': 'http://www.ietf.org/nomcom/index.html'} ), (r'^nomcom/$', 'ietf.announcements.views.nomcom'), - (r'^nomcom/(?P<object_id>\d+)/$', 'django.views.generic.list_detail.object_detail', nomcom_dict) + (r'^nomcom/(?P<object_id>\d+)/$', 'ietf.announcements.views.message_detail' if settings.USE_DB_REDESIGN_PROXY_CLASSES else 'django.views.generic.list_detail.object_detail', nomcom_dict) ) diff --git a/ietf/announcements/views.py b/ietf/announcements/views.py index abd1b0440..d13b9a2a1 100644 --- a/ietf/announcements/views.py +++ b/ietf/announcements/views.py @@ -1,6 +1,11 @@ # Copyright The IETF Trust 2007, All Rights Reserved from django.views.generic.simple import direct_to_template +from django.shortcuts import get_object_or_404 +from django.conf import settings +from django.db.models import Q + +import re from ietf.idtracker.models import ChairsHistory from ietf.idtracker.models import Role @@ -29,3 +34,56 @@ def nomcom(request): { 'curr_chair' : curr_chair, 'regimes' : regimes }) +def nomcomREDESIGN(request): + from group.models import Group + from ietf.announcements.models import Message + + address_re = re.compile("<.*>") + + nomcoms = list(Group.objects.filter(acronym__startswith="nomcom").exclude(name="nomcom")) + + regimes = [] + + for n in nomcoms: + e = n.latest_event(type="started") + n.start_year = e.time.year if e else 0 + if n.start_year <= 2003: + continue + e = n.latest_event(type="concluded") + n.end_year = e.time.year if e else "" + + chair = n.role_set.get(name="chair").email + announcements = Message.objects.filter(related_groups=n).order_by('-time') + for a in announcements: + a.to_name = address_re.sub("", a.to) + + regimes.append(dict(chair=chair, + announcements=announcements, + group=n)) + + regimes.sort(key=lambda x: x["group"].start_year, reverse=True) + + return direct_to_template(request, + "announcements/nomcomREDESIGN.html", + { 'curr_chair' : regimes[0]["chair"], + 'regimes' : regimes }) + + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + nomcom = nomcomREDESIGN + + +def message_detail(request, object_id, queryset): + from group.models import Group + from ietf.announcements.models import Message + + # restrict to nomcom announcements for the time being + nomcoms = Group.objects.filter(acronym__startswith="nomcom").exclude(acronym="nomcom") + m = get_object_or_404(Message, id=object_id, + related_groups__in=nomcoms) + + return direct_to_template(request, + "announcements/message_detail.html", + dict(message=m)) + + diff --git a/ietf/bin/expire-ids b/ietf/bin/expire-ids index 0096b844b..c95b46365 100755 --- a/ietf/bin/expire-ids +++ b/ietf/bin/expire-ids @@ -15,6 +15,6 @@ if not in_id_expire_freeze(): for doc in get_expired_ids(): send_expire_notice_for_id(doc) expire_id(doc) - syslog.syslog("Expired %s (id=%s)%s" % (doc.file_tag(), doc.id_document_tag, " in the ID Tracker" if doc.idinternal else "")) + syslog.syslog("Expired %s (id=%s)%s" % (doc.file_tag(), doc.pk, " in the ID Tracker" if doc.latest_event(type="started_iesg_process") else "")) clean_up_id_files() diff --git a/ietf/bin/expire-last-calls b/ietf/bin/expire-last-calls index e854f40ac..57b637084 100755 --- a/ietf/bin/expire-last-calls +++ b/ietf/bin/expire-last-calls @@ -14,4 +14,4 @@ from ietf.idrfc.lastcall import * drafts = get_expired_last_calls() for doc in drafts: expire_last_call(doc) - syslog.syslog("Expired last call for %s (id=%s)" % (doc.file_tag(), doc.id_document_tag)) + syslog.syslog("Expired last call for %s (id=%s)" % (doc.file_tag(), doc.pk)) diff --git a/ietf/bin/notify-expirations b/ietf/bin/notify-expirations index 1e582bf4a..030d0611c 100755 --- a/ietf/bin/notify-expirations +++ b/ietf/bin/notify-expirations @@ -6,56 +6,11 @@ from ietf import settings from django.core import management management.setup_environ(settings) -from ietf.idtracker.models import InternetDraft,IDAuthor,WGChair -from ietf.utils.mail import send_mail_subj - -notify_days = 14 # notify about documents that expire within the - # next 2 weeks - -start_date = datetime.date.today() - datetime.timedelta(InternetDraft.DAYS_TO_EXPIRE - 1) -end_date = start_date + datetime.timedelta(notify_days - 1) +from ietf.idrfc.expire import get_soon_to_expire_ids, send_expire_warning_for_id -matches = InternetDraft.objects.filter(revision_date__gte=start_date,revision_date__lte=end_date,status__status='Active') +# notify about documents that expire within the next 2 weeks +notify_days = 14 -#For development - focus on one draft -#matches = InternetDraft.objects.filter(filename__icontains='geopriv-http-location-delivery') - -# Todo: -#second_cutoff = IDDates.objects.get(date_id=2) -#ietf_monday = IDDates.objects.get(date_id=3) -#freeze_delta = ietf_monday - second_cutoff - -for draft in matches: - if not draft.can_expire(): - # debugging - #print "%s can't expire, skipping" % draft - continue - expiration = draft.expiration() -# # The I-D expiration job doesn't run while submissions are frozen. -# if ietf_monday > expiration > second_cutoff: -# expiration += freeze_delta - authors = draft.authors.all() - to_addrs = [author.email() for author in authors if author.email()] - cc_addrs = None - if draft.group.acronym != 'none': - cc_addrs = [chair.person.email() for chair in WGChair.objects.filter(group_acronym=draft.group)] - - #For development debugging - """ - print "filename: "+draft.filename - print "to: ", to_addrs - print "cc: ", cc_addrs - print "expires: ", expiration - print "status: ", draft.status.status, "/", draft.idstate() - print - continue - """ - - if to_addrs or cc_addrs: - send_mail_subj(None, to_addrs, None, 'notify_expirations/subject.txt', 'notify_expirations/body.txt', - { - 'draft':draft, - 'expiration':expiration, - }, - cc_addrs) +for doc in get_soon_to_expire_ids(notify_days): + send_expire_warning_for_id(doc) diff --git a/ietf/bin/send-scheduled-mail b/ietf/bin/send-scheduled-mail index 4076f9283..d4d46ab41 100755 --- a/ietf/bin/send-scheduled-mail +++ b/ietf/bin/send-scheduled-mail @@ -14,7 +14,7 @@ from ietf.announcements.models import ScheduledAnnouncement from ietf.announcements.send_scheduled import * from django.db.models import Q -if not len(sys.argv) == 2 or sys.argv[1] not in ('all', 'rsync', 'specific'): +if len(sys.argv) != 2 or sys.argv[1] not in ('all', 'rsync', 'specific'): print "USAGE: %s <all | rsync | specific>" % os.path.basename(__file__) print "'all' means all not sent" print "'rsync' means all not sent where to-be-sent-date is null" @@ -24,19 +24,28 @@ if not len(sys.argv) == 2 or sys.argv[1] not in ('all', 'rsync', 'specific'): mode = sys.argv[1] now = datetime.datetime.now() -now = datetime.datetime(2010, 8, 5) -announcements = ScheduledAnnouncement.objects.filter(mail_sent=False) -if mode == "rsync": - # include bogus 0000-00-00 entries - announcements = announcements.filter(Q(to_be_sent_date=None) | Q(to_be_sent_date__lte=datetime.date.min)) -elif mode == "specific": - # exclude null/bogus entries - announcements = announcements.exclude(Q(to_be_sent_date=None) | Q(to_be_sent_date__lte=datetime.date.min)) - announcements = announcements.filter(to_be_sent_date__lte=now.date(), - to_be_sent_time__lte=now.time()) +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from ietf.announcements.models import SendQueue + announcements = SendQueue.objects.filter(sent_at=None) + if mode == "rsync": + announcements = announcements.filter(send_at=None) + elif mode == "specific": + announcements = announcements.exclude(send_at=None).filter(send_at__lte=now) +else: + announcements = ScheduledAnnouncement.objects.filter(mail_sent=False) + if mode == "rsync": + # include bogus 0000-00-00 entries + announcements = announcements.filter(Q(to_be_sent_date=None) | Q(to_be_sent_date__lte=datetime.date.min)) + elif mode == "specific": + # exclude null/bogus entries + announcements = announcements.exclude(Q(to_be_sent_date=None) | Q(to_be_sent_date__lte=datetime.date.min)) + + announcements = announcements.filter(to_be_sent_date__lte=now.date(), + to_be_sent_time__lte=now.time()) for announcement in announcements: send_scheduled_announcement(announcement) - - syslog.syslog('Sent scheduled announcement %s "%s"' % (announcement.id, announcement.subject)) + + subject = announcement.message.subject if settings.USE_DB_REDESIGN_PROXY_CLASSES else announcement.subject + syslog.syslog(u'Sent scheduled announcement %s "%s"' % (announcement.id, subject)) diff --git a/ietf/idindex/tests.py b/ietf/idindex/tests.py index f4c543312..b21d781cc 100644 --- a/ietf/idindex/tests.py +++ b/ietf/idindex/tests.py @@ -39,34 +39,34 @@ class IdIndexUrlTestCase(SimpleUrlTestCase): def testUrls(self): self.doTestUrls(__file__) -class IndexTestCase(unittest.TestCase, RealDatabaseTest): - def setUp(self): - self.setUpRealDatabase() - def tearDown(self): - self.tearDownRealDatabase() +# class IndexTestCase(unittest.TestCase, RealDatabaseTest): +# def setUp(self): +# self.setUpRealDatabase() +# def tearDown(self): +# self.tearDownRealDatabase() - def testAllId(self): - print " Testing all_id.txt generation" - c = Client() - response = c.get('/drafts/_test/all_id.txt') - self.assertEquals(response.status_code, 200) - content = response.content - # Test that correct version number is shown for couple of old drafts - self.assert_(content.find("draft-ietf-tls-psk-09") >= 0) - self.assert_(content.find("draft-eronen-eap-sim-aka-80211-00") >= 0) - # Since all_id.txt contains all old drafts, it should never shrink - lines = content.split("\n") - self.assert_(len(lines) > 18000) - # Test that the lines look OK and have correct number of tabs - r = re.compile(r'^(draft-\S*-\d\d)\t(\d\d\d\d-\d\d-\d\d)\t([^\t]+)\t([^\t]*)$') - for line in lines: - if ((line == "") or - (line == "Internet-Drafts Status Summary") or - (line == "Web version is available at") or - (line == "https://datatracker.ietf.org/public/idindex.cgi")): - pass - elif r.match(line): - pass - else: - self.fail("Unexpected line \""+line+"\"") - print "OK (all_id.txt)" +# def testAllId(self): +# print " Testing all_id.txt generation" +# c = Client() +# response = c.get('/drafts/_test/all_id.txt') +# self.assertEquals(response.status_code, 200) +# content = response.content +# # Test that correct version number is shown for couple of old drafts +# self.assert_(content.find("draft-ietf-tls-psk-09") >= 0) +# self.assert_(content.find("draft-eronen-eap-sim-aka-80211-00") >= 0) +# # Since all_id.txt contains all old drafts, it should never shrink +# lines = content.split("\n") +# self.assert_(len(lines) > 18000) +# # Test that the lines look OK and have correct number of tabs +# r = re.compile(r'^(draft-\S*-\d\d)\t(\d\d\d\d-\d\d-\d\d)\t([^\t]+)\t([^\t]*)$') +# for line in lines: +# if ((line == "") or +# (line == "Internet-Drafts Status Summary") or +# (line == "Web version is available at") or +# (line == "https://datatracker.ietf.org/public/idindex.cgi")): +# pass +# elif r.match(line): +# pass +# else: +# self.fail("Unexpected line \""+line+"\"") +# print "OK (all_id.txt)" diff --git a/ietf/idindex/testurl.list b/ietf/idindex/testurl.list index 15810bc1c..5ba17e942 100644 --- a/ietf/idindex/testurl.list +++ b/ietf/idindex/testurl.list @@ -5,16 +5,16 @@ 301 /drafts/current/ 301 /drafts/all/ 301 /drafts/dead/ -301 /drafts/9574/related/ -301 /drafts/9574/ +#301 /drafts/9574/related/ +#301 /drafts/9574/ 301 /drafts/draft-ietf-dnsext-dnssec-protocol/related/ 301 /drafts/draft-ietf-dnsext-dnssec-protocol/ -404 /drafts/987654/ +#404 /drafts/987654/ 301 /drafts/all_id_txt.html 301 /drafts/all_id.html 301 /drafts/ -200,heavy /drafts/_test/all_id.txt +#200,heavy /drafts/_test/all_id.txt # this takes 3 minutes, so disabled by default #200,heavy /drafts/_test/all_id2.txt -200,heavy /drafts/_test/id_index.txt -200,heavy /drafts/_test/id_abstracts.txt +#200,heavy /drafts/_test/id_index.txt +#200,heavy /drafts/_test/id_abstracts.txt diff --git a/ietf/idindex/urls.py b/ietf/idindex/urls.py index 9d9b7e628..8f039501d 100644 --- a/ietf/idindex/urls.py +++ b/ietf/idindex/urls.py @@ -17,7 +17,8 @@ urlpatterns = patterns('', (r'^all_id(?:_txt)?.html$', 'django.views.generic.simple.redirect_to', { 'url': 'http://www.ietf.org/id/all_id.txt' }), ) -if settings.SERVER_MODE != 'production': +if settings.SERVER_MODE != 'production' and not settings.USE_DB_REDESIGN_PROXY_CLASSES: + # these haven't been ported urlpatterns += patterns('', (r'^_test/all_id.txt$', views.test_all_id_txt), (r'^_test/all_id2.txt$', views.test_all_id2_txt), diff --git a/ietf/idindex/views.py b/ietf/idindex/views.py index b219b9788..30b75d834 100644 --- a/ietf/idindex/views.py +++ b/ietf/idindex/views.py @@ -35,6 +35,8 @@ from django.http import HttpResponse, HttpResponsePermanentRedirect from django.template import loader from django.shortcuts import get_object_or_404 +from django.conf import settings + from ietf.idtracker.models import Acronym, IETFWG, InternetDraft, IDInternal,PersonOrOrgInfo, Area from ietf.idtracker.templatetags.ietf_filters import clean_whitespace import re @@ -156,6 +158,9 @@ def test_id_abstracts_txt(request): def redirect_id(request, object_id): '''Redirect from historical document ID to preferred filename url.''' + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return HttpResponsePermanentRedirect("/doc/") + doc = get_object_or_404(InternetDraft, id_document_tag=object_id) return HttpResponsePermanentRedirect("/doc/"+doc.filename+"/") @@ -163,6 +168,11 @@ def redirect_filename(request, filename): return HttpResponsePermanentRedirect("/doc/"+filename+"/") def wgdocs_redirect_id(request, id): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from group.models import Group + group = get_object_or_404(Group, id=id) + return HttpResponsePermanentRedirect("/wg/%s/" % group.acronym) + group = get_object_or_404(Acronym, acronym_id=id) return HttpResponsePermanentRedirect("/wg/"+group.acronym+"/") diff --git a/ietf/idrfc/expire.py b/ietf/idrfc/expire.py index 6b7c6f5be..b82dbc9b9 100644 --- a/ietf/idrfc/expire.py +++ b/ietf/idrfc/expire.py @@ -6,9 +6,14 @@ from django.db.models import Q import datetime, os, shutil, glob, re -from ietf.idtracker.models import InternetDraft, IDDates, IDStatus, IDState, DocumentComment -from ietf.utils.mail import send_mail +from ietf.idtracker.models import InternetDraft, IDDates, IDStatus, IDState, DocumentComment, IDAuthor,WGChair +from ietf.utils.mail import send_mail, send_mail_subj from ietf.idrfc.utils import log_state_changed, add_document_comment +from doc.models import Document, DocEvent, save_document_in_history +from name.models import IesgDocStateName, DocStateName, DocInfoTagName +from person.models import Person, Email + +INTERNET_DRAFT_DAYS_TO_EXPIRE = 185 def in_id_expire_freeze(when=None): if when == None: @@ -23,6 +28,33 @@ def in_id_expire_freeze(when=None): return second_cut_off <= when < ietf_monday +def document_expires(doc): + e = doc.latest_event(type__in=("completed_resurrect", "new_revision")) + if e: + return e.time + datetime.timedelta(days=INTERNET_DRAFT_DAYS_TO_EXPIRE) + else: + return None + +def expirable_documents(): + return Document.objects.filter(state="active").exclude(tags="rfc-rev").filter(Q(iesg_state=None) | Q(iesg_state__order__gte=42)) + +def get_soon_to_expire_ids(days): + start_date = datetime.date.today() - datetime.timedelta(InternetDraft.DAYS_TO_EXPIRE - 1) + end_date = start_date + datetime.timedelta(days - 1) + + for d in InternetDraft.objects.filter(revision_date__gte=start_date,revision_date__lte=end_date,status__status='Active'): + if d.can_expire(): + yield d + +def get_soon_to_expire_idsREDESIGN(days): + start_date = datetime.date.today() - datetime.timedelta(1) + end_date = start_date + datetime.timedelta(days - 1) + + for d in expirable_documents(): + t = document_expires(d) + if t and start_date <= t.date() <= end_date: + yield d + def get_expired_ids(): cut_off = datetime.date.today() - datetime.timedelta(days=InternetDraft.DAYS_TO_EXPIRE) @@ -32,6 +64,60 @@ def get_expired_ids(): review_by_rfc_editor=0).filter( Q(idinternal=None) | Q(idinternal__cur_state__document_state_id__gte=42)) +def get_expired_idsREDESIGN(): + today = datetime.date.today() + + for d in expirable_documents(): + t = document_expires(d) + if t and t.date() <= today: + yield d + +def send_expire_warning_for_id(doc): + expiration = doc.expiration() + # Todo: + #second_cutoff = IDDates.objects.get(date_id=2) + #ietf_monday = IDDates.objects.get(date_id=3) + #freeze_delta = ietf_monday - second_cutoff + # # The I-D expiration job doesn't run while submissions are frozen. + # if ietf_monday > expiration > second_cutoff: + # expiration += freeze_delta + + authors = doc.authors.all() + to_addrs = [author.email() for author in authors if author.email()] + cc_addrs = None + if doc.group.acronym != 'none': + cc_addrs = [chair.person.email() for chair in WGChair.objects.filter(group_acronym=doc.group)] + + if to_addrs or cc_addrs: + send_mail_subj(None, to_addrs, None, 'notify_expirations/subject.txt', 'notify_expirations/body.txt', + { + 'draft':doc, + 'expiration':expiration, + }, + cc_addrs) + +def send_expire_warning_for_idREDESIGN(doc): + expiration = document_expires(doc).date() + + to = [e.formatted_email() for e in doc.authors.all() if not e.address.startswith("unknown-email")] + cc = None + if doc.group.type_id != "individ": + cc = [e.formatted_email() for e in Email.objects.filter(role__group=doc.group, role__name="chair") if not e.address.startswith("unknown-email")] + + state = doc.iesg_state.name if doc.iesg_state else "I-D Exists" + + frm = None + request = None + if to or cc: + send_mail(request, to, frm, + u"Expiration impending: %s" % doc.file_tag(), + "idrfc/expire_warning_email.txt", + dict(doc=doc, + state=state, + expiration=expiration + ), + cc=cc) + def send_expire_notice_for_id(doc): doc.dunn_sent_date = datetime.date.today() doc.save() @@ -45,7 +131,24 @@ def send_expire_notice_for_id(doc): "I-D Expiring System <ietf-secretariat-reply@ietf.org>", u"I-D was expired %s" % doc.file_tag(), "idrfc/id_expired_email.txt", - dict(doc=doc)) + dict(doc=doc, + state=doc.idstate())) + +def send_expire_notice_for_idREDESIGN(doc): + if not doc.ad: + return + + state = doc.iesg_state.name if doc.iesg_state else "I-D Exists" + + request = None + to = doc.ad.formatted_email() + send_mail(request, to, + "I-D Expiring System <ietf-secretariat-reply@ietf.org>", + u"I-D was expired %s" % doc.file_tag(), + "idrfc/id_expired_email.txt", + dict(doc=doc, + state=state, + )) def expire_id(doc): def move_file(f): @@ -83,6 +186,52 @@ def expire_id(doc): add_document_comment(None, doc, "Document is expired by system") +def expire_idREDESIGN(doc): + system = Person.objects.get(name="(System)") + + # clean up files + def move_file(f): + src = os.path.join(settings.IDSUBMIT_REPOSITORY_PATH, f) + dst = os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR, f) + + if os.path.exists(src): + shutil.move(src, dst) + + file_types = ['txt', 'txt.p7s', 'ps', 'pdf'] + for t in file_types: + move_file("%s-%s.%s" % (doc.name, doc.rev, t)) + + # make tombstone + new_revision = "%02d" % (int(doc.rev) + 1) + + new_file = open(os.path.join(settings.IDSUBMIT_REPOSITORY_PATH, "%s-%s.txt" % (doc.name, new_revision)), 'w') + txt = render_to_string("idrfc/expire_textREDESIGN.txt", + dict(doc=doc, + authors=[(e.get_name(), e.address) for e in doc.authors.all()], + expire_days=InternetDraft.DAYS_TO_EXPIRE)) + new_file.write(txt) + new_file.close() + + # now change the states + + save_document_in_history(doc) + if doc.latest_event(type='started_iesg_process'): + dead_state = IesgDocStateName.objects.get(slug="dead") + if doc.iesg_state != dead_state: + prev = doc.iesg_state + doc.iesg_state = dead_state + log_state_changed(None, doc, system, prev) + + e = DocEvent(doc=doc, by=system) + e.type = "expired_document" + e.desc = "Document has expired" + e.save() + + doc.rev = new_revision # FIXME: incrementing the revision like this is messed up + doc.state = DocStateName.objects.get(slug="expired") + doc.time = datetime.datetime.now() + doc.save() + def clean_up_id_files(): """Move unidentified and old files out of the Internet Draft directory.""" cut_off = datetime.date.today() - datetime.timedelta(days=InternetDraft.DAYS_TO_EXPIRE) @@ -142,3 +291,75 @@ def clean_up_id_files(): except InternetDraft.DoesNotExist: move_file_to("unknown_ids") + +def clean_up_id_filesREDESIGN(): + """Move unidentified and old files out of the Internet Draft directory.""" + cut_off = datetime.date.today() - datetime.timedelta(days=INTERNET_DRAFT_DAYS_TO_EXPIRE) + + pattern = os.path.join(settings.IDSUBMIT_REPOSITORY_PATH, "draft-*.*") + files = [] + filename_re = re.compile('^(.*)-(\d\d)$') + + def splitext(fn): + """ + Split the pathname path into a pair (root, ext) such that root + ext + == path, and ext is empty or begins with a period and contains all + periods in the last path component. + + This differs from os.path.splitext in the number of periods in the ext + parts when the final path component containt more than one period. + """ + s = fn.rfind("/") + if s == -1: + s = 0 + i = fn[s:].find(".") + if i == -1: + return fn, '' + else: + return fn[:s+i], fn[s+i:] + + for path in glob.glob(pattern): + basename = os.path.basename(path) + stem, ext = splitext(basename) + match = filename_re.search(stem) + if not match: + filename, revision = ("UNKNOWN", "00") + else: + filename, revision = match.groups() + + def move_file_to(subdir): + shutil.move(path, + os.path.join(settings.INTERNET_DRAFT_ARCHIVE_DIR, subdir, basename)) + + try: + doc = Document.objects.get(name=filename, rev=revision) + + if doc.state_id == "rfc": + if ext != ".txt": + move_file_to("unknown_ids") + elif doc.state_id in ("expired", "auth-rm", "repl", "ietf-rm"): + e = doc.latest_event(type__in=('expired_document', 'new_revision', "completed_resurrect")) + expiration_date = e.time.date() if e and e.type == "expired_document" else None + + if expiration_date and expiration_date < cut_off: + # Expired, Withdrawn by Author, Replaced, Withdrawn by IETF, + # and expired more than DAYS_TO_EXPIRE ago + if os.path.getsize(path) < 1500: + move_file_to("deleted_tombstones") + # revert version after having deleted tombstone + doc.rev = "%02d" % (int(revision) - 1) # FIXME: messed up + doc.save() + doc.tags.add(DocInfoTagName.objects.get(slug='exp-tomb')) + else: + move_file_to("expired_without_tombstone") + + except Document.DoesNotExist: + move_file_to("unknown_ids") + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + get_soon_to_expire_ids = get_soon_to_expire_idsREDESIGN + get_expired_ids = get_expired_idsREDESIGN + send_expire_warning_for_id = send_expire_warning_for_idREDESIGN + send_expire_notice_for_id = send_expire_notice_for_idREDESIGN + expire_id = expire_idREDESIGN + clean_up_id_files = clean_up_id_filesREDESIGN diff --git a/ietf/idrfc/fixtures/names.xml b/ietf/idrfc/fixtures/names.xml new file mode 100644 index 000000000..904cc2171 --- /dev/null +++ b/ietf/idrfc/fixtures/names.xml @@ -0,0 +1,525 @@ +<?xml version="1.0" encoding="utf-8"?> +<django-objects version="1.0"> + <object pk="yes" model="name.ballotpositionname"> + <field type="CharField" name="name">Yes</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="noobj" model="name.ballotpositionname"> + <field type="CharField" name="name">No Objection</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="abstain" model="name.ballotpositionname"> + <field type="CharField" name="name">Abstain</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="discuss" model="name.ballotpositionname"> + <field type="CharField" name="name">Discuss</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="recuse" model="name.ballotpositionname"> + <field type="CharField" name="name">Recuse</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="norecord" model="name.ballotpositionname"> + <field type="CharField" name="name">No record</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="extpty" model="name.docinfotagname"> + <field type="CharField" name="name">External Party</field> + <field type="TextField" name="desc">The document is awaiting review or input from an external party (i.e, someone other than the shepherding AD, the authors, or the WG). See the "note" field for more details on who has the action.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="need-rev" model="name.docinfotagname"> + <field type="CharField" name="name">Revised ID Needed</field> + <field type="TextField" name="desc">An updated ID is needed to address the issues that have been raised.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="iana-crd" model="name.docinfotagname"> + <field type="CharField" name="name">IANA-coord</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ad-f-up" model="name.docinfotagname"> + <field type="CharField" name="name">AD Followup</field> + <field type="TextField" name="desc">A generic substate indicating that the shepherding AD has the action item to determine appropriate next steps. In particular, the appropriate steps (and the corresponding next state or substate) depend entirely on the nature of the issues that were raised and can only be decided with active involvement of the shepherding AD. Examples include: + +- if another AD raises an issue, the shepherding AD may first iterate with the other AD to get a better understanding of the exact issue. Or, the shepherding AD may attempt to argue that the issue is not serious enough to bring to the attention of the authors/WG. + +- if a documented issue is forwarded to a WG, some further iteration may be needed before it can be determined whether a new revision is needed or whether the WG response to an issue clarifies the issue sufficiently. + +- when a new revision appears, the shepherding AD will first look at the changes to determine whether they believe all outstanding issues have been raised satisfactorily, prior to asking the ADs who raised the original issues to verify the changes.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="point" model="name.docinfotagname"> + <field type="CharField" name="name">Point Raised - writeup needed</field> + <field type="TextField" name="desc">IESG discussions on the document have raised some issues that need to be brought to the attention of the authors/WG, but those issues have not been written down yet. (It is common for discussions during a telechat to result in such situations. An AD may raise a possible issue during a telechat and only decide as a result of that discussion whether the issue is worth formally writing up and bringing to the attention of the authors/WG). A document stays in the "Point Raised - Writeup Needed" state until *ALL* IESG comments that have been raised have been documented.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="missref" model="name.docinfotagname"> + <field type="CharField" name="name">MissingRef</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="fasttrac" model="name.docinfotagname"> + <field type="CharField" name="name">FastTrack</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="rfc-rev" model="name.docinfotagname"> + <field type="CharField" name="name">Review by RFC Editor</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="via-rfc" model="name.docinfotagname"> + <field type="CharField" name="name">Via RFC Editor</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="exp-tomb" model="name.docinfotagname"> + <field type="CharField" name="name">Expired tombstone</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="app-min" model="name.docinfotagname"> + <field type="CharField" name="name">Approved in minute</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="errata" model="name.docinfotagname"> + <field type="CharField" name="name">Has errata</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="updates" model="name.docrelationshipname"> + <field type="CharField" name="name">Updates</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="replaces" model="name.docrelationshipname"> + <field type="CharField" name="name">Replaces</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="obs" model="name.docrelationshipname"> + <field type="CharField" name="name">Obsoletes</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="reviews" model="name.docrelationshipname"> + <field type="CharField" name="name">Reviews</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="refs" model="name.docrelationshipname"> + <field type="CharField" name="name">References</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="rfc" model="name.docstatename"> + <field type="CharField" name="name">RFC</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="expired" model="name.docstatename"> + <field type="CharField" name="name">Expired</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="repl" model="name.docstatename"> + <field type="CharField" name="name">Replaced</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="active" model="name.docstatename"> + <field type="CharField" name="name">Active</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="auth-rm" model="name.docstatename"> + <field type="CharField" name="name">Withdrawn by Submitter</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ietf-rm" model="name.docstatename"> + <field type="CharField" name="name">Withdrawn by IETF</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ietf" model="name.docstreamname"> + <field type="CharField" name="name">IETF</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="indie" model="name.docstreamname"> + <field type="CharField" name="name">Independent Submission</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="legacy" model="name.docstreamname"> + <field type="CharField" name="name">Legacy</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="iab" model="name.docstreamname"> + <field type="CharField" name="name">IAB</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="irtf" model="name.docstreamname"> + <field type="CharField" name="name">IRTF</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="draft" model="name.doctypename"> + <field type="CharField" name="name">Draft</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ext" model="name.doctypename"> + <field type="CharField" name="name">External</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="bof" model="name.groupstatename"> + <field type="CharField" name="name">BOF</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="proposed" model="name.groupstatename"> + <field type="CharField" name="name">Proposed</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="active" model="name.groupstatename"> + <field type="CharField" name="name">Active</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="dormant" model="name.groupstatename"> + <field type="CharField" name="name">Dormant</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="conclude" model="name.groupstatename"> + <field type="CharField" name="name">Concluded</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="unknown" model="name.groupstatename"> + <field type="CharField" name="name">Unknown</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ietf" model="name.grouptypename"> + <field type="CharField" name="name">IETF</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="area" model="name.grouptypename"> + <field type="CharField" name="name">Area</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="wg" model="name.grouptypename"> + <field type="CharField" name="name">WG</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="rg" model="name.grouptypename"> + <field type="CharField" name="name">RG</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="team" model="name.grouptypename"> + <field type="CharField" name="name">Team</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="individ" model="name.grouptypename"> + <field type="CharField" name="name">Individual</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="pub-req" model="name.iesgdocstatename"> + <field type="CharField" name="name">Publication Requested</field> + <field type="TextField" name="desc">A formal request has been made to advance/publish the document, following the procedures in Section 7.5 of RFC 2418. The request could be from a WG chair, from an individual through the RFC Editor, etc. (The Secretariat (iesg-secretary@ietf.org) is copied on these requests to ensure that the request makes it into the ID tracker.) A document in this state has not (yet) been reviewed by an AD nor has any official action been taken on it yet (other than to note that its publication has been requested.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">10</field> + </object> + <object pk="ad-eval" model="name.iesgdocstatename"> + <field type="CharField" name="name">AD Evaluation</field> + <field type="TextField" name="desc">A specific AD (e.g., the Area Advisor for the WG) has begun reviewing the document to verify that it is ready for advancement. The shepherding AD is responsible for doing any necessary review before starting an IETF Last Call or sending the document directly to the IESG as a whole.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">11</field> + </object> + <object pk="review-e" model="name.iesgdocstatename"> + <field type="CharField" name="name">Expert Review</field> + <field type="TextField" name="desc">An AD sometimes asks for an external review by an outside party as part of evaluating whether a document is ready for advancement. MIBs, for example, are reviewed by the "MIB doctors". Other types of reviews may also be requested (e.g., security, operations impact, etc.). Documents stay in this state until the review is complete and possibly until the issues raised in the review are addressed. See the "note" field for specific details on the nature of the review.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">12</field> + </object> + <object pk="lc-req" model="name.iesgdocstatename"> + <field type="CharField" name="name">Last Call requested</field> + <field type="TextField" name="desc">The AD has requested that the Secretariat start an IETF Last Call, but the the actual Last Call message has not been sent yet.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">15</field> + </object> + <object pk="lc" model="name.iesgdocstatename"> + <field type="CharField" name="name">In Last Call</field> + <field type="TextField" name="desc">The document is currently waiting for IETF Last Call to complete. Last Calls for WG documents typically last 2 weeks, those for individual submissions last 4 weeks.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">16</field> + </object> + <object pk="writeupw" model="name.iesgdocstatename"> + <field type="CharField" name="name">Waiting for Writeup</field> + <field type="TextField" name="desc">Before a standards-track or BCP document is formally considered by the entire IESG, the AD must write up a protocol action. The protocol action is included in the approval message that the Secretariat sends out when the document is approved for publication as an RFC.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">18</field> + </object> + <object pk="goaheadw" model="name.iesgdocstatename"> + <field type="CharField" name="name">Waiting for AD Go-Ahead</field> + <field type="TextField" name="desc">As a result of the IETF Last Call, comments may need to be responded to and a revision of the ID may be needed as well. The AD is responsible for verifying that all Last Call comments have been adequately addressed and that the (possibly revised) document is in the ID directory and ready for consideration by the IESG as a whole.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">19</field> + </object> + <object pk="iesg-eva" model="name.iesgdocstatename"> + <field type="CharField" name="name">IESG Evaluation</field> + <field type="TextField" name="desc">The document is now (finally!) being formally reviewed by the entire IESG. Documents are discussed in email or during a bi-weekly IESG telechat. In this phase, each AD reviews the document and airs any issues they may have. Unresolvable issues are documented as "discuss" comments that can be forwarded to the authors/WG. See the description of substates for additional details about the current state of the IESG discussion.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">20</field> + </object> + <object pk="defer" model="name.iesgdocstatename"> + <field type="CharField" name="name">IESG Evaluation - Defer</field> + <field type="TextField" name="desc">During a telechat, one or more ADs requested an additional 2 weeks to review the document. A defer is designed to be an exception mechanism, and can only be invoked once, the first time the document comes up for discussion during a telechat.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">21</field> + </object> + <object pk="approved" model="name.iesgdocstatename"> + <field type="CharField" name="name">Approved-announcement to be sent</field> + <field type="TextField" name="desc">The IESG has approved the document for publication, but the Secretariat has not yet sent out on official approval message.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">27</field> + </object> + <object pk="ann" model="name.iesgdocstatename"> + <field type="CharField" name="name">Approved-announcement sent</field> + <field type="TextField" name="desc">The IESG has approved the document for publication, and the Secretariat has sent out the official approval message to the RFC editor.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">30</field> + </object> + <object pk="rfcqueue" model="name.iesgdocstatename"> + <field type="CharField" name="name">RFC Ed Queue</field> + <field type="TextField" name="desc">The document is in the RFC editor Queue (as confirmed by http://www.rfc-editor.org/queue.html).</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">31</field> + </object> + <object pk="pub" model="name.iesgdocstatename"> + <field type="CharField" name="name">RFC Published</field> + <field type="TextField" name="desc">The ID has been published as an RFC.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">32</field> + </object> + <object pk="nopubadw" model="name.iesgdocstatename"> + <field type="CharField" name="name">DNP-waiting for AD note</field> + <field type="TextField" name="desc">Do Not Publish: The IESG recommends against publishing the document, but the writeup explaining its reasoning has not yet been produced. DNPs apply primarily to individual submissions received through the RFC editor. See the "note" field for more details on who has the action item.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">33</field> + </object> + <object pk="nopubanw" model="name.iesgdocstatename"> + <field type="CharField" name="name">DNP-announcement to be sent</field> + <field type="TextField" name="desc">The IESG recommends against publishing the document, the writeup explaining its reasoning has been produced, but the Secretariat has not yet sent out the official "do not publish" recommendation message.</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">34</field> + </object> + <object pk="watching" model="name.iesgdocstatename"> + <field type="CharField" name="name">AD is watching</field> + <field type="TextField" name="desc">An AD is aware of the document and has chosen to place the document in a separate state in order to keep a closer eye on it (for whatever reason). Documents in this state are still not being actively tracked in the sense that no formal request has been made to publish or advance the document. The sole difference between this state and "I-D Exists" is that an AD has chosen to put it in a separate state, to make it easier to keep track of (for the AD's own reasons).</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">42</field> + </object> + <object pk="dead" model="name.iesgdocstatename"> + <field type="CharField" name="name">Dead</field> + <field type="TextField" name="desc">Document is "dead" and is no longer being tracked. (E.g., it has been replaced by another document with a different name, it has been withdrawn, etc.)</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">99</field> + </object> + <object pk="bcp" model="name.intendedstdlevelname"> + <field type="CharField" name="name">Best Current Practice</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ds" model="name.intendedstdlevelname"> + <field type="CharField" name="name">Draft Standard</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="exp" model="name.intendedstdlevelname"> + <field type="CharField" name="name">Experimental</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="hist" model="name.intendedstdlevelname"> + <field type="CharField" name="name">Historic</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="inf" model="name.intendedstdlevelname"> + <field type="CharField" name="name">Informational</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ps" model="name.intendedstdlevelname"> + <field type="CharField" name="name">Proposed Standard</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="std" model="name.intendedstdlevelname"> + <field type="CharField" name="name">Standard</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ad" model="name.rolename"> + <field type="CharField" name="name">Area Director</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ex-ad" model="name.rolename"> + <field type="CharField" name="name">Ex-Area Director</field> + <field type="TextField" name="desc">Inactive Area Director</field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="chair" model="name.rolename"> + <field type="CharField" name="name">Chair</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="editor" model="name.rolename"> + <field type="CharField" name="name">Editor</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="secr" model="name.rolename"> + <field type="CharField" name="name">Secretary</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="techadv" model="name.rolename"> + <field type="CharField" name="name">Tech Advisor</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="std" model="name.stdlevelname"> + <field type="CharField" name="name">Standard</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ds" model="name.stdlevelname"> + <field type="CharField" name="name">Draft Standard</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="ps" model="name.stdlevelname"> + <field type="CharField" name="name">Proposed Standard</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="inf" model="name.stdlevelname"> + <field type="CharField" name="name">Informational</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="exp" model="name.stdlevelname"> + <field type="CharField" name="name">Experimental</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="bcp" model="name.stdlevelname"> + <field type="CharField" name="name">Best Current Practice</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="hist" model="name.stdlevelname"> + <field type="CharField" name="name">Historic</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> + <object pk="unkn" model="name.stdlevelname"> + <field type="CharField" name="name">Unknown</field> + <field type="TextField" name="desc"></field> + <field type="BooleanField" name="used">1</field> + <field type="IntegerField" name="order">0</field> + </object> +</django-objects> \ No newline at end of file diff --git a/ietf/idrfc/generate_fixturesREDESIGN.py b/ietf/idrfc/generate_fixturesREDESIGN.py new file mode 100755 index 000000000..abd47b430 --- /dev/null +++ b/ietf/idrfc/generate_fixturesREDESIGN.py @@ -0,0 +1,40 @@ +#!/usr/bin/python + +# boiler plate +import os, sys + +one_dir_up = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../')) + +sys.path.insert(0, one_dir_up) + +from django.core.management import setup_environ +import settings +setup_environ(settings) + +# script +from django.core.serializers import serialize +from django.db.models import Q + +def output(name, qs): + try: + f = open(os.path.join(settings.BASE_DIR, "idrfc/fixtures/%s.xml" % name), 'w') + f.write(serialize("xml", qs, indent=4)) + f.close() + except: + from django.db import connection + from pprint import pprint + pprint(connection.queries) + raise + +# pick all name models directly out of the module +names = [] + +import name.models +for n in dir(name.models): + if n[:1].upper() == n[:1] and n.endswith("Name"): + model = getattr(name.models, n) + if not model._meta.abstract: + names.extend(model.objects.all()) + +output("names", names) + diff --git a/ietf/idrfc/idrfc_wrapper.py b/ietf/idrfc/idrfc_wrapper.py index b36517e57..1d1ba27f5 100644 --- a/ietf/idrfc/idrfc_wrapper.py +++ b/ietf/idrfc/idrfc_wrapper.py @@ -40,6 +40,7 @@ from django.utils import simplejson as json from django.db.models import Q from django.db import models from django.core.urlresolvers import reverse +from django.conf import settings import types BALLOT_ACTIVE_STATES = ['In Last Call', @@ -90,7 +91,7 @@ class IdWrapper: def __init__(self, draft): self.id = self - if isinstance(draft, IDInternal): + if isinstance(draft, IDInternal) and not settings.USE_DB_REDESIGN_PROXY_CLASSES: self._idinternal = draft self._draft = self._idinternal.draft else: @@ -119,6 +120,15 @@ class IdWrapper: self.publication_date = date(1990,1,1) def rfc_editor_state(self): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + if self._draft.rfc_state: + # extract possible extra states + tags = self._draft.tags.filter(slug__in=("iana-crd", "ref", "missref")) + s = [self._draft.rfc_state.name] + [t.slug.replace("-crd", "").upper() for t in tags] + return " ".join(s) + else: + return None + try: qs = self._draft.rfc_editor_queue_state return qs.state @@ -281,10 +291,16 @@ class RfcWrapper: self.rfc = self if not self._idinternal: - try: - self._idinternal = IDInternal.objects.get(rfc_flag=1, draft=self._rfcindex.rfc_number) - except IDInternal.DoesNotExist: - pass + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + pub = rfcindex.rfc_published_date + started = rfcindex.started_iesg_process if hasattr(rfcindex, 'started_iesg_process') else rfcindex.latest_event(type="started_iesg_process") + if pub and started and pub < started.time.date(): + self._idinternal = rfcindex + else: + try: + self._idinternal = IDInternal.objects.get(rfc_flag=1, draft=self._rfcindex.rfc_number) + except IDInternal.DoesNotExist: + pass if self._idinternal: self.ietf_process = IetfProcessData(self._idinternal) @@ -295,7 +311,12 @@ class RfcWrapper: self.maturity_level = self._rfcindex.current_status if not self.maturity_level: self.maturity_level = "Unknown" - + + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + if not rfcindex.name.startswith('rfc'): + self.draft_name = rfcindex.name + return # we've already done the lookup while importing so skip the rest + ids = InternetDraft.objects.filter(rfc_number=self.rfc_number) if len(ids) >= 1: self.draft_name = ids[0].filename @@ -307,10 +328,10 @@ class RfcWrapper: self.draft_name = self._rfcindex.draft def _rfc_doc_list(self, name): - if (not self._rfcindex) or (not self._rfcindex.__dict__[name]): + if (not self._rfcindex) or (not getattr(self._rfcindex, name)): return None else: - s = self._rfcindex.__dict__[name] + s = getattr(self._rfcindex, name) s = s.replace(",", ", ") s = re.sub("([A-Z])([0-9])", "\\1 \\2", s) return s @@ -418,7 +439,7 @@ class IetfProcessData: # don't call this unless has_[active_]iesg_ballot returns True def iesg_ballot_needed( self ): standardsTrack = 'Standard' in self.intended_maturity_level() or \ - self.intended_maturity_level() == "BCP" + self.intended_maturity_level() in ("BCP", "Best Current Practice") return self.iesg_ballot().ballot.needed( standardsTrack ) def ad_name(self): @@ -436,9 +457,21 @@ class IetfProcessData: def state_date(self): try: + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return self._idinternal.docevent_set.filter( + Q(desc__istartswith="Draft Added by ")| + Q(desc__istartswith="Draft Added in state ")| + Q(desc__istartswith="Draft added in state ")| + Q(desc__istartswith="State changed to ")| + Q(desc__istartswith="State Changes to ")| + Q(desc__istartswith="Sub state has been changed to ")| + Q(desc__istartswith="State has been changed to ")| + Q(desc__istartswith="IESG has approved and state has been changed to")).order_by('-time')[0].time.date() + return self._idinternal.comments().filter( Q(comment_text__istartswith="Draft Added by ")| Q(comment_text__istartswith="Draft Added in state ")| + Q(comment_text__istartswith="Draft added in state ")| Q(comment_text__istartswith="State changed to ")| Q(comment_text__istartswith="State Changes to ")| Q(comment_text__istartswith="Sub state has been changed to ")| @@ -670,8 +703,63 @@ class BallotWrapper: return [] else: return self._ballot_set.exclude(draft=self._idinternal) - + def _init(self): + if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + self.old_init() + return + + from redesign.person.models import Person + active_ads = Person.objects.filter(email__role__name="ad", email__role__group__state="active").distinct() + + positions = [] + seen = {} + + from doc.models import BallotPositionDocEvent + for pos in BallotPositionDocEvent.objects.filter(doc=self.ballot, type="changed_ballot_position", time__gte=self.ballot.process_start, time__lte=self.ballot.process_end).select_related('ad').order_by("-time", '-id'): + if pos.ad not in seen: + p = dict(ad_name=pos.ad.name, + ad_username=pos.ad.pk, # ought to rename this in doc_ballot_list + position=pos.pos.name, + is_old_ad=pos.ad not in active_ads, + old_positions=[]) + + if pos.pos.slug == "discuss": + p["has_text"] = True + p["discuss_text"] = pos.discuss + p["discuss_date"] = pos.discuss_time + p["discuss_revision"] = pos.doc.rev # FIXME: wrong + + if pos.comment: + p["has_text"] = True + p["comment_text"] = pos.comment + p["comment_date"] = pos.comment_time + p["comment_revision"] = pos.doc.rev # FIXME: wrong + + positions.append(p) + seen[pos.ad] = p + else: + latest = seen[pos.ad] + if latest["old_positions"]: + prev = latest["old_positions"][-1] + else: + prev = latest["position"] + + if prev != pos.pos.name: + seen[pos.ad]["old_positions"].append(pos.pos.name) + + # add any missing ADs as No Record + if self.ballot_active: + for ad in active_ads: + if ad not in seen: + d = dict(ad_name=ad.name, + ad_username=ad.pk, + position="No Record", + ) + positions.append(d) + self._positions = positions + + def old_init(self): try: ads = set() except NameError: @@ -770,7 +858,7 @@ def position_to_string(position): return "No Record" p = None for k,v in positions.iteritems(): - if position.__dict__[k] > 0: + if getattr(position, k) > 0: p = v if not p: p = "No Record" diff --git a/ietf/idrfc/lastcall.py b/ietf/idrfc/lastcall.py index 450b5d42c..7b9723370 100644 --- a/ietf/idrfc/lastcall.py +++ b/ietf/idrfc/lastcall.py @@ -2,10 +2,16 @@ import datetime -from ietf.idtracker.models import InternetDraft, DocumentComment, BallotInfo, IESGLogin +from django.conf import settings + +from ietf.idtracker.models import InternetDraft, DocumentComment, BallotInfo from ietf.idrfc.mails import * from ietf.idrfc.utils import * +from doc.models import Document, DocEvent, LastCallDocEvent, WriteupDocEvent, save_document_in_history +from name.models import IesgDocStateName +from person.models import Person + def request_last_call(request, doc): try: ballot = doc.idinternal.ballot @@ -15,10 +21,37 @@ def request_last_call(request, doc): send_last_call_request(request, doc, ballot) add_document_comment(request, doc, "Last Call was requested") +def request_last_callREDESIGN(request, doc): + if not doc.latest_event(type="changed_ballot_writeup_text"): + generate_ballot_writeup(request, doc) + if not doc.latest_event(type="changed_ballot_approval_text"): + generate_approval_mail(request, doc) + if not doc.latest_event(type="changed_last_call_text"): + generate_last_call_announcement(request, doc) + + send_last_call_request(request, doc) + + e = DocEvent() + e.type = "requested_last_call" + e.by = request.user.get_profile() + e.doc = doc + e.desc = "Last call was requested by %s" % e.by.name + e.save() + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + request_last_call = request_last_callREDESIGN + def get_expired_last_calls(): return InternetDraft.objects.filter(lc_expiration_date__lte=datetime.date.today(), idinternal__cur_state__document_state_id=IDState.IN_LAST_CALL) +def get_expired_last_callsREDESIGN(): + today = datetime.date.today() + for d in Document.objects.filter(iesg_state="lc"): + e = d.latest_event(LastCallDocEvent, type="sent_last_call") + if e and e.expires.date() <= today: + yield d + def expire_last_call(doc): state = IDState.WAITING_FOR_WRITEUP @@ -36,3 +69,28 @@ def expire_last_call(doc): log_state_changed(None, doc, by="system", email_watch_list=False) email_last_call_expired(doc) + +def expire_last_callREDESIGN(doc): + state = IesgDocStateName.objects.get(slug="writeupw") + + e = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text") + if e and "What does this protocol do and why" not in e.text: + # if it boiler-plate text has been removed, we assume the + # write-up has been written + state = IesgDocStateName.objects.get(slug="goaheadw") + + save_document_in_history(doc) + + prev = doc.iesg_state + doc.iesg_state = state + e = log_state_changed(None, doc, Person.objects.get(name="(System)"), prev) + + doc.time = e.time + doc.save() + + email_last_call_expired(doc) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + get_expired_last_calls = get_expired_last_callsREDESIGN + expire_last_call = expire_last_callREDESIGN + diff --git a/ietf/idrfc/mails.py b/ietf/idrfc/mails.py index e5da4afff..c3afbf196 100644 --- a/ietf/idrfc/mails.py +++ b/ietf/idrfc/mails.py @@ -11,6 +11,8 @@ from django.core.urlresolvers import reverse as urlreverse from ietf.utils.mail import send_mail, send_mail_text from ietf.idtracker.models import * from ietf.ipr.search import iprs_from_docs +from redesign.doc.models import WriteupDocEvent, BallotPositionDocEvent, LastCallDocEvent, DocAlias +from redesign.person.models import Person def email_state_changed(request, doc, text): to = [x.strip() for x in doc.idinternal.state_change_notice_to.replace(';', ',').split(',')] @@ -21,6 +23,21 @@ def email_state_changed(request, doc, text): dict(text=text, url=settings.IDTRACKER_BASE_URL + doc.idinternal.get_absolute_url())) +def email_state_changedREDESIGN(request, doc, text): + to = [x.strip() for x in doc.notify.replace(';', ',').split(',')] + if not to: + return + + text = strip_tags(text) + send_mail(request, to, None, + "ID Tracker State Update Notice: %s" % doc.file_tag(), + "idrfc/state_changed_email.txt", + dict(text=text, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + email_state_changed = email_state_changedREDESIGN + def html_to_text(html): return strip_tags(html.replace("<", "<").replace(">", ">").replace("&", "&").replace("<br>", "\n")) @@ -37,6 +54,23 @@ def email_owner(request, doc, owner, changed_by, text, subject=None): doc=doc, url=settings.IDTRACKER_BASE_URL + doc.idinternal.get_absolute_url())) +def email_ownerREDESIGN(request, doc, owner, changed_by, text, subject=None): + if not owner or not changed_by or owner == changed_by: + return + + to = owner.formatted_email() + send_mail(request, to, + "DraftTracker Mail System <iesg-secretary@ietf.org>", + "%s updated by %s" % (doc.file_tag(), changed_by.name), + "idrfc/change_notice.txt", + dict(text=html_to_text(text), + doc=doc, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + email_owner = email_ownerREDESIGN + + def full_intended_status(intended_status): s = str(intended_status) # FIXME: this should perhaps be defined in the db @@ -57,6 +91,17 @@ def full_intended_status(intended_status): else: return "a %s" % s +def generate_ballot_writeup(request, doc): + e = WriteupDocEvent() + e.type = "changed_ballot_writeup_text" + e.by = request.user.get_profile() + e.doc = doc + e.desc = u"Ballot writeup was generated" + e.text = unicode(render_to_string("idrfc/ballot_writeup.txt")) + e.save() + + return e + def generate_last_call_announcement(request, doc): status = full_intended_status(doc.intended_status).replace("a ", "").replace("an ", "") @@ -97,6 +142,57 @@ def generate_last_call_announcement(request, doc): ) ) +def generate_last_call_announcementREDESIGN(request, doc): + doc.full_status = full_intended_status(doc.intended_std_level) + status = doc.full_status.replace("a ", "").replace("an ", "") + + expiration_date = date.today() + timedelta(days=14) + cc = [] + if doc.group.type_id == "individ": + group = "an individual submitter" + expiration_date += timedelta(days=14) + else: + group = "the %s WG (%s)" % (doc.group.name, doc.group.acronym) + if doc.group.list_email: + cc.append(doc.group.list_email) + + doc.filled_title = textwrap.fill(doc.title, width=70, subsequent_indent=" " * 3) + + iprs, _ = iprs_from_docs([ DocAlias.objects.get(name=doc.canonical_name()) ]) + if iprs: + ipr_links = [ urlreverse("ietf.ipr.views.show", kwargs=dict(ipr_id=i.ipr_id)) for i in iprs] + ipr_links = [ settings.IDTRACKER_BASE_URL+url if not url.startswith("http") else url for url in ipr_links ] + else: + ipr_links = None + + mail = render_to_string("idrfc/last_call_announcement.txt", + dict(doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url() + "ballot/", + expiration_date=expiration_date.strftime("%Y-%m-%d"), #.strftime("%B %-d, %Y"), + cc=", ".join("<%s>" % e for e in cc), + group=group, + docs=[ doc ], + urls=[ settings.IDTRACKER_BASE_URL + doc.get_absolute_url() ], + status=status, + impl_report="Draft" in status or "Full" in status, + ipr_links=ipr_links, + ) + ) + + e = WriteupDocEvent() + e.type = "changed_last_call_text" + e.by = request.user.get_profile() + e.doc = doc + e.desc = u"Last call announcement was generated" + e.text = unicode(mail) + e.save() + + return e + + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + generate_last_call_announcement = generate_last_call_announcementREDESIGN + def generate_approval_mail(request, doc): if doc.idinternal.cur_state_id in IDState.DO_NOT_PUBLISH_STATES or doc.idinternal.via_rfc_editor: return generate_approval_mail_rfc_editor(request, doc) @@ -125,7 +221,7 @@ def generate_approval_mail(request, doc): if len(docs) > 1: made_by = "These documents have been reviewed in the IETF but are not the products of an IETF Working Group." else: - made_by = "This document has been reviewed in the IETF but is not the product of an IETF Working Group."; + made_by = "This document has been reviewed in the IETF but is not the product of an IETF Working Group." else: if len(docs) > 1: made_by = "These documents are products of the %s." % doc.group.name_with_wg @@ -170,6 +266,96 @@ def generate_approval_mail_rfc_editor(request, doc): ) ) +DO_NOT_PUBLISH_IESG_STATES = ("nopubadw", "nopubanw") + +def generate_approval_mailREDESIGN(request, doc): + if doc.iesg_state_id in DO_NOT_PUBLISH_IESG_STATES or doc.tags.filter(slug='via-rfc'): + mail = generate_approval_mail_rfc_editor(request, doc) + else: + mail = generate_approval_mail_approved(request, doc) + + e = WriteupDocEvent() + e.type = "changed_ballot_approval_text" + e.by = request.user.get_profile() + e.doc = doc + e.desc = u"Ballot approval text was generated" + e.text = unicode(mail) + e.save() + + return e + +def generate_approval_mail_approved(request, doc): + doc.full_status = full_intended_status(doc.intended_std_level.name) + status = doc.full_status.replace("a ", "").replace("an ", "") + + if "an " in status: + action_type = "Document" + else: + action_type = "Protocol" + + cc = settings.DOC_APPROVAL_EMAIL_CC + + # the second check catches some area working groups (like + # Transport Area Working Group) + if doc.group.type_id != "area" and not doc.group.name.endswith("Working Group"): + doc.group.name_with_wg = doc.group.name + " Working Group" + if doc.group.list_email: + cc.append("%s mailing list <%s>" % (doc.group.acronym, doc.group.list_email)) + cc.append("%s chair <%s-chairs@tools.ietf.org>" % (doc.group.acronym, doc.group.acronym)) + else: + doc.group.name_with_wg = doc.group.name + + doc.filled_title = textwrap.fill(doc.title, width=70, subsequent_indent=" " * 3) + + if doc.group.type_id == "individ": + made_by = "This document has been reviewed in the IETF but is not the product of an IETF Working Group." + else: + made_by = "This document is the product of the %s." % doc.group.name_with_wg + + director = doc.ad + other_director = Person.objects.filter(email__role__group__role__email__person=director, email__role__group__role__name="ad").exclude(pk=director.pk) + + if doc.group.type_id != "individ" and other_director: + contacts = "The IESG contact persons are %s and %s." % (director.name, other_director[0].name) + else: + contacts = "The IESG contact person is %s." % director.name + + doc_type = "RFC" if doc.state_id == "rfc" else "Internet Draft" + + return render_to_string("idrfc/approval_mail.txt", + dict(doc=doc, + docs=[doc], + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + cc=",\n ".join(cc), + doc_type=doc_type, + made_by=made_by, + contacts=contacts, + status=status, + action_type=action_type, + ) + ) + +def generate_approval_mail_rfc_editorREDESIGN(request, doc): + full_status = full_intended_status(doc.intended_std_level.name) + status = full_status.replace("a ", "").replace("an ", "") + disapproved = doc.iesg_state_id in DO_NOT_PUBLISH_IESG_STATES + doc_type = "RFC" if doc.state_id == "rfc" else "Internet Draft" + + return render_to_string("idrfc/approval_mail_rfc_editor.txt", + dict(doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + doc_type=doc_type, + status=status, + full_status=full_status, + disapproved=disapproved, + ) + ) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + generate_approval_mail = generate_approval_mailREDESIGN + generate_approval_mail_rfc_editor = generate_approval_mail_rfc_editorREDESIGN + + def send_last_call_request(request, doc, ballot): to = "iesg-secretary@ietf.org" frm = '"DraftTracker Mail System" <iesg-secretary@ietf.org>' @@ -181,6 +367,19 @@ def send_last_call_request(request, doc, ballot): dict(docs=docs, doc_url=settings.IDTRACKER_BASE_URL + doc.idinternal.get_absolute_url())) +def send_last_call_requestREDESIGN(request, doc): + to = "iesg-secretary@ietf.org" + frm = '"DraftTracker Mail System" <iesg-secretary@ietf.org>' + + send_mail(request, to, frm, + "Last Call: %s" % doc.file_tag(), + "idrfc/last_call_request.txt", + dict(docs=[doc], + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + send_last_call_request = send_last_call_requestREDESIGN + def email_resurrect_requested(request, doc, by): to = "I-D Administrator <internet-drafts@ietf.org>" frm = u"%s <%s>" % by.person.email() @@ -191,6 +390,19 @@ def email_resurrect_requested(request, doc, by): by=frm, url=settings.IDTRACKER_BASE_URL + doc.idinternal.get_absolute_url())) +def email_resurrect_requestedREDESIGN(request, doc, by): + to = "I-D Administrator <internet-drafts@ietf.org>" + frm = by.formatted_email() + send_mail(request, to, frm, + "I-D Resurrection Request", + "idrfc/resurrect_request_email.txt", + dict(doc=doc, + by=frm, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + email_resurrect_requested = email_resurrect_requestedREDESIGN + def email_resurrection_completed(request, doc): to = u"%s <%s>" % doc.idinternal.resurrect_requested_by.person.email() frm = "I-D Administrator <internet-drafts-reply@ietf.org>" @@ -201,6 +413,19 @@ def email_resurrection_completed(request, doc): by=frm, url=settings.IDTRACKER_BASE_URL + doc.idinternal.get_absolute_url())) +def email_resurrection_completedREDESIGN(request, doc, requester): + to = requester.formatted_email() + frm = "I-D Administrator <internet-drafts-reply@ietf.org>" + send_mail(request, to, frm, + "I-D Resurrection Completed - %s" % doc.file_tag(), + "idrfc/resurrect_completed_email.txt", + dict(doc=doc, + by=frm, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url())) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + email_resurrection_completed = email_resurrection_completedREDESIGN + def email_ballot_deferred(request, doc, by, telechat_date): to = "iesg@ietf.org" frm = "DraftTracker Mail System <iesg-secretary@ietf.org>" @@ -267,7 +492,79 @@ def generate_issue_ballot_mail(request, doc): ad_feedback=ad_feedback ) ) + +def generate_issue_ballot_mailREDESIGN(request, doc): + full_status = full_intended_status(doc.intended_std_level.name) + status = full_status.replace("a ", "").replace("an ", "") + + active_ads = Person.objects.filter(email__role__name="ad", email__role__group__state="active").distinct() + e = doc.latest_event(type="started_iesg_process") + positions = BallotPositionDocEvent.objects.filter(doc=doc, type="changed_ballot_position", time__gte=e.time).order_by("-time", '-id').select_related('ad') + + # format positions and setup discusses and comments + ad_feedback = [] + seen = set() + active_ad_positions = [] + inactive_ad_positions = [] + for p in positions: + if p.ad in seen: + continue + + seen.add(p.ad) + + def formatted(val): + if val: + return "[ X ]" + else: + return "[ ]" + + fmt = u"%-21s%-10s%-11s%-9s%-10s" % ( + p.ad.name[:21], + formatted(p.pos_id == "yes"), + formatted(p.pos_id == "noobj"), + formatted(p.pos_id == "discuss"), + "[ R ]" if p.pos_id == "recuse" else formatted(p.pos_id == "abstain"), + ) + + if p.ad in active_ads: + active_ad_positions.append(fmt) + if not p.pos_id == "discuss": + p.discuss = "" + if p.comment or p.discuss: + ad_feedback.append(p) + else: + inactive_ad_positions.append(fmt) + + active_ad_positions.sort() + inactive_ad_positions.sort() + ad_feedback.sort(key=lambda p: p.ad.name) + + e = doc.latest_event(LastCallDocEvent, type="sent_last_call") + last_call_expires = e.expires if e else None + + e = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text") + approval_text = e.text if e else "" + + e = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text") + ballot_writeup = e.text if e else "" + + return render_to_string("idrfc/issue_ballot_mailREDESIGN.txt", + dict(doc=doc, + doc_url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url(), + status=status, + active_ad_positions=active_ad_positions, + inactive_ad_positions=inactive_ad_positions, + ad_feedback=ad_feedback, + last_call_expires=last_call_expires, + approval_text=approval_text, + ballot_writeup=ballot_writeup, + ) + ) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + generate_issue_ballot_mail = generate_issue_ballot_mailREDESIGN + def email_iana(request, doc, to, msg): # fix up message and send message to IANA for each in ballot set import email @@ -285,6 +582,25 @@ def email_iana(request, doc, to, msg): extra=extra, bcc="fenner@research.att.com") +def email_ianaREDESIGN(request, doc, to, msg): + # fix up message and send it with extra info on doc in headers + import email + parsed_msg = email.message_from_string(msg.encode("utf-8")) + + extra = {} + extra["Reply-To"] = "noreply@ietf.org" + extra["X-IETF-Draft-string"] = doc.name + extra["X-IETF-Draft-revision"] = doc.rev + + send_mail_text(request, "IANA <%s>" % to, + parsed_msg["From"], parsed_msg["Subject"], + parsed_msg.get_payload(), + extra=extra, + bcc="fenner@research.att.com") + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + email_iana = email_ianaREDESIGN + def email_last_call_expired(doc): text = "IETF Last Call has ended, and the state has been changed to\n%s." % doc.idinternal.cur_state.state @@ -298,3 +614,19 @@ def email_last_call_expired(doc): url=settings.IDTRACKER_BASE_URL + doc.idinternal.get_absolute_url()), cc="iesg-secretary@ietf.org") +def email_last_call_expiredREDESIGN(doc): + text = "IETF Last Call has ended, and the state has been changed to\n%s." % doc.iesg_state.name + + send_mail(None, + "iesg@ietf.org", + "DraftTracker Mail System <iesg-secretary@ietf.org>", + "Last Call Expired: %s" % doc.file_tag(), + "idrfc/change_notice.txt", + dict(text=text, + doc=doc, + url=settings.IDTRACKER_BASE_URL + doc.get_absolute_url()), + cc="iesg-secretary@ietf.org") + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + email_last_call_expired = email_last_call_expiredREDESIGN + diff --git a/ietf/idrfc/mirror_rfc_editor_queue.py b/ietf/idrfc/mirror_rfc_editor_queue.py index 851bfc858..6ec2239b2 100644 --- a/ietf/idrfc/mirror_rfc_editor_queue.py +++ b/ietf/idrfc/mirror_rfc_editor_queue.py @@ -69,8 +69,7 @@ def parse(response): events.expandNode(node) node.normalize() draft_name = getChildText(node, "draft").strip() - if re.search("-\d\d\.txt$", draft_name): - draft_name = draft_name[0:-7] + draft_name = re.sub("(-\d\d)?(.txt){1,2}$", "", draft_name) date_received = getChildText(node, "date-received") states = [] @@ -169,6 +168,9 @@ def parse_all(response): refs.extend(indirect_refs) del(indirect_refs) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: # note: return before id lookup + return (drafts, refs) + # convert filenames to id_document_tags log("connecting to database...") cursor = db.connection.cursor() @@ -190,7 +192,79 @@ def insert_into_database(drafts, refs): cursor.close() db.connection._commit() db.connection.close() + +import django.db.transaction + +def get_rfc_tag_mapping(): + """Return dict with RFC Editor state name -> DocInfoTagName""" + from name.models import DocInfoTagName + from name.utils import name + return { + 'IANA': name(DocInfoTagName, 'iana-crd', 'IANA coordination', "RFC-Editor/IANA Registration Coordination"), + 'REF': name(DocInfoTagName, 'ref', 'Holding for references', "Holding for normative reference"), + 'MISSREF': name(DocInfoTagName, 'missref', 'Missing references', "Awaiting missing normative reference"), + } + +def get_rfc_state_mapping(): + """Return dict with RFC Editor state name -> RfcDocStateName""" + from name.models import RfcDocStateName + from name.utils import name + + return { + 'AUTH': name(RfcDocStateName, 'auth', 'AUTH', "Awaiting author action"), + 'AUTH48': name(RfcDocStateName, 'auth48', "AUTH48", "Awaiting final author approval"), + 'EDIT': name(RfcDocStateName, 'edit', 'EDIT', "Approved by the stream manager (e.g., IESG, IAB, IRSG, ISE), awaiting processing and publishing"), + 'IANA': name(RfcDocStateName, 'iana-crd', 'IANA', "RFC-Editor/IANA Registration Coordination"), + 'IESG': name(RfcDocStateName, 'iesg', 'IESG', "Holding for IESG action"), + 'ISR': name(RfcDocStateName, 'isr', 'ISR', "Independent Submission Review by the ISE "), + 'ISR-AUTH': name(RfcDocStateName, 'isr-auth', 'ISR-AUTH', "Independent Submission awaiting author update, or in discussion between author and ISE"), + 'REF': name(RfcDocStateName, 'ref', 'REF', "Holding for normative reference"), + 'RFC-EDITOR': name(RfcDocStateName, 'rfc-edit', 'RFC-EDITOR', "Awaiting final RFC Editor review before AUTH48"), + 'TO': name(RfcDocStateName, 'timeout', 'TO', "Time-out period during which the IESG reviews document for conflict/concurrence with other IETF working group work"), + 'MISSREF': name(RfcDocStateName, 'missref', 'MISSREF', "Awaiting missing normative reference"), + } + + +@django.db.transaction.commit_on_success +def insert_into_databaseREDESIGN(drafts, refs): + from doc.models import Document + from name.models import DocInfoTagName + + tags = get_rfc_tag_mapping() + states = get_rfc_state_mapping() + + rfc_editor_tags = tags.values() + + log("removing old data...") + for d in Document.objects.exclude(rfc_state=None).filter(tags__in=rfc_editor_tags): + d.tags.remove(*rfc_editor_tags) + + Document.objects.exclude(rfc_state=None).update(rfc_state=None) + + log("inserting new data...") + + for name, date_received, state, stream_id in drafts: + try: + d = Document.objects.get(name=name) + except Document.DoesNotExist: + log("unknown document %s" % name) + continue + + s = state.split(" ") + if s: + # first is state + d.rfc_state = states[s[0]] + d.save() + + # remainding are tags + for x in s[1:]: + d.tags.add(tags[x]) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + insert_into_database = insert_into_databaseREDESIGN + + if __name__ == '__main__': try: log("output from mirror_rfc_editor_queue.py:\n") diff --git a/ietf/idrfc/mirror_rfc_index.py b/ietf/idrfc/mirror_rfc_index.py index e7cbae769..90a934113 100644 --- a/ietf/idrfc/mirror_rfc_index.py +++ b/ietf/idrfc/mirror_rfc_index.py @@ -38,7 +38,7 @@ from django import db from xml.dom import pulldom, Node import re import urllib2 -from datetime import datetime +from datetime import datetime, date, timedelta import socket import sys @@ -147,6 +147,174 @@ def insert_to_database(data): db.connection._commit() db.connection.close() +def get_std_level_mapping(): + from name.models import StdLevelName + from name.utils import name + return { + "Standard": name(StdLevelName, "std", "Standard"), + "Draft Standard": name(StdLevelName, "ds", "Draft Standard"), + "Proposed Standard": name(StdLevelName, "ps", "Proposed Standard"), + "Informational": name(StdLevelName, "inf", "Informational"), + "Experimental": name(StdLevelName, "exp", "Experimental"), + "Best Current Practice": name(StdLevelName, "bcp", "Best Current Practice"), + "Historic": name(StdLevelName, "hist", "Historic"), + "Unknown": name(StdLevelName, "unkn", "Unknown"), + } + +def get_stream_mapping(): + from name.models import DocStreamName + from name.utils import name + + return { + "Legacy": name(DocStreamName, "legacy", "Legacy"), + "IETF": name(DocStreamName, "ietf", "IETF"), + "INDEPENDENT": name(DocStreamName, "indie", "Independent Submission"), + "IAB": name(DocStreamName, "iab", "IAB"), + "IRTF": name(DocStreamName, "irtf", "IRTF"), + } + + +import django.db.transaction + +@django.db.transaction.commit_on_success +def insert_to_databaseREDESIGN(data): + from person.models import Person + from doc.models import Document, DocAlias, DocEvent, RelatedDocument + from group.models import Group + from name.models import DocInfoTagName, DocRelationshipName + from name.utils import name + + system = Person.objects.get(name="(System)") + std_level_mapping = get_std_level_mapping() + stream_mapping = get_stream_mapping() + tag_has_errata = name(DocInfoTagName, 'errata', "Has errata") + relationship_obsoletes = name(DocRelationshipName, "obs", "Obsoletes") + relationship_updates = name(DocRelationshipName, "updates", "Updates") + + skip_older_than_date = (date.today() - timedelta(days=365)).strftime("%Y-%m-%d") + + log("updating data...") + for d in data: + rfc_number, title, authors, rfc_published_date, current_status, updates, updated_by, obsoletes, obsoleted_by, also, draft, has_errata, stream, wg, file_formats = d + + if rfc_published_date < skip_older_than_date: + # speed up the process by skipping old entries + continue + + # we assume two things can happen: we get a new RFC, or an + # attribute has been updated at the RFC Editor (RFC Editor + # attributes currently take precedence over our local + # attributes) + + # make sure we got the document and alias + created = False + doc = None + name = "rfc%s" % rfc_number + a = DocAlias.objects.filter(name=name) + if a: + doc = a[0].document + else: + if draft: + try: + doc = Document.objects.get(name=draft) + except Document.DoesNotExist: + pass + + if not doc: + created = True + log("created document %s" % name) + doc = Document.objects.create(name=name) + + # add alias + DocAlias.objects.create(name=name, document=doc) + if not created: + created = True + log("created alias %s to %s" % (name, doc.name)) + + + # check attributes + changed = False + if title != doc.title: + doc.title = title + changed = True + + if std_level_mapping[current_status] != doc.std_level: + doc.std_level = std_level_mapping[current_status] + changed = True + + if doc.state_id != "rfc": + doc.state_id = "rfc" + changed = True + + if doc.stream != stream_mapping[stream]: + doc.stream = stream_mapping[stream] + changed = True + + if not doc.group and wg: + doc.group = Group.objects.get(acronym=wg) + changed = True + + pubdate = datetime.strptime(rfc_published_date, "%Y-%m-%d") + if not doc.latest_event(type="published_rfc", time=pubdate): + e = DocEvent(doc=doc, type="published_rfc") + e.time = pubdate + e.by = system + e.desc = "RFC published" + e.save() + changed = True + + def parse_relation_list(s): + if not s: + return [] + res = [] + for x in s.split(","): + if x[:3] in ("NIC", "IEN", "STD", "RTR"): + # try translating this to RFCs that we can handle + # sensibly; otherwise we'll have to ignore them + l = DocAlias.objects.filter(name__startswith="rfc", document__docalias__name=x.lower()) + else: + l = DocAlias.objects.filter(name=x.lower()) + + for a in l: + if a not in res: + res.append(a) + return res + + for x in parse_relation_list(obsoletes): + if not RelatedDocument.objects.filter(source=doc, target=x, relationship=relationship_obsoletes): + RelatedDocument.objects.create(source=doc, target=x, relationship=relationship_obsoletes) + changed = True + + for x in parse_relation_list(updates): + if not RelatedDocument.objects.filter(source=doc, target=x, relationship=relationship_updates): + RelatedDocument.objects.create(source=doc, target=x, relationship=relationship_updates) + changed = True + + if also: + for a in also.lower().split(","): + if not DocAlias.objects.filter(name=a): + DocAlias.objects.create(name=a, document=doc) + changed = True + + if has_errata: + if not doc.tags.filter(pk=tag_has_errata.pk): + doc.tags.add(tag_has_errata) + changed = True + else: + if doc.tags.filter(pk=tag_has_errata.pk): + doc.tags.remove(tag_has_errata) + changed = True + + if changed: + if not created: + log("%s changed" % name) + doc.time = datetime.now() + doc.save() + + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + insert_to_database = insert_to_databaseREDESIGN + if __name__ == '__main__': try: log("output from mirror_rfc_index.py:\n") diff --git a/ietf/idrfc/models.py b/ietf/idrfc/models.py index 046f68cd8..c6fb882f0 100644 --- a/ietf/idrfc/models.py +++ b/ietf/idrfc/models.py @@ -96,4 +96,9 @@ class DraftVersions(models.Model): return "DraftVersions"+self.filename+self.revision+str(self.revision_date) class Meta: db_table = "draft_versions_mirror" - + + +from django.conf import settings +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + RfcIndexOld = RfcIndex + from redesign.doc.proxy import RfcIndex diff --git a/ietf/idrfc/templatetags/ballot_icon.py b/ietf/idrfc/templatetags/ballot_icon.py index e75e265fd..8a804d435 100644 --- a/ietf/idrfc/templatetags/ballot_icon.py +++ b/ietf/idrfc/templatetags/ballot_icon.py @@ -32,6 +32,7 @@ from django import template from django.core.urlresolvers import reverse as urlreverse +from django.conf import settings from ietf.idtracker.models import IDInternal, BallotInfo from ietf.idrfc.idrfc_wrapper import position_to_string, BALLOT_ACTIVE_STATES from ietf.idtracker.templatetags.ietf_filters import in_group, timesince_days @@ -40,12 +41,21 @@ register = template.Library() def get_user_adid(context): if 'user' in context and in_group(context['user'], "Area_Director"): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return context['user'].get_profile().id return context['user'].get_profile().iesg_login_id() else: return None def get_user_name(context): if 'user' in context and context['user'].is_authenticated(): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from person.models import Person + try: + return context['user'].get_profile().name + except Person.DoesNotExist: + return None + person = context['user'].get_profile().person() if person: return str(person) @@ -61,7 +71,7 @@ def render_ballot_icon(context, doc): return "" if str(doc.cur_state) not in BALLOT_ACTIVE_STATES: return "" - if doc.rfc_flag: + if doc.rfc_flag and not settings.USE_DB_REDESIGN_PROXY_CLASSES: name = doc.document().filename() else: name = doc.document().filename diff --git a/ietf/idrfc/tests.py b/ietf/idrfc/tests.py index ae3b8d62d..c00ef41a4 100644 --- a/ietf/idrfc/tests.py +++ b/ietf/idrfc/tests.py @@ -774,6 +774,34 @@ class ExpireIDsTestCase(django.test.TestCase): self.assertTrue(in_id_expire_freeze(datetime.datetime(2010, 7, 12, 10, 0))) self.assertTrue(in_id_expire_freeze(datetime.datetime(2010, 7, 25, 0, 0))) self.assertTrue(not in_id_expire_freeze(datetime.datetime(2010, 7, 26, 0, 0))) + + def test_warn_expirable_ids(self): + from ietf.idrfc.expire import get_soon_to_expire_ids, send_expire_warning_for_id + + # hack into almost expirable state + draft = InternetDraft.objects.get(filename="draft-ietf-mipshop-pfmipv6") + draft.status = IDStatus.objects.get(status="Active") + draft.review_by_rfc_editor = 0 + draft.revision_date = datetime.date.today() - datetime.timedelta(days=InternetDraft.DAYS_TO_EXPIRE - 7) + draft.idinternal.cur_state_id = IDState.AD_WATCHING + draft.idinternal.save() + draft.save() + + author = PersonOrOrgInfo.objects.all()[0] + IDAuthor.objects.create(document=draft, person=author, author_order=1) + EmailAddress.objects.create(person_or_org=author, type="I-D", priority=draft.pk, address="author@example.com") + + # test query + documents = list(get_soon_to_expire_ids(14)) + self.assertEquals(len(documents), 1) + + # test send warning + mailbox_before = len(mail_outbox) + + send_expire_warning_for_id(documents[0]) + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("author@example.com" in str(mail_outbox[-1])) def test_expire_ids(self): from ietf.idrfc.expire import get_expired_ids, send_expire_notice_for_id, expire_id @@ -1277,3 +1305,5 @@ class MirrorScriptTestCases(unittest.TestCase,RealDatabaseTest): self.assertEquals(len(refs), 3) print "OK" +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from testsREDESIGN import * diff --git a/ietf/idrfc/testsREDESIGN.py b/ietf/idrfc/testsREDESIGN.py new file mode 100644 index 000000000..c52bc5468 --- /dev/null +++ b/ietf/idrfc/testsREDESIGN.py @@ -0,0 +1,1408 @@ +# Copyright (C) 2009 Nokia Corporation and/or its subsidiary(-ies). +# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com> +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# * Neither the name of the Nokia Corporation and/or its +# subsidiary(-ies) nor the names of its contributors may be used +# to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import unittest +import StringIO +import os, shutil +from datetime import date, timedelta + +import django.test +from django.core.urlresolvers import reverse as urlreverse +from django.conf import settings + +from pyquery import PyQuery + +from ietf.idtracker.models import IDDates +from doc.models import * +from name.models import * +from group.models import * +from person.models import * +from ietf.iesg.models import TelechatDates +from ietf.utils.test_utils import SimpleUrlTestCase, RealDatabaseTest, login_testing_unauthorized +from ietf.utils.test_runner import mail_outbox +from ietf.utils.test_data import make_test_data + +class IdRfcUrlTestCase(SimpleUrlTestCase): + def testUrls(self): + #self.doTestUrls(__file__) + self.doTestUrls(os.path.join(os.path.dirname(os.path.abspath(__file__)), "testurlREDESIGN.list")) + + +class ChangeStateTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_change_state(self): + draft = make_test_data() + draft.iesg_state = IesgDocStateName.objects.get(slug="ad-eval") + draft.save() + + url = urlreverse('doc_change_state', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + first_state = draft.iesg_state + next_states = get_next_iesg_states(first_state) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form select[name=state]')), 1) + + if next_states: + self.assertTrue(len(q('.next-states form input[type=hidden]')) > 0) + + + # faulty post + r = self.client.post(url, dict(state="foobarbaz")) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state, first_state) + + + # change state + events_before = draft.docevent_set.count() + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict(state="review-e")) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state_id, "review-e") + self.assertEquals(draft.docevent_set.count(), events_before + 1) + self.assertTrue("State changed" in draft.docevent_set.all()[0].desc) + self.assertEquals(len(mail_outbox), mailbox_before + 2) + self.assertTrue("State Update Notice" in mail_outbox[-2]['Subject']) + self.assertTrue(draft.name in mail_outbox[-1]['Subject']) + + + # check that we got a previous state now + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('.prev-state form input[name="state"][value="ad-eval"]')), 1) + + + def test_request_last_call(self): + draft = make_test_data() + draft.iesg_state = IesgDocStateName.objects.get(slug="ad-eval") + draft.save() + + self.client.login(remote_user="secretary") + url = urlreverse('doc_change_state', kwargs=dict(name=draft.name)) + + mailbox_before = len(mail_outbox) + + self.assertTrue(not draft.latest_event(type="changed_ballot_writeup_text")) + r = self.client.post(url, dict(state="lc-req")) + self.assertContains(r, "Your request to issue the Last Call") + + # last call text + e = draft.latest_event(WriteupDocEvent, type="changed_last_call_text") + self.assertTrue(e) + self.assertTrue("The IESG has received" in e.text) + self.assertTrue(draft.title in e.text) + self.assertTrue(draft.get_absolute_url() in e.text) + + # approval text + e = draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text") + self.assertTrue(e) + self.assertTrue("The IESG has approved" in e.text) + self.assertTrue(draft.title in e.text) + self.assertTrue(draft.get_absolute_url() in e.text) + + # ballot writeup + e = draft.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text") + self.assertTrue(e) + self.assertTrue("Technical Summary" in e.text) + + # mail notice + self.assertTrue(len(mail_outbox) > mailbox_before) + self.assertTrue("Last Call:" in mail_outbox[-1]['Subject']) + + # comment + self.assertTrue("Last call was requested" in draft.latest_event().desc) + + +class EditInfoTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_edit_info(self): + draft = make_test_data() + url = urlreverse('doc_edit_info', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form select[name=intended_std_level]')), 1) + self.assertEquals(len(q('form input[name=via_rfc_editor]')), 1) + + prev_ad = draft.ad + # faulty post + r = self.client.post(url, dict(ad="123456789")) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form ul.errorlist')) > 0) + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.ad, prev_ad) + + # edit info + events_before = draft.docevent_set.count() + mailbox_before = len(mail_outbox) + + new_ad = Person.objects.get(name="Ad No1") + + r = self.client.post(url, + dict(intended_std_level=str(draft.intended_std_level.pk), + status_date=str(date.today() + timedelta(2)), + via_rfc_editor="1", + ad=str(new_ad.pk), + create_in_state="pub-req", + notify="test@example.com", + note="New note", + telechat_date="", + )) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertTrue(draft.tags.filter(slug="via-rfc")) + self.assertEquals(draft.ad, new_ad) + self.assertEquals(draft.note, "New note") + self.assertTrue(not draft.latest_event(TelechatDocEvent, type="telechat_date")) + self.assertEquals(draft.docevent_set.count(), events_before + 4) + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue(draft.name in mail_outbox[-1]['Subject']) + + def test_edit_telechat_date(self): + draft = make_test_data() + + url = urlreverse('doc_edit_info', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + data = dict(intended_std_level=str(draft.intended_std_level_id), + status_date=str(date.today() + timedelta(2)), + via_rfc_editor="1", + create_in_state="pub-req", + ad=str(draft.ad_id), + notify="test@example.com", + note="", + ) + + # add to telechat + self.assertTrue(not draft.latest_event(TelechatDocEvent, "scheduled_for_telechat")) + data["telechat_date"] = TelechatDates.objects.all()[0].date1.isoformat() + r = self.client.post(url, data) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertTrue(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat")) + self.assertEquals(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date, TelechatDates.objects.all()[0].date1) + + # change telechat + data["telechat_date"] = TelechatDates.objects.all()[0].date2.isoformat() + r = self.client.post(url, data) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date, TelechatDates.objects.all()[0].date2) + + # remove from agenda + data["telechat_date"] = "" + r = self.client.post(url, data) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertTrue(not draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date) + + def test_start_iesg_process_on_draft(self): + draft = make_test_data() + draft.ad = None + draft.iesg_state = None + draft.save() + draft.docevent_set.all().delete() + + url = urlreverse('doc_edit_info', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form select[name=intended_std_level]')), 1) + self.assertEquals(len(q('form input[name=via_rfc_editor]')), 1) + self.assertTrue('@' in q('form input[name=notify]')[0].get('value')) + + # add + events_before = draft.docevent_set.count() + mailbox_before = len(mail_outbox) + + ad = Person.objects.get(name="Aread Irector") + + r = self.client.post(url, + dict(intended_std_level=str(draft.intended_std_level_id), + status_date=str(date.today() + timedelta(2)), + via_rfc_editor="1", + ad=ad.pk, + create_in_state="watching", + notify="test@example.com", + note="This is a note", + telechat_date="", + )) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertTrue(draft.tags.filter(slug="via-rfc")) + self.assertEquals(draft.iesg_state_id, "watching") + self.assertEquals(draft.ad, ad) + self.assertEquals(draft.note, "This is a note") + self.assertTrue(not draft.latest_event(TelechatDocEvent, type="scheduled_for_telechat")) + self.assertEquals(draft.docevent_set.count(), events_before + 4) + events = list(draft.docevent_set.order_by('time', 'id')) + self.assertEquals(events[-4].type, "started_iesg_process") + self.assertEquals(len(mail_outbox), mailbox_before) + + +class ResurrectTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_request_resurrect(self): + draft = make_test_data() + draft.state_id = "expired" + draft.save() + + url = urlreverse('doc_request_resurrect', kwargs=dict(name=draft.name)) + + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form input[type=submit]')), 1) + + + # request resurrect + events_before = draft.docevent_set.count() + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict()) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.docevent_set.count(), events_before + 1) + e = draft.latest_event(type="requested_resurrect") + self.assertTrue(e) + self.assertEquals(e.by, Person.objects.get(name="Aread Irector")) + self.assertTrue("Resurrection" in e.desc) + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("Resurrection" in mail_outbox[-1]['Subject']) + + def test_resurrect(self): + draft = make_test_data() + draft.state_id = "expired" + draft.save() + DocEvent.objects.create(doc=draft, + type="requested_resurrect", + by=Person.objects.get(name="Aread Irector")) + + url = urlreverse('doc_resurrect', kwargs=dict(name=draft.name)) + + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form input[type=submit]')), 1) + + # request resurrect + events_before = draft.docevent_set.count() + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict()) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.docevent_set.count(), events_before + 1) + self.assertEquals(draft.latest_event().type, "completed_resurrect") + self.assertEquals(draft.state_id, "active") + self.assertEquals(len(mail_outbox), mailbox_before + 1) + +class AddCommentTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_add_comment(self): + draft = make_test_data() + url = urlreverse('doc_add_comment', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form textarea[name=comment]')), 1) + + # request resurrect + events_before = draft.docevent_set.count() + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict(comment="This is a test.")) + self.assertEquals(r.status_code, 302) + + self.assertEquals(draft.docevent_set.count(), events_before + 1) + self.assertEquals("This is a test.", draft.latest_event().desc) + self.assertEquals("added_comment", draft.latest_event().type) + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("updated" in mail_outbox[-1]['Subject']) + self.assertTrue(draft.name in mail_outbox[-1]['Subject']) + +class EditPositionTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_edit_position(self): + draft = make_test_data() + url = urlreverse('doc_edit_position', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "ad", url) + + ad = Person.objects.get(name="Aread Irector") + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form input[name=position]')) > 0) + self.assertEquals(len(q('form textarea[name=comment]')), 1) + + # vote + events_before = draft.docevent_set.count() + + r = self.client.post(url, dict(position="discuss", + discuss="This is a discussion test.", + comment="This is a test.")) + self.assertEquals(r.status_code, 302) + + pos = draft.latest_event(BallotPositionDocEvent, ad=ad) + self.assertEquals(pos.pos.slug, "discuss") + self.assertTrue("This is a discussion test." in pos.discuss) + self.assertTrue(pos.discuss_time != None) + self.assertTrue("This is a test." in pos.comment) + self.assertTrue(pos.comment_time != None) + self.assertTrue("New position" in pos.desc) + self.assertEquals(draft.docevent_set.count(), events_before + 3) + + # recast vote + events_before = draft.docevent_set.count() + r = self.client.post(url, dict(position="noobj")) + self.assertEquals(r.status_code, 302) + + pos = draft.latest_event(BallotPositionDocEvent, ad=ad) + self.assertEquals(pos.pos.slug, "noobj") + self.assertEquals(draft.docevent_set.count(), events_before + 1) + self.assertTrue("Position for" in pos.desc) + + # clear vote + events_before = draft.docevent_set.count() + r = self.client.post(url, dict(position="norecord")) + self.assertEquals(r.status_code, 302) + + pos = draft.latest_event(BallotPositionDocEvent, ad=ad) + self.assertEquals(pos.pos.slug, "norecord") + self.assertEquals(draft.docevent_set.count(), events_before + 1) + self.assertTrue("Position for" in pos.desc) + + # change comment + events_before = draft.docevent_set.count() + r = self.client.post(url, dict(position="norecord", comment="New comment.")) + self.assertEquals(r.status_code, 302) + + pos = draft.latest_event(BallotPositionDocEvent, ad=ad) + self.assertEquals(pos.pos.slug, "norecord") + self.assertEquals(draft.docevent_set.count(), events_before + 2) + self.assertTrue("Ballot comment text updated" in pos.desc) + + def test_edit_position_as_secretary(self): + draft = make_test_data() + url = urlreverse('doc_edit_position', kwargs=dict(name=draft.name)) + ad = Person.objects.get(name="Aread Irector") + url += "?ad=%s" % ad.pk + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form input[name=position]')) > 0) + + # vote on behalf of AD + events_before = draft.docevent_set.count() + r = self.client.post(url, dict(position="discuss", discuss="Test discuss text")) + self.assertEquals(r.status_code, 302) + + pos = draft.latest_event(BallotPositionDocEvent, ad=ad) + self.assertEquals(pos.pos.slug, "discuss") + self.assertEquals(pos.discuss, "Test discuss text") + self.assertTrue("New position" in pos.desc) + self.assertTrue("by Sec" in pos.desc) + + def test_send_ballot_comment(self): + draft = make_test_data() + draft.notify = "somebody@example.com" + draft.save() + + ad = Person.objects.get(name="Aread Irector") + + BallotPositionDocEvent.objects.create(doc=draft, type="changed_ballot_position", + by=ad, ad=ad, pos=BallotPositionName.objects.get(slug="yes"), + comment="Test!", + comment_time=datetime.datetime.now()) + + url = urlreverse('doc_send_ballot_comment', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('form input[name="cc"]')) > 0) + + # send + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict(cc="test@example.com", cc_state_change="1")) + self.assertEquals(r.status_code, 302) + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + m = mail_outbox[-1] + self.assertTrue("COMMENT" in m['Subject']) + self.assertTrue(draft.name in m['Subject']) + self.assertTrue("Test!" in str(m)) + + +class DeferBallotTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_defer_ballot(self): + draft = make_test_data() + draft.iesg_state_id = "iesg-eva" + draft.save() + + url = urlreverse('doc_defer_ballot', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + + # defer + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict()) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state_id, "defer") + + self.assertEquals(len(mail_outbox), mailbox_before + 2) + self.assertTrue("State Update" in mail_outbox[-2]['Subject']) + self.assertTrue("Deferred" in mail_outbox[-1]['Subject']) + self.assertTrue(draft.file_tag() in mail_outbox[-1]['Subject']) + + def test_undefer_ballot(self): + draft = make_test_data() + draft.iesg_state_id = "defer" + draft.save() + + url = urlreverse('doc_undefer_ballot', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "ad", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + + # undefer + r = self.client.post(url, dict()) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state_id, "iesg-eva") + +class BallotWriteupsTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_edit_last_call_text(self): + draft = make_test_data() + url = urlreverse('doc_ballot_lastcall', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('textarea[name=last_call_text]')), 1) + self.assertEquals(len(q('input[type=submit][value*="Save Last Call"]')), 1) + # we're secretariat, so we got The Link + self.assertEquals(len(q('a:contains("Make Last Call")')), 1) + + # subject error + r = self.client.post(url, dict( + last_call_text="Subject: test\r\nhello\r\n\r\n", + save_last_call_text="1")) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue(len(q('ul.errorlist')) > 0) + + # save + r = self.client.post(url, dict( + last_call_text="This is a simple test.", + save_last_call_text="1")) + self.assertEquals(r.status_code, 200) + draft = Document.objects.get(name=draft.name) + self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_last_call_text").text) + + # test regenerate + r = self.client.post(url, dict( + last_call_text="This is a simple test.", + regenerate_last_call_text="1")) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + draft = Document.objects.get(name=draft.name) + self.assertTrue("Subject: Last Call" in draft.latest_event(WriteupDocEvent, type="changed_last_call_text").text) + + + def test_request_last_call(self): + draft = make_test_data() + url = urlreverse('doc_ballot_lastcall', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # give us an announcement to send + r = self.client.post(url, dict(regenerate_last_call_text="1")) + self.assertEquals(r.status_code, 200) + + mailbox_before = len(mail_outbox) + + # send + r = self.client.post(url, dict( + last_call_text=draft.latest_event(WriteupDocEvent, type="changed_last_call_text").text, + send_last_call_request="1")) + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state_id, "lc-req") + self.assertEquals(len(mail_outbox), mailbox_before + 3) + self.assertTrue("Last Call" in mail_outbox[-1]['Subject']) + self.assertTrue(draft.name in mail_outbox[-1]['Subject']) + + def test_edit_ballot_writeup(self): + draft = make_test_data() + url = urlreverse('doc_ballot_writeupnotes', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('textarea[name=ballot_writeup]')), 1) + self.assertEquals(len(q('input[type=submit][value*="Save Ballot Writeup"]')), 1) + + # save + r = self.client.post(url, dict( + ballot_writeup="This is a simple test.", + save_ballot_writeup="1")) + self.assertEquals(r.status_code, 200) + draft = Document.objects.get(name=draft.name) + self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text").text) + + def test_issue_ballot(self): + draft = make_test_data() + url = urlreverse('doc_ballot_writeupnotes', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "ad", url) + + def create_pos(num, vote, comment="", discuss=""): + ad = Person.objects.get(name="Ad No%s" % num) + e = BallotPositionDocEvent() + e.doc = draft + e.by = ad + e.ad = ad + e.pos = BallotPositionName.objects.get(slug=vote) + e.type = "changed_ballot_position" + e.comment = comment + if e.comment: + e.comment_time = datetime.datetime.now() + e.discuss = discuss + if e.discuss: + e.discuss_time = datetime.datetime.now() + e.save() + + # active + create_pos(1, "yes", discuss="discuss1 " * 20) + create_pos(2, "noobj", comment="comment2 " * 20) + create_pos(3, "discuss", discuss="discuss3 " * 20, comment="comment3 " * 20) + create_pos(4, "abstain") + create_pos(5, "recuse") + + # inactive + create_pos(9, "yes") + + # we need approval text to be able to submit + e = WriteupDocEvent() + e.doc = draft + e.by = Person.objects.get(name="Aread Irector") + e.type = "changed_ballot_approval_text" + e.text = "The document has been approved." + e.save() + + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict( + ballot_writeup="This is a test.", + issue_ballot="1")) + self.assertEquals(r.status_code, 200) + draft = Document.objects.get(name=draft.name) + + self.assertTrue(draft.latest_event(type="sent_ballot_announcement")) + self.assertEquals(len(mail_outbox), mailbox_before + 2) + issue_email = mail_outbox[-2] + self.assertTrue("Evaluation:" in issue_email['Subject']) + self.assertTrue("comment1" not in str(issue_email)) + self.assertTrue("comment2" in str(issue_email)) + self.assertTrue("comment3" in str(issue_email)) + self.assertTrue("discuss3" in str(issue_email)) + self.assertTrue("This is a test" in str(issue_email)) + self.assertTrue("The document has been approved" in str(issue_email)) + + def test_edit_approval_text(self): + draft = make_test_data() + url = urlreverse('doc_ballot_approvaltext', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('textarea[name=approval_text]')), 1) + self.assertEquals(len(q('input[type=submit][value*="Save Approval"]')), 1) + + # save + r = self.client.post(url, dict( + approval_text="This is a simple test.", + save_approval_text="1")) + self.assertEquals(r.status_code, 200) + draft = Document.objects.get(name=draft.name) + self.assertTrue("This is a simple test" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) + + # test regenerate + r = self.client.post(url, dict(regenerate_approval_text="1")) + self.assertEquals(r.status_code, 200) + draft = Document.objects.get(name=draft.name) + self.assertTrue("Subject: Protocol Action" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) + + # test regenerate when it's a disapprove + draft.iesg_state_id = "nopubadw" + draft.save() + + r = self.client.post(url, dict(regenerate_approval_text="1")) + self.assertEquals(r.status_code, 200) + draft = Document.objects.get(name=draft.name) + self.assertTrue("NOT be published" in draft.latest_event(WriteupDocEvent, type="changed_ballot_approval_text").text) + +class ApproveBallotTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_approve_ballot(self): + draft = make_test_data() + draft.iesg_state_id = "iesg-eva" # make sure it's approvable + draft.save() + url = urlreverse('doc_approve_ballot', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue("Send out the announcement" in q('.actions input[type=submit]')[0].get('value')) + self.assertEquals(len(q('.announcement pre:contains("Subject: Protocol Action")')), 1) + + # approve + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict()) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state_id, "ann") + self.assertEquals(len(mail_outbox), mailbox_before + 4) + self.assertTrue("Protocol Action" in mail_outbox[-2]['Subject']) + # the IANA copy + self.assertTrue("Protocol Action" in mail_outbox[-1]['Subject']) + + def test_disapprove_ballot(self): + draft = make_test_data() + draft.iesg_state_id = "nopubadw" + draft.save() + + url = urlreverse('doc_approve_ballot', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # disapprove (the Martians aren't going to be happy) + mailbox_before = len(mail_outbox) + + r = self.client.post(url, dict()) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state_id, "dead") + self.assertEquals(len(mail_outbox), mailbox_before + 3) + self.assertTrue("NOT be published" in str(mail_outbox[-1])) + +class MakeLastCallTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_make_last_call(self): + draft = make_test_data() + draft.iesg_state_id = "lc-req" + draft.save() + + url = urlreverse('doc_make_last_call', kwargs=dict(name=draft.name)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('input[name=last_call_sent_date]')), 1) + + # make last call + mailbox_before = len(mail_outbox) + + expire_date = q('input[name=last_call_expiration_date]')[0].get("value") + + r = self.client.post(url, + dict(last_call_sent_date=q('input[name=last_call_sent_date]')[0].get("value"), + last_call_expiration_date=expire_date + )) + self.assertEquals(r.status_code, 302) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state.slug, "lc") + self.assertEquals(draft.latest_event(LastCallDocEvent, "sent_last_call").expires.strftime("%Y-%m-%d"), expire_date) + self.assertEquals(len(mail_outbox), mailbox_before + 4) + + self.assertTrue("Last Call" in mail_outbox[-4]['Subject']) + # the IANA copy + self.assertTrue("Last Call" in mail_outbox[-3]['Subject']) + +class ExpireIDsTestCase(django.test.TestCase): + fixtures = ['names'] + + def setUp(self): + self.id_dir = os.path.abspath("tmp-id-dir") + self.archive_dir = os.path.abspath("tmp-id-archive") + os.mkdir(self.id_dir) + os.mkdir(self.archive_dir) + os.mkdir(os.path.join(self.archive_dir, "unknown_ids")) + os.mkdir(os.path.join(self.archive_dir, "deleted_tombstones")) + os.mkdir(os.path.join(self.archive_dir, "expired_without_tombstone")) + + settings.IDSUBMIT_REPOSITORY_PATH = self.id_dir + settings.INTERNET_DRAFT_ARCHIVE_DIR = self.archive_dir + + def tearDown(self): + shutil.rmtree(self.id_dir) + shutil.rmtree(self.archive_dir) + + def write_id_file(self, name, size): + f = open(os.path.join(self.id_dir, name), 'w') + f.write("a" * size) + f.close() + + def test_in_id_expire_freeze(self): + from ietf.idrfc.expire import in_id_expire_freeze + + # dummy id dates + IDDates.objects.create(id=IDDates.SECOND_CUT_OFF, date=datetime.date(2010, 7, 12), description="", f_name="") + IDDates.objects.create(id=IDDates.IETF_MONDAY, date=datetime.date(2010, 7, 26), description="", f_name="") + + self.assertTrue(not in_id_expire_freeze(datetime.datetime(2010, 7, 11, 0, 0))) + self.assertTrue(not in_id_expire_freeze(datetime.datetime(2010, 7, 12, 8, 0))) + self.assertTrue(in_id_expire_freeze(datetime.datetime(2010, 7, 12, 10, 0))) + self.assertTrue(in_id_expire_freeze(datetime.datetime(2010, 7, 25, 0, 0))) + self.assertTrue(not in_id_expire_freeze(datetime.datetime(2010, 7, 26, 0, 0))) + + def test_warn_expirable_ids(self): + from ietf.idrfc.expire import get_soon_to_expire_ids, send_expire_warning_for_id, INTERNET_DRAFT_DAYS_TO_EXPIRE + + draft = make_test_data() + + self.assertEquals(len(list(get_soon_to_expire_ids(14))), 0) + + # hack into expirable state + draft.iesg_state = None + draft.save() + + NewRevisionDocEvent.objects.create( + type="new_revision", + by=Person.objects.get(name="Aread Irector"), + doc=draft, + desc="New revision", + time=datetime.datetime.now() - datetime.timedelta(days=INTERNET_DRAFT_DAYS_TO_EXPIRE - 7), + rev="01" + ) + + self.assertEquals(len(list(get_soon_to_expire_ids(14))), 1) + + # test send warning + mailbox_before = len(mail_outbox) + + send_expire_warning_for_id(draft) + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("aread@ietf.org" in str(mail_outbox[-1])) # author + self.assertTrue("wgchairman@ietf.org" in str(mail_outbox[-1])) + + def test_expire_ids(self): + from ietf.idrfc.expire import get_expired_ids, send_expire_notice_for_id, expire_id, INTERNET_DRAFT_DAYS_TO_EXPIRE + + draft = make_test_data() + + self.assertEquals(len(list(get_expired_ids())), 0) + + # hack into expirable state + draft.iesg_state = None + draft.save() + + NewRevisionDocEvent.objects.create( + type="new_revision", + by=Person.objects.get(name="Aread Irector"), + doc=draft, + desc="New revision", + time=datetime.datetime.now() - datetime.timedelta(days=INTERNET_DRAFT_DAYS_TO_EXPIRE + 1), + rev="01" + ) + + self.assertEquals(len(list(get_expired_ids())), 1) + + draft.iesg_state = IesgDocStateName.objects.get(slug="watching") + draft.save() + + self.assertEquals(len(list(get_expired_ids())), 1) + + # test notice + mailbox_before = len(mail_outbox) + + send_expire_notice_for_id(draft) + + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("expired" in mail_outbox[-1]["Subject"]) + + # test expiry + txt = "%s-%s.txt" % (draft.name, draft.rev) + self.write_id_file(txt, 5000) + + revision_before = draft.rev + + expire_id(draft) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.state_id, "expired") + self.assertEquals(int(draft.rev), int(revision_before) + 1) + self.assertEquals(draft.iesg_state_id, "dead") + self.assertTrue(draft.latest_event(type="expired_document")) + self.assertTrue(not os.path.exists(os.path.join(self.id_dir, txt))) + self.assertTrue(os.path.exists(os.path.join(self.archive_dir, txt))) + new_txt = "%s-%s.txt" % (draft.name, draft.rev) + self.assertTrue(os.path.exists(os.path.join(self.id_dir, new_txt))) + + def test_clean_up_id_files(self): + draft = make_test_data() + + from ietf.idrfc.expire import clean_up_id_files, INTERNET_DRAFT_DAYS_TO_EXPIRE + + # put unknown file + unknown = "draft-i-am-unknown-01.txt" + self.write_id_file(unknown, 5000) + + clean_up_id_files() + + self.assertTrue(not os.path.exists(os.path.join(self.id_dir, unknown))) + self.assertTrue(os.path.exists(os.path.join(self.archive_dir, "unknown_ids", unknown))) + + + # put file with malformed name (no revision) + malformed = draft.name + ".txt" + self.write_id_file(malformed, 5000) + + clean_up_id_files() + + self.assertTrue(not os.path.exists(os.path.join(self.id_dir, malformed))) + self.assertTrue(os.path.exists(os.path.join(self.archive_dir, "unknown_ids", malformed))) + + + # RFC draft + draft.state = DocStateName.objects.get(slug="rfc") + draft.save() + + txt = "%s-%s.txt" % (draft.name, draft.rev) + self.write_id_file(txt, 5000) + pdf = "%s-%s.pdf" % (draft.name, draft.rev) + self.write_id_file(pdf, 5000) + + clean_up_id_files() + + # txt files shouldn't be moved (for some reason) + self.assertTrue(os.path.exists(os.path.join(self.id_dir, txt))) + + self.assertTrue(not os.path.exists(os.path.join(self.id_dir, pdf))) + self.assertTrue(os.path.exists(os.path.join(self.archive_dir, "unknown_ids", pdf))) + + + # expire draft + draft.state = DocStateName.objects.get(slug="expired") + draft.save() + + e = DocEvent() + e.doc = draft + e.by = Person.objects.get(name="(System)") + e.type = "expired_document" + e.text = "Document has expired" + e.time = datetime.date.today() - datetime.timedelta(days=INTERNET_DRAFT_DAYS_TO_EXPIRE + 1) + e.save() + + # expired without tombstone + txt = "%s-%s.txt" % (draft.name, draft.rev) + self.write_id_file(txt, 5000) + + clean_up_id_files() + + self.assertTrue(not os.path.exists(os.path.join(self.id_dir, txt))) + self.assertTrue(os.path.exists(os.path.join(self.archive_dir, "expired_without_tombstone", txt))) + + + # expired with tombstone + revision_before = draft.rev + + txt = "%s-%s.txt" % (draft.name, draft.rev) + self.write_id_file(txt, 1000) # < 1500 means tombstone + + clean_up_id_files() + + self.assertTrue(not os.path.exists(os.path.join(self.id_dir, txt))) + self.assertTrue(os.path.exists(os.path.join(self.archive_dir, "deleted_tombstones", txt))) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(int(draft.rev), int(revision_before) - 1) + self.assertTrue(draft.tags.filter(slug="exp-tomb")) + +class ExpireLastCallTestCase(django.test.TestCase): + fixtures = ['names'] + + def test_expire_last_call(self): + from ietf.idrfc.lastcall import get_expired_last_calls, expire_last_call + + # check that non-expirable drafts aren't expired + + draft = make_test_data() + draft.iesg_state_id = "lc" + draft.save() + + secretary = Person.objects.get(name="Sec Retary") + + self.assertEquals(len(list(get_expired_last_calls())), 0) + + e = LastCallDocEvent() + e.doc = draft + e.by = secretary + e.type = "sent_last_call" + e.text = "Last call sent" + e.expires = datetime.datetime.now() + datetime.timedelta(days=14) + e.save() + + self.assertEquals(len(list(get_expired_last_calls())), 0) + + # test expired + e = LastCallDocEvent() + e.doc = draft + e.by = secretary + e.type = "sent_last_call" + e.text = "Last call sent" + e.expires = datetime.datetime.now() + e.save() + + drafts = list(get_expired_last_calls()) + self.assertEquals(len(drafts), 1) + + # expire it + mailbox_before = len(mail_outbox) + events_before = draft.docevent_set.count() + + expire_last_call(drafts[0]) + + draft = Document.objects.get(name=draft.name) + self.assertEquals(draft.iesg_state.slug, "writeupw") + self.assertEquals(draft.docevent_set.count(), events_before + 1) + self.assertEquals(len(mail_outbox), mailbox_before + 1) + self.assertTrue("Last Call Expired" in mail_outbox[-1]["Subject"]) + + + +TEST_RFC_INDEX = '''<?xml version="1.0" encoding="UTF-8"?> +<rfc-index xmlns="http://www.rfc-editor.org/rfc-index" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.rfc-editor.org/rfc-index + http://www.rfc-editor.org/rfc-index.xsd"> + <bcp-entry> + <doc-id>BCP0110</doc-id> + <is-also> + <doc-id>RFC4170</doc-id> + </is-also> + </bcp-entry> + <bcp-entry> + <doc-id>BCP0111</doc-id> + <is-also> + <doc-id>RFC4181</doc-id> + <doc-id>RFC4841</doc-id> + </is-also> + </bcp-entry> + <fyi-entry> + <doc-id>FYI0038</doc-id> + <is-also> + <doc-id>RFC3098</doc-id> + </is-also> + </fyi-entry> + <rfc-entry> + <doc-id>RFC1938</doc-id> + <title>A One-Time Password System</title> + <author> + <name>N. Haller</name> + </author> + <author> + <name>C. Metz</name> + </author> + <date> + <month>May</month> + <year>1996</year> + </date> + <format> + <file-format>ASCII</file-format> + <char-count>44844</char-count> + <page-count>18</page-count> + </format> + <keywords> + <kw>OTP</kw> + <kw>authentication</kw> + <kw>S/KEY</kw> + </keywords> + <abstract><p>This document describes a one-time password authentication system (OTP). [STANDARDS-TRACK]</p></abstract> + <obsoleted-by> + <doc-id>RFC2289</doc-id> + </obsoleted-by> + <current-status>PROPOSED STANDARD</current-status> + <publication-status>PROPOSED STANDARD</publication-status> + <stream>Legacy</stream> + </rfc-entry> + <rfc-entry> + <doc-id>RFC2289</doc-id> + <title>A One-Time Password System</title> + <author> + <name>N. Haller</name> + </author> + <author> + <name>C. Metz</name> + </author> + <author> + <name>P. Nesser</name> + </author> + <author> + <name>M. Straw</name> + </author> + <date> + <month>February</month> + <year>1998</year> + </date> + <format> + <file-format>ASCII</file-format> + <char-count>56495</char-count> + <page-count>25</page-count> + </format> + <keywords> + <kw>ONE-PASS</kw> + <kw>authentication</kw> + <kw>OTP</kw> + <kw>replay</kw> + <kw>attach</kw> + </keywords> + <abstract><p>This document describes a one-time password authentication system (OTP). The system provides authentication for system access (login) and other applications requiring authentication that is secure against passive attacks based on replaying captured reusable passwords. [STANDARDS- TRACK]</p></abstract> + <obsoletes> + <doc-id>RFC1938</doc-id> + </obsoletes> + <is-also> + <doc-id>STD0061</doc-id> + </is-also> + <current-status>STANDARD</current-status> + <publication-status>DRAFT STANDARD</publication-status> + <stream>Legacy</stream> + </rfc-entry> + <rfc-entry> + <doc-id>RFC3098</doc-id> + <title>How to Advertise Responsibly Using E-Mail and Newsgroups or - how NOT to $$$$$ MAKE ENEMIES FAST! $$$$$</title> + <author> + <name>T. Gavin</name> + </author> + <author> + <name>D. Eastlake 3rd</name> + </author> + <author> + <name>S. Hambridge</name> + </author> + <date> + <month>April</month> + <year>2001</year> + </date> + <format> + <file-format>ASCII</file-format> + <char-count>64687</char-count> + <page-count>28</page-count> + </format> + <keywords> + <kw>internet</kw> + <kw>marketing</kw> + <kw>users</kw> + <kw>service</kw> + <kw>providers</kw> + <kw>isps</kw> + </keywords> + <abstract><p>This memo offers useful suggestions for responsible advertising techniques that can be used via the internet in an environment where the advertiser, recipients, and the Internet Community can coexist in a productive and mutually respectful fashion. This memo provides information for the Internet community.</p></abstract> + <draft>draft-ietf-run-adverts-02</draft> + <is-also> + <doc-id>FYI0038</doc-id> + </is-also> + <current-status>INFORMATIONAL</current-status> + <publication-status>INFORMATIONAL</publication-status> + <stream>Legacy</stream> + </rfc-entry> + <rfc-entry> + <doc-id>RFC4170</doc-id> + <title>Tunneling Multiplexed Compressed RTP (TCRTP)</title> + <author> + <name>B. Thompson</name> + </author> + <author> + <name>T. Koren</name> + </author> + <author> + <name>D. Wing</name> + </author> + <date> + <month>November</month> + <year>2005</year> + </date> + <format> + <file-format>ASCII</file-format> + <char-count>48990</char-count> + <page-count>24</page-count> + </format> + <keywords> + <kw>real-time transport protocol</kw> + </keywords> + <abstract><p>This document describes a method to improve the bandwidth utilization of RTP streams over network paths that carry multiple Real-time Transport Protocol (RTP) streams in parallel between two endpoints, as in voice trunking. The method combines standard protocols that provide compression, multiplexing, and tunneling over a network path for the purpose of reducing the bandwidth used when multiple RTP streams are carried over that path. This document specifies an Internet Best Current Practices for the Internet Community, and requests discussion and suggestions for improvements.</p></abstract> + <draft>draft-ietf-avt-tcrtp-08</draft> + <is-also> + <doc-id>BCP0110</doc-id> + </is-also> + <current-status>BEST CURRENT PRACTICE</current-status> + <publication-status>BEST CURRENT PRACTICE</publication-status> + <stream>IETF</stream> + <area>rai</area> + <wg_acronym>avt</wg_acronym> + </rfc-entry> + <rfc-entry> + <doc-id>RFC4181</doc-id> + <title>Guidelines for Authors and Reviewers of MIB Documents</title> + <author> + <name>C. Heard</name> + <title>Editor</title> + </author> + <date> + <month>September</month> + <year>2005</year> + </date> + <format> + <file-format>ASCII</file-format> + <char-count>102521</char-count> + <page-count>42</page-count> + </format> + <keywords> + <kw>standards-track specifications</kw> + <kw>management information base</kw> + <kw>review</kw> + </keywords> + <abstract><p>This memo provides guidelines for authors and reviewers of IETF standards-track specifications containing MIB modules. Applicable portions may be used as a basis for reviews of other MIB documents. This document specifies an Internet Best Current Practices for the Internet Community, and requests discussion and suggestions for improvements.</p></abstract> + <draft>draft-ietf-ops-mib-review-guidelines-04</draft> + <updated-by> + <doc-id>RFC4841</doc-id> + </updated-by> + <is-also> + <doc-id>BCP0111</doc-id> + </is-also> + <current-status>BEST CURRENT PRACTICE</current-status> + <publication-status>BEST CURRENT PRACTICE</publication-status> + <stream>IETF</stream> + <area>rtg</area> + <wg_acronym>ospf</wg_acronym> + <errata-url>http://www.rfc-editor.org/errata_search.php?rfc=4181</errata-url> + </rfc-entry> + <rfc-entry> + <doc-id>RFC4841</doc-id> + <title>RFC 4181 Update to Recognize the IETF Trust</title> + <author> + <name>C. Heard</name> + <title>Editor</title> + </author> + <date> + <month>March</month> + <year>2007</year> + </date> + <format> + <file-format>ASCII</file-format> + <char-count>4414</char-count> + <page-count>3</page-count> + </format> + <keywords> + <kw>management information base</kw> + <kw> standards-track specifications</kw> + <kw>mib review</kw> + </keywords> + <abstract><p>This document updates RFC 4181, "Guidelines for Authors and Reviewers of MIB Documents", to recognize the creation of the IETF Trust. This document specifies an Internet Best Current Practices for the Internet Community, and requests discussion and suggestions for improvements.</p></abstract> + <draft>draft-heard-rfc4181-update-00</draft> + <updates> + <doc-id>RFC4181</doc-id> + </updates> + <is-also> + <doc-id>BCP0111</doc-id> + </is-also> + <current-status>BEST CURRENT PRACTICE</current-status> + <publication-status>BEST CURRENT PRACTICE</publication-status> + <stream>IETF</stream> + <wg_acronym>NON WORKING GROUP</wg_acronym> + </rfc-entry> + <std-entry> + <doc-id>STD0061</doc-id> + <title>A One-Time Password System</title> + <is-also> + <doc-id>RFC2289</doc-id> + </is-also> + </std-entry> +</rfc-index> +''' + +TEST_QUEUE = '''<rfc-editor-queue xmlns="http://www.rfc-editor.org/rfc-editor-queue"> +<section name="IETF STREAM: WORKING GROUP STANDARDS TRACK"> +<entry xml:id="draft-ietf-sipping-app-interaction-framework"> +<draft>draft-ietf-sipping-app-interaction-framework-05.txt</draft> +<date-received>2005-10-17</date-received> +<state>EDIT</state> +<normRef> +<ref-name>draft-ietf-sip-gruu</ref-name> +<ref-state>IN-QUEUE</ref-state> +</normRef> +<authors>J. Rosenberg</authors> +<title> +A Framework for Application Interaction in the Session Initiation Protocol (SIP) +</title> +<bytes>94672</bytes> +<source>Session Initiation Proposal Investigation</source> +</entry> +</section> +<section name="IETF STREAM: NON-WORKING GROUP STANDARDS TRACK"> +<entry xml:id="draft-ietf-sip-gruu"> +<draft>draft-ietf-sip-gruu-15.txt</draft> +<date-received>2007-10-15</date-received> +<state>MISSREF</state> +<normRef> +<ref-name>draft-ietf-sip-outbound</ref-name> +<ref-state>NOT-RECEIVED</ref-state> +</normRef> +<authors>J. Rosenberg</authors> +<title> +Obtaining and Using Globally Routable User Agent (UA) URIs (GRUU) in the Session Initiation Protocol (SIP) +</title> +<bytes>95501</bytes> +<source>Session Initiation Protocol</source> +</entry> +</section> +<section name="IETF STREAM: WORKING GROUP INFORMATIONAL/EXPERIMENTAL/BCP"> +</section> +<section name="IETF STREAM: NON-WORKING GROUP INFORMATIONAL/EXPERIMENTAL/BCP"> +<entry xml:id="draft-thomson-beep-async"> +<draft>draft-thomson-beep-async-02.txt</draft> +<date-received>2009-05-12</date-received> +<state>EDIT</state> +<state>IANA</state> +<authors>M. Thomson</authors> +<title> +Asynchronous Channels for the Blocks Extensible Exchange Protocol (BEEP) +</title> +<bytes>17237</bytes> +<source>IETF - NON WORKING GROUP</source> +</entry> +</section> +<section name="IAB STREAM"> +</section> +<section name="IRTF STREAM"> +</section> +<section name="INDEPENDENT SUBMISSIONS"> +</section> +</rfc-editor-queue> +''' + +class MirrorScriptTestCases(unittest.TestCase,RealDatabaseTest): + + def setUp(self): + self.setUpRealDatabase() + def tearDown(self): + self.tearDownRealDatabase() + + def testRfcIndex(self): + print " Testing rfc-index.xml parsing" + from ietf.idrfc.mirror_rfc_index import parse + data = parse(StringIO.StringIO(TEST_RFC_INDEX)) + self.assertEquals(len(data), 6) + print "OK" + + def testRfcEditorQueue(self): + print " Testing queue2.xml parsing" + from ietf.idrfc.mirror_rfc_editor_queue import parse_all + (drafts,refs) = parse_all(StringIO.StringIO(TEST_QUEUE)) + self.assertEquals(len(drafts), 3) + self.assertEquals(len(refs), 3) + print "OK" + diff --git a/ietf/idrfc/testurlREDESIGN.list b/ietf/idrfc/testurlREDESIGN.list new file mode 100644 index 000000000..10a54f1cd --- /dev/null +++ b/ietf/idrfc/testurlREDESIGN.list @@ -0,0 +1,88 @@ +200 / +200 /doc/ +200,heavy /doc/all/ +200,heavy /doc/active/ + +# draft that's now RFC +200 /doc/draft-ietf-avt-rtp-atrac-family/ +200 /doc/draft-ietf-avt-rtp-atrac-family/doc.json +200 /doc/draft-ietf-avt-rtp-atrac-family/ballot.json +200 /doc/draft-ietf-avt-rtp-atrac-family/_ballot.data + +# replaced draft, never went to IESG +200 /doc/draft-eronen-mobike-mopo/ +404 /doc/draft-eronen-mobike-mopo/ballot.json +404 /doc/draft-eronen-mobike-mopo/_ballot.data + +# expired draft +200 /doc/draft-eronen-eap-sim-aka-80211/ + +# Normal RFC +200 /doc/rfc4739/ +200 /doc/rfc4739/doc.json +200 /doc/rfc4739/ballot.json # has ballot from I-D +200 /doc/rfc4739/_ballot.data + +# RFC that's evaluated in IESG +200 /doc/rfc3852/ +200 /doc/rfc3852/doc.json +200 /doc/rfc3852/ballot.json +200 /doc/rfc3852/_ballot.data + +# old RFC +200 /doc/rfc822/ +200 /doc/rfc822/doc.json + +# ballot sets +200 /doc/rfc3550/ballot.json +200 /doc/rfc3550/_ballot.data +200 /doc/rfc3551/ballot.json +200 /doc/rfc3551/_ballot.data +200 /doc/draft-irtf-dtnrg-ltp/ballot.json +200 /doc/draft-irtf-dtnrg-ltp/_ballot.data + +# file formats +200 /doc/rfc9/ # PDF only +200 /doc/rfc2490/ # TXT+PDF+PS +200 /doc/rfc500/ # not online + +404 /doc/draft-no-such-draft/ +404 /doc/rfc4637/ + +200 /doc/rfc2444/doc.json # foreignkey problem with Django 1.x + +# current AD -- needs to be updated at some point +200 /doc/ad/robert.sparks/ +# former AD +200 /doc/ad/sam.hartman/ +404 /doc/ad/no.body/ + +# ballot exists, but it's not issued +404 /doc/draft-ietf-aaa-diameter-api/ballot.json +404 /doc/draft-ietf-aaa-diameter-api/_ballot.data +# ballot does not exist +404 /doc/draft-zeilenga-cldap/ballot.json +404 /doc/draft-zeilenga-cldap/_ballot.data +# comment with created_by=999 +200 /doc/draft-ietf-l3vpn-2547bis-mcast-bgp/ +# comment with created_by=0 (and no idinternal entry) +200 /doc/draft-ietf-proto-wgdocument-states/ + +200 /doc/search/ +200 /doc/search/?rfcs=on&name=snmp +200 /doc/search/?rfcs=on&name=nfs&by=ad&ad=104942 +200 /doc/search/?activeDrafts=on&name=sipping +200 /doc/search/?oldDrafts=on&name=tls +200 /doc/search/?activeDrafts=on&oldDrafts=on&ad=104942&by=ad +200 /doc/search/?activeDrafts=on&state=iesg-eva&by=state +200 /doc/search/?activeDrafts=on&oldDrafts=on&subState=need-rev&by=state +200 /doc/search/?activeDrafts=on&oldDrafts=on&rfcs=on&ad=104942&name=nfs&by=ad +200 /doc/search/?rfcs=on&group=tls&by=group +200 /doc/search/?activeDrafts=on&group=tls&by=group +200 /doc/search/?activeDrafts=on&oldDrafts=on&rfcs=on&author=eronen&by=author +200 /doc/search/?activeDrafts=on&oldDrafts=on&rfcs=on&area=934&name=ldap&by=area +200 /doc/search/?activeDrafts=on&name=asdfsadfsdfasdf +200 /doc/search/?activeDrafts=on&name=%EF%BD%8C #non-ASCII + +# Test case for missing publication date +200 /doc/search/?oldDrafts=on&name=ppvpn diff --git a/ietf/idrfc/utils.py b/ietf/idrfc/utils.py index 1deaa50fa..c19629081 100644 --- a/ietf/idrfc/utils.py +++ b/ietf/idrfc/utils.py @@ -1,3 +1,5 @@ +from django.conf import settings + from ietf.idtracker.models import InternetDraft, DocumentComment, BallotInfo, IESGLogin from ietf.idrfc.mails import * @@ -60,6 +62,31 @@ def log_state_changed(request, doc, by, email_watch_list=True, note=''): return change +def log_state_changedREDESIGN(request, doc, by, prev_iesg_state, note=''): + from doc.models import DocEvent + + e = DocEvent(doc=doc, by=by) + e.type = "changed_document" + e.desc = u"State changed to <b>%s</b> from %s" % ( + doc.iesg_state.name, + prev_iesg_state.name if prev_iesg_state else "None") + + if note: + e.desc += "<br>%s" % note + + if doc.iesg_state_id == "lc": + writeup = doc.latest_event(WriteupDocEvent, type="changed_last_call_text") + if writeup: + e.desc += "<br><br><b>The following Last Call Announcement was sent out:</b><br><br>" + e.desc += writeup.text.replace("\n", "<br><br>") + + e.save() + return e + + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + log_state_changed = log_state_changedREDESIGN + def update_telechat(request, idinternal, new_telechat_date, new_returning_item=None): on_agenda = bool(new_telechat_date) @@ -95,3 +122,58 @@ def update_telechat(request, idinternal, new_telechat_date, new_returning_item=N (new_telechat_date, idinternal.telechat_date)) idinternal.telechat_date = new_telechat_date + +def update_telechatREDESIGN(request, doc, by, new_telechat_date, new_returning_item=None): + from doc.models import TelechatDocEvent + + on_agenda = bool(new_telechat_date) + + prev = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat") + prev_returning = bool(prev and prev.returning_item) + prev_telechat = prev.telechat_date if prev else None + prev_agenda = bool(prev_telechat) + + returning_item_changed = bool(new_returning_item != None and new_returning_item != prev_returning) + + if new_returning_item == None: + returning = prev_returning + else: + returning = new_returning_item + + if returning == prev_returning and new_telechat_date == prev_telechat: + # fully updated, nothing to do + return + + # auto-update returning item + if (not returning_item_changed and on_agenda and prev_agenda + and new_telechat_date != prev_telechat): + returning = True + + e = TelechatDocEvent() + e.type = "scheduled_for_telechat" + e.by = by + e.doc = doc + e.returning_item = returning + e.telechat_date = new_telechat_date + + if on_agenda != prev_agenda: + if on_agenda: + e.desc = "Placed on agenda for telechat - %s by %s" % ( + new_telechat_date, by.name) + else: + e.desc = "Removed from agenda for telechat by %s" % by.name + elif on_agenda and new_telechat_date != prev_telechat: + e.desc = "Telechat date has been changed to <b>%s</b> from <b>%s</b> by %s" % ( + new_telechat_date, prev_telechat, by.name) + else: + # we didn't reschedule but flipped returning item bit - let's + # just explain that + if returning: + e.desc = "Added as returning item on telechat by %s" % by.name + else: + e.desc = "Removed as returning item on telechat by %s" % by.name + + e.save() + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + update_telechat = update_telechatREDESIGN diff --git a/ietf/idrfc/views_ballot.py b/ietf/idrfc/views_ballot.py index 6c4aaad63..e3a3cc28b 100644 --- a/ietf/idrfc/views_ballot.py +++ b/ietf/idrfc/views_ballot.py @@ -10,11 +10,12 @@ from django.template.loader import render_to_string from django.template import RequestContext from django import forms from django.utils.html import strip_tags +from django.conf import settings -from ietf import settings from ietf.utils.mail import send_mail_text, send_mail_preformatted from ietf.ietfauth.decorators import group_required from ietf.idtracker.templatetags.ietf_filters import in_group +from ietf.ietfauth.decorators import has_role from ietf.idtracker.models import * from ietf.iesg.models import * from ietf.ipr.models import IprDetail @@ -24,6 +25,9 @@ from ietf.idrfc.utils import * from ietf.idrfc.lastcall import request_last_call from ietf.idrfc.idrfc_wrapper import BallotWrapper +from doc.models import Document, DocEvent, BallotPositionDocEvent, LastCallDocEvent, save_document_in_history +from name.models import BallotPositionName, IesgDocStateName + BALLOT_CHOICES = (("yes", "Yes"), ("noobj", "No Objection"), @@ -88,14 +92,13 @@ def edit_position(request, name): if not ad_username: raise Http404() ad = get_object_or_404(IESGLogin, login_name=ad_username) - + pos, discuss, comment = get_ballot_info(doc.idinternal.ballot, ad) if request.method == 'POST': form = EditPositionForm(request.POST) if form.is_valid(): - - # save the vote + # save the vote clean = form.cleaned_data if clean['return_to_url']: @@ -206,6 +209,155 @@ def edit_position(request, name): ), context_instance=RequestContext(request)) +class EditPositionFormREDESIGN(forms.Form): + position = forms.ModelChoiceField(queryset=BallotPositionName.objects.all(), widget=forms.RadioSelect, initial="norecord", required=True) + discuss = forms.CharField(required=False, widget=forms.Textarea) + comment = forms.CharField(required=False, widget=forms.Textarea) + return_to_url = forms.CharField(required=False, widget=forms.HiddenInput) + + def clean_discuss(self): + entered_discuss = self.cleaned_data["discuss"] + entered_pos = self.cleaned_data["position"] + if entered_pos.slug == "discuss" and not entered_discuss: + raise forms.ValidationError("You must enter a non-empty discuss") + return entered_discuss + +@group_required('Area_Director','Secretariat') +def edit_positionREDESIGN(request, name): + """Vote and edit discuss and comment on Internet Draft as Area Director.""" + doc = get_object_or_404(Document, docalias__name=name) + started_process = doc.latest_event(type="started_iesg_process") + if not doc.iesg_state or not started_process: + raise Http404() + + ad = login = request.user.get_profile() + + if 'HTTP_REFERER' in request.META: + return_to_url = request.META['HTTP_REFERER'] + else: + return_to_url = doc.get_absolute_url() + + # if we're in the Secretariat, we can select an AD to act as stand-in for + if not has_role(request.user, "Area Director"): + ad_id = request.GET.get('ad') + if not ad_id: + raise Http404() + from person.models import Person + ad = get_object_or_404(Person, pk=ad_id) + + old_pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, time__gte=started_process.time) + + if request.method == 'POST': + form = EditPositionForm(request.POST) + if form.is_valid(): + + # save the vote + clean = form.cleaned_data + + if clean['return_to_url']: + return_to_url = clean['return_to_url'] + + pos = BallotPositionDocEvent(doc=doc, by=login) + pos.type = "changed_ballot_position" + pos.ad = ad + pos.pos = clean["position"] + pos.comment = clean["comment"].strip() + pos.comment_time = old_pos.comment_time if old_pos else None + pos.discuss = clean["discuss"].strip() + pos.discuss_time = old_pos.discuss_time if old_pos else None + + changes = [] + added_events = [] + # possibly add discuss/comment comments to history trail + # so it's easy to see + old_comment = old_pos.comment if old_pos else "" + if pos.comment != old_comment: + pos.comment_time = pos.time + changes.append("comment") + + if pos.comment: + e = DocEvent(doc=doc) + e.by = ad # otherwise we can't see who's saying it + e.type = "added_comment" + e.desc = "[Ballot comment]\n" + pos.comment + added_events.append(e) + + old_discuss = old_pos.discuss if old_pos else "" + if pos.discuss != old_discuss: + pos.discuss_time = pos.time + changes.append("discuss") + + if pos.discuss: + e = DocEvent(doc=doc, by=login) + e.by = ad # otherwise we can't see who's saying it + e.type = "added_comment" + e.desc = "[Ballot discuss]\n" + pos.discuss + added_events.append(e) + + # figure out a description + if not old_pos and pos.pos.slug != "norecord": + pos.desc = u"[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.name) + elif old_pos and pos.pos != old_pos.pos: + pos.desc = "[Ballot Position Update] Position for %s has been changed to %s from %s" % (pos.ad.name, pos.pos.name, old_pos.pos.name) + + if not pos.desc and changes: + pos.desc = u"Ballot %s text updated for %s" % (u" and ".join(changes), ad.name) + + # only add new event if we actually got a change + if pos.desc: + if login != ad: + pos.desc += u" by %s" % login.name + + pos.save() + + for e in added_events: + e.save() # save them after the position is saved to get later id + + doc.time = pos.time + doc.save() + + if request.POST.get("send_mail"): + qstr = "?return_to_url=%s" % return_to_url + if request.GET.get('ad'): + qstr += "&ad=%s" % request.GET.get('ad') + return HttpResponseRedirect(urlreverse("doc_send_ballot_comment", kwargs=dict(name=doc.name)) + qstr) + elif request.POST.get("Defer"): + return HttpResponseRedirect(urlreverse("doc_defer_ballot", kwargs=dict(name=doc))) + elif request.POST.get("Undefer"): + return HttpResponseRedirect(urlreverse("doc_undefer_ballot", kwargs=dict(name=doc))) + else: + return HttpResponseRedirect(return_to_url) + else: + initial = {} + if old_pos: + initial['position'] = old_pos.pos.slug + initial['discuss'] = old_pos.discuss + initial['comment'] = old_pos.comment + + if return_to_url: + initial['return_to_url'] = return_to_url + + form = EditPositionForm(initial=initial) + + ballot_deferred = None + if doc.iesg_state_id == "defer": + ballot_deferred = doc.latest_event(type="changed_document", desc__startswith="State changed to <b>IESG Evaluation - Defer</b>") + + return render_to_response('idrfc/edit_positionREDESIGN.html', + dict(doc=doc, + form=form, + ad=ad, + return_to_url=return_to_url, + old_pos=old_pos, + ballot_deferred=ballot_deferred, + ), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + edit_position = edit_positionREDESIGN + EditPositionForm = EditPositionFormREDESIGN + + @group_required('Area_Director','Secretariat') def send_ballot_comment(request, name): """Email Internet Draft ballot discuss/comment for area director.""" @@ -278,6 +430,83 @@ def send_ballot_comment(request, name): ), context_instance=RequestContext(request)) +@group_required('Area_Director','Secretariat') +def send_ballot_commentREDESIGN(request, name): + """Email Internet Draft ballot discuss/comment for area director.""" + doc = get_object_or_404(Document, docalias__name=name) + started_process = doc.latest_event(type="started_iesg_process") + if not started_process: + raise Http404() + + ad = login = request.user.get_profile() + + return_to_url = request.GET.get('return_to_url') + if not return_to_url: + return_to_url = doc.get_absolute_url() + + if 'HTTP_REFERER' in request.META: + back_url = request.META['HTTP_REFERER'] + else: + back_url = doc.get_absolute_url() + + # if we're in the Secretariat, we can select an AD to act as stand-in for + if not has_role(request.user, "Area Director"): + ad_id = request.GET.get('ad') + if not ad_id: + raise Http404() + from person.models import Person + ad = get_object_or_404(Person, pk=ad_id) + + pos = doc.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad, time__gte=started_process.time) + if not pos: + raise Http404() + + subj = [] + d = "" + if pos.pos == "discuss" and pos.discuss: + d = pos.discuss + subj.append("DISCUSS") + c = "" + if pos.comment: + c = pos.comment + subj.append("COMMENT") + + ad_name_genitive = ad.name + "'" if ad.name.endswith('s') else ad.name + "'s" + subject = "%s %s on %s" % (ad_name_genitive, pos.pos.name if pos.pos else "No Position", doc.name + "-" + doc.rev) + if subj: + subject += ": (with %s)" % " and ".join(subj) + + doc.filename = doc.name # compatibility attributes + doc.revision_display = doc.rev + body = render_to_string("idrfc/ballot_comment_mail.txt", + dict(discuss=d, comment=c, ad=ad.name, doc=doc, pos=pos.pos)) + frm = ad.formatted_email() + to = "The IESG <iesg@ietf.org>" + + if request.method == 'POST': + cc = [x.strip() for x in request.POST.get("cc", "").split(',') if x.strip()] + if request.POST.get("cc_state_change") and doc.notify: + cc.extend(doc.notify.split(',')) + + send_mail_text(request, to, frm, subject, body, cc=", ".join(cc)) + + return HttpResponseRedirect(return_to_url) + + return render_to_response('idrfc/send_ballot_commentREDESIGN.html', + dict(doc=doc, + subject=subject, + body=body, + frm=frm, + to=to, + ad=ad, + can_send=d or c, + back_url=back_url, + ), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + send_ballot_comment = send_ballot_commentREDESIGN + @group_required('Area_Director','Secretariat') def defer_ballot(request, name): @@ -309,9 +538,46 @@ def defer_ballot(request, name): return render_to_response('idrfc/defer_ballot.html', dict(doc=doc, - telechat_date=telechat_date), + telechat_date=telechat_date, + back_url=doc.idinternal.get_absolute_url()), context_instance=RequestContext(request)) +@group_required('Area_Director','Secretariat') +def defer_ballotREDESIGN(request, name): + """Signal post-pone of Internet Draft ballot, notifying relevant parties.""" + doc = get_object_or_404(Document, docalias__name=name) + if not doc.iesg_state: + raise Http404() + + login = request.user.get_profile() + telechat_date = TelechatDates.objects.all()[0].date2 + + if request.method == 'POST': + save_document_in_history(doc) + + prev = doc.iesg_state + doc.iesg_state = IesgDocStateName.objects.get(slug='defer') + e = log_state_changed(request, doc, login, prev) + + doc.time = e.time + doc.save() + + email_state_changed(request, doc, e.desc) + + update_telechat(request, doc, login, telechat_date) + email_ballot_deferred(request, doc, login.name, telechat_date) + + return HttpResponseRedirect(doc.get_absolute_url()) + + return render_to_response('idrfc/defer_ballot.html', + dict(doc=doc, + telechat_date=telechat_date, + back_url=doc.get_absolute_url()), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + defer_ballot = defer_ballotREDESIGN + @group_required('Area_Director','Secretariat') def undefer_ballot(request, name): """Delete deferral of Internet Draft ballot.""" @@ -336,9 +602,45 @@ def undefer_ballot(request, name): return HttpResponseRedirect(doc.idinternal.get_absolute_url()) return render_to_response('idrfc/undefer_ballot.html', - dict(doc=doc,telechat_date=telechat_date), + dict(doc=doc, + telechat_date=telechat_date, + back_url=doc.idinternal.get_absolute_url()), context_instance=RequestContext(request)) +@group_required('Area_Director','Secretariat') +def undefer_ballotREDESIGN(request, name): + """Delete deferral of Internet Draft ballot.""" + doc = get_object_or_404(Document, docalias__name=name) + if not doc.iesg_state: + raise Http404() + + login = request.user.get_profile() + telechat_date = TelechatDates.objects.all()[0].date1 + + if request.method == 'POST': + save_document_in_history(doc) + + prev = doc.iesg_state + doc.iesg_state = IesgDocStateName.objects.get(slug='iesg-eva') + e = log_state_changed(request, doc, login, prev) + + doc.time = e.time + doc.save() + + email_state_changed(request, doc, e.desc) + + return HttpResponseRedirect(doc.get_absolute_url()) + + return render_to_response('idrfc/undefer_ballot.html', + dict(doc=doc, + telechat_date=telechat_date, + back_url=doc.get_absolute_url()), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + undefer_ballot = undefer_ballotREDESIGN + + class LastCallTextForm(forms.ModelForm): def clean_last_call_text(self): lines = self.cleaned_data["last_call_text"].split("\r\n") @@ -424,6 +726,7 @@ def lastcalltext(request, name): return render_to_response('idrfc/ballot_lastcalltext.html', dict(doc=doc, + back_url=doc.idinternal.get_absolute_url(), ballot=ballot, last_call_form=last_call_form, can_request_last_call=can_request_last_call, @@ -432,6 +735,101 @@ def lastcalltext(request, name): ), context_instance=RequestContext(request)) + +class LastCallTextFormREDESIGN(forms.Form): + last_call_text = forms.CharField(widget=forms.Textarea, required=True) + + def clean_last_call_text(self): + lines = self.cleaned_data["last_call_text"].split("\r\n") + for l, next in zip(lines, lines[1:]): + if l.startswith('Subject:') and next.strip(): + raise forms.ValidationError("Subject line appears to have a line break, please make sure there is no line breaks in the subject line and that it is followed by an empty line.") + + return self.cleaned_data["last_call_text"].replace("\r", "") + +@group_required('Area_Director','Secretariat') +def lastcalltextREDESIGN(request, name): + """Editing of the last call text""" + doc = get_object_or_404(Document, docalias__name=name) + if not doc.iesg_state: + raise Http404() + + login = request.user.get_profile() + + existing = doc.latest_event(WriteupDocEvent, type="changed_last_call_text") + if not existing: + existing = generate_last_call_announcement(request, doc) + + form = LastCallTextForm(initial=dict(last_call_text=existing.text)) + + if request.method == 'POST': + if "save_last_call_text" in request.POST or "send_last_call_request" in request.POST: + form = LastCallTextForm(request.POST) + if form.is_valid(): + t = form.cleaned_data['last_call_text'] + if t != existing.text: + e = WriteupDocEvent(doc=doc, by=login) + e.by = login + e.type = "changed_last_call_text" + e.desc = "Last call announcement was changed" + e.text = t + e.save() + + doc.time = e.time + doc.save() + + if "send_last_call_request" in request.POST: + save_document_in_history(doc) + + prev = doc.iesg_state + doc.iesg_state = IesgDocStateName.objects.get(slug='lc-req') + e = log_state_changed(request, doc, login, prev) + + doc.time = e.time + doc.save() + + email_state_changed(request, doc, e.desc) + email_owner(request, doc, doc.ad, login, e.desc) + + request_last_call(request, doc) + + return render_to_response('idrfc/last_call_requested.html', + dict(doc=doc), + context_instance=RequestContext(request)) + + if "regenerate_last_call_text" in request.POST: + e = generate_last_call_announcement(request, doc) + + doc.time = e.time + doc.save() + + # make sure form has the updated text + form = LastCallTextForm(initial=dict(last_call_text=e.text)) + + + can_request_last_call = doc.iesg_state.order < 27 + can_make_last_call = doc.iesg_state.order < 20 + can_announce = doc.iesg_state.order > 19 + + need_intended_status = "" + if not doc.intended_std_level: + need_intended_status = doc.file_tag() + + return render_to_response('idrfc/ballot_lastcalltext.html', + dict(doc=doc, + back_url=doc.get_absolute_url(), + last_call_form=form, + can_request_last_call=can_request_last_call, + can_make_last_call=can_make_last_call, + need_intended_status=need_intended_status, + ), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + LastCallTextForm = LastCallTextFormREDESIGN + lastcalltext = lastcalltextREDESIGN + + @group_required('Area_Director','Secretariat') def ballot_writeupnotes(request, name): """Editing of ballot write-up and notes""" @@ -489,7 +887,8 @@ def ballot_writeupnotes(request, name): doc.idinternal.save() return render_to_response('idrfc/ballot_issued.html', - dict(doc=doc), + dict(doc=doc, + back_url=doc.idinternal.get_absolute_url()), context_instance=RequestContext(request)) @@ -507,6 +906,94 @@ def ballot_writeupnotes(request, name): ), context_instance=RequestContext(request)) +class BallotWriteupFormREDESIGN(forms.Form): + ballot_writeup = forms.CharField(widget=forms.Textarea, required=True) + + def clean_ballot_writeup(self): + return self.cleaned_data["ballot_writeup"].replace("\r", "") + +@group_required('Area_Director','Secretariat') +def ballot_writeupnotesREDESIGN(request, name): + """Editing of ballot write-up and notes""" + doc = get_object_or_404(Document, docalias__name=name) + started_process = doc.latest_event(type="started_iesg_process") + if not started_process: + raise Http404() + + login = request.user.get_profile() + + approval = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text") + + existing = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text") + if not existing: + existing = generate_ballot_writeup(request, doc) + + form = BallotWriteupForm(initial=dict(ballot_writeup=existing.text)) + + if request.method == 'POST' and "save_ballot_writeup" in request.POST or "issue_ballot" in request.POST: + form = BallotWriteupForm(request.POST) + if form.is_valid(): + t = form.cleaned_data["ballot_writeup"] + if t != existing.text: + e = WriteupDocEvent(doc=doc, by=login) + e.by = login + e.type = "changed_ballot_writeup_text" + e.desc = "Ballot writeup was changed" + e.text = t + e.save() + + doc.time = e.time + doc.save() + + if "issue_ballot" in request.POST and approval: + if has_role(request.user, "Area Director") and not doc.latest_event(BallotPositionDocEvent, ad=login, time__gte=started_process.time): + # sending the ballot counts as a yes + pos = BallotPositionDocEvent(doc=doc, by=login) + pos.type = "changed_ballot_position" + pos.ad = login + pos.pos_id = "yes" + pos.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.pos.name, pos.ad.name) + pos.save() + + msg = generate_issue_ballot_mail(request, doc) + send_mail_preformatted(request, msg) + + email_iana(request, doc, 'drafts-eval@icann.org', msg) + + e = DocEvent(doc=doc, by=login) + e.by = login + e.type = "sent_ballot_announcement" + e.desc = "Ballot has been issued by %s" % login.name + e.save() + + doc.time = e.time + doc.save() + + return render_to_response('idrfc/ballot_issued.html', + dict(doc=doc, + back_url=doc.get_absolute_url()), + context_instance=RequestContext(request)) + + + need_intended_status = "" + if not doc.intended_std_level: + need_intended_status = doc.file_tag() + + return render_to_response('idrfc/ballot_writeupnotesREDESIGN.html', + dict(doc=doc, + back_url=doc.get_absolute_url(), + ballot_issued=bool(doc.latest_event(type="sent_ballot_announcement")), + ballot_writeup_form=form, + need_intended_status=need_intended_status, + approval=approval, + ), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + BallotWriteupForm = BallotWriteupFormREDESIGN + ballot_writeupnotes = ballot_writeupnotesREDESIGN + + @group_required('Area_Director','Secretariat') def ballot_approvaltext(request, name): """Editing of approval text""" @@ -549,6 +1036,7 @@ def ballot_approvaltext(request, name): return render_to_response('idrfc/ballot_approvaltext.html', dict(doc=doc, + back_url=doc.idinternal.get_absolute_url(), ballot=ballot, approval_text_form=approval_text_form, can_announce=can_announce, @@ -556,6 +1044,69 @@ def ballot_approvaltext(request, name): ), context_instance=RequestContext(request)) +class ApprovalTextFormREDESIGN(forms.Form): + approval_text = forms.CharField(widget=forms.Textarea, required=True) + + def clean_approval_text(self): + return self.cleaned_data["approval_text"].replace("\r", "") + +@group_required('Area_Director','Secretariat') +def ballot_approvaltextREDESIGN(request, name): + """Editing of approval text""" + doc = get_object_or_404(Document, docalias__name=name) + if not doc.iesg_state: + raise Http404() + + login = request.user.get_profile() + + existing = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text") + if not existing: + existing = generate_approval_mail(request, doc) + + form = ApprovalTextForm(initial=dict(approval_text=existing.text)) + + if request.method == 'POST': + if "save_approval_text" in request.POST: + form = ApprovalTextForm(request.POST) + if form.is_valid(): + t = form.cleaned_data['approval_text'] + if t != existing.text: + e = WriteupDocEvent(doc=doc, by=login) + e.by = login + e.type = "changed_ballot_approval_text" + e.desc = "Ballot approval text was changed" + e.text = t + e.save() + + doc.time = e.time + doc.save() + + if "regenerate_approval_text" in request.POST: + e = generate_approval_mail(request, doc) + + doc.time = e.time + doc.save() + + # make sure form has the updated text + form = ApprovalTextForm(initial=dict(approval_text=existing.text)) + + can_announce = doc.iesg_state.order > 19 + need_intended_status = "" + if not doc.intended_std_level: + need_intended_status = doc.file_tag() + + return render_to_response('idrfc/ballot_approvaltext.html', + dict(doc=doc, + back_url=doc.get_absolute_url(), + approval_text_form=form, + can_announce=can_announce, + need_intended_status=need_intended_status, + ), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + ApprovalTextForm = ApprovalTextFormREDESIGN + ballot_approvaltext = ballot_approvaltextREDESIGN @group_required('Secretariat') def approve_ballot(request, name): @@ -628,6 +1179,83 @@ def approve_ballot(request, name): announcement=announcement), context_instance=RequestContext(request)) +@group_required('Secretariat') +def approve_ballotREDESIGN(request, name): + """Approve ballot, sending out announcement, changing state.""" + doc = get_object_or_404(Document, docalias__name=name) + if not doc.iesg_state: + raise Http404() + + login = request.user.get_profile() + + e = doc.latest_event(WriteupDocEvent, type="changed_ballot_approval_text") + if not e: + e = generate_approval_mail(request, doc) + approval_text = e.text + + e = doc.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text") + if not e: + e = generate_ballot_writeup(request, doc) + ballot_writeup = e.text + + if "NOT be published" in approval_text: + action = "do_not_publish" + elif "To: RFC Editor" in approval_text: + action = "to_rfc_editor" + else: + action = "to_announcement_list" + + announcement = approval_text + "\n\n" + ballot_writeup + + if request.method == 'POST': + if action == "do_not_publish": + new_state = IesgDocStateName.objects.get(slug="dead") + else: + new_state = IesgDocStateName.objects.get(slug="ann") + + # fixup document + save_document_in_history(doc) + + prev = doc.iesg_state + doc.iesg_state = new_state + + e = DocEvent(doc=doc, by=login) + if action == "do_not_publish": + e.type = "iesg_disapproved" + e.desc = "Do Not Publish note has been sent to RFC Editor" + else: + e.type = "iesg_approved" + e.desc = "IESG has approved the document" + + e.save() + + change_description = e.desc + " and state has been changed to %s" % doc.iesg_state.name + + e = log_state_changed(request, doc, login, prev) + + doc.time = e.time + doc.save() + + email_state_changed(request, doc, change_description) + email_owner(request, doc, doc.ad, login, change_description) + + # send announcement + send_mail_preformatted(request, announcement) + + if action == "to_announcement_list": + email_iana(request, doc, "drafts-approval@icann.org", announcement) + + return HttpResponseRedirect(doc.get_absolute_url()) + + return render_to_response('idrfc/approve_ballot.html', + dict(doc=doc, + action=action, + announcement=announcement), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + approve_ballot = approve_ballotREDESIGN + class MakeLastCallForm(forms.Form): last_call_sent_date = forms.DateField(required=True) @@ -684,3 +1312,65 @@ def make_last_call(request, name): form=form), context_instance=RequestContext(request)) + +@group_required('Secretariat') +def make_last_callREDESIGN(request, name): + """Make last call for Internet Draft, sending out announcement.""" + doc = get_object_or_404(Document, docalias__name=name) + if not doc.iesg_state: + raise Http404() + + login = request.user.get_profile() + + e = doc.latest_event(WriteupDocEvent, type="changed_last_call_text") + if not e: + e = generate_last_call_announcement(request, doc) + announcement = e.text + + if request.method == 'POST': + form = MakeLastCallForm(request.POST) + if form.is_valid(): + send_mail_preformatted(request, announcement) + email_iana(request, doc, "drafts-lastcall@icann.org", announcement) + + save_document_in_history(doc) + + prev = doc.iesg_state + doc.iesg_state = IesgDocStateName.objects.get(slug='lc') + e = log_state_changed(request, doc, login, prev) + + doc.time = e.time + doc.save() + + change_description = "Last call has been made for %s and state has been changed to %s" % (doc.name, doc.iesg_state.name) + email_state_changed(request, doc, change_description) + email_owner(request, doc, doc.ad, login, change_description) + + e = LastCallDocEvent(doc=doc, by=login) + e.type = "sent_last_call" + e.desc = "Last call sent by %s" % login.name + if form.cleaned_data['last_call_sent_date'] != e.time.date(): + e.time = datetime.datetime.combine(form.cleaned_data['last_call_sent_date'], e.time.time()) + e.expires = form.cleaned_data['last_call_expiration_date'] + e.save() + + return HttpResponseRedirect(doc.get_absolute_url()) + else: + initial = {} + initial["last_call_sent_date"] = date.today() + expire_days = 14 + if doc.group.type_id == "individ": + expire_days = 28 + + initial["last_call_expiration_date"] = date.today() + timedelta(days=expire_days) + + form = MakeLastCallForm(initial=initial) + + return render_to_response('idrfc/make_last_callREDESIGN.html', + dict(doc=doc, + form=form), + context_instance=RequestContext(request)) + + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + make_last_call = make_last_callREDESIGN diff --git a/ietf/idrfc/views_doc.py b/ietf/idrfc/views_doc.py index f6cfaae70..359a03780 100644 --- a/ietf/idrfc/views_doc.py +++ b/ietf/idrfc/views_doc.py @@ -41,9 +41,9 @@ from django.template.defaultfilters import truncatewords_html from django.utils import simplejson as json from django.utils.decorators import decorator_from_middleware from django.middleware.gzip import GZipMiddleware -from django.core.urlresolvers import reverse, NoReverseMatch +from django.core.urlresolvers import reverse as urlreverse, NoReverseMatch +from django.conf import settings -from ietf import settings from ietf.idtracker.models import InternetDraft, IDInternal, BallotInfo, DocumentComment from ietf.idtracker.templatetags.ietf_filters import format_textarea, fill from ietf.idrfc import markup_txt @@ -85,6 +85,7 @@ def include_text(request): def document_main_rfc(request, rfc_number, tab): rfci = get_object_or_404(RfcIndex, rfc_number=rfc_number) + rfci.viewing_as_rfc = True doc = RfcWrapper(rfci) info = {} @@ -122,7 +123,7 @@ def document_main(request, name, tab): return document_main_rfc(request, int(m.group(1)), tab) id = get_object_or_404(InternetDraft, filename=name) doc = IdWrapper(id) - + info = {} info['has_pdf'] = (".pdf" in doc.file_types()) info['is_rfc'] = False @@ -148,21 +149,59 @@ def document_main(request, name, tab): # doc is either IdWrapper or RfcWrapper def _get_history(doc, versions): results = [] - if doc.is_id_wrapper: - comments = DocumentComment.objects.filter(document=doc.tracker_id).exclude(rfc_flag=1) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + versions = [] # clear versions + event_holder = doc._draft if hasattr(doc, "_draft") else doc._rfcindex + for e in event_holder.docevent_set.all().select_related('by').order_by('-time', 'id'): + info = {} + if e.type == "new_revision": + filename = u"%s-%s" % (e.doc.name, e.newrevisiondocevent.rev) + e.desc = 'New version available: <a href="http://tools.ietf.org/id/%s.txt">%s</a>' % (filename, filename) + if int(e.newrevisiondocevent.rev) != 0: + e.desc += ' (<a href="http://tools.ietf.org/rfcdiff?url2=%s">diff from -%02d</a>)' % (filename, int(e.newrevisiondocevent.rev) - 1) + info["dontmolest"] = True + + multiset_ballot_text = "This was part of a ballot set with: " + if e.desc.startswith(multiset_ballot_text): + names = e.desc[len(multiset_ballot_text):].split(", ") + e.desc = multiset_ballot_text + ", ".join(u'<a href="%s">%s</a>' % (urlreverse("doc_view", kwargs={'name': n }), n) for n in names) + info["dontmolest"] = True + + info['text'] = e.desc + info['by'] = e.by.name + info['textSnippet'] = truncatewords_html(format_textarea(fill(info['text'], 80)), 25) + info['snipped'] = info['textSnippet'][-3:] == "..." and e.type != "new_revision" + results.append({'comment':e, 'info':info, 'date':e.time, 'is_com':True}) + + prev_rev = "00" + # actually, we're already sorted and this ruins the sort from + # the ids which is sometimes needed, so the function should be + # rewritten to not rely on a resort + results.sort(key=lambda x: x['date']) + for o in results: + e = o["comment"] + if e.type == "new_revision": + e.version = e.newrevisiondocevent.rev + else: + e.version = prev_rev + prev_rev = e.version else: - comments = DocumentComment.objects.filter(document=doc.rfc_number,rfc_flag=1) - if len(comments) > 0: - # also include rfc_flag=NULL, but only if at least one - # comment with rfc_flag=1 exists (usually NULL means same as 0) - comments = DocumentComment.objects.filter(document=doc.rfc_number).exclude(rfc_flag=0) - for comment in comments.order_by('-date','-time','-id').filter(public_flag=1).select_related('created_by'): - info = {} - info['text'] = comment.comment_text - info['by'] = comment.get_fullname() - info['textSnippet'] = truncatewords_html(format_textarea(fill(info['text'], 80)), 25) - info['snipped'] = info['textSnippet'][-3:] == "..." - results.append({'comment':comment, 'info':info, 'date':comment.datetime(), 'is_com':True}) + if doc.is_id_wrapper: + comments = DocumentComment.objects.filter(document=doc.tracker_id).exclude(rfc_flag=1) + else: + comments = DocumentComment.objects.filter(document=doc.rfc_number,rfc_flag=1) + if len(comments) > 0: + # also include rfc_flag=NULL, but only if at least one + # comment with rfc_flag=1 exists (usually NULL means same as 0) + comments = DocumentComment.objects.filter(document=doc.rfc_number).exclude(rfc_flag=0) + for comment in comments.order_by('-date','-time','-id').filter(public_flag=1).select_related('created_by'): + info = {} + info['text'] = comment.comment_text + info['by'] = comment.get_fullname() + info['textSnippet'] = truncatewords_html(format_textarea(fill(info['text'], 80)), 25) + info['snipped'] = info['textSnippet'][-3:] == "..." + results.append({'comment':comment, 'info':info, 'date':comment.datetime(), 'is_com':True}) + if doc.is_id_wrapper and versions: for v in versions: if v['draft_name'] == doc.draft_name: @@ -171,11 +210,11 @@ def _get_history(doc, versions): results.insert(0, v) if doc.is_id_wrapper and doc.draft_status == "Expired" and doc._draft.expiration_date: results.append({'is_text':True, 'date':doc._draft.expiration_date, 'text':'Draft expired'}) - if doc.is_rfc_wrapper: + if not settings.USE_DB_REDESIGN_PROXY_CLASSES and doc.is_rfc_wrapper: text = 'RFC Published' if doc.draft_name: try: - text = 'RFC Published (see <a href="%s">%s</a> for earlier history)' % (reverse('doc_view', args=[doc.draft_name]),doc.draft_name) + text = 'RFC Published (see <a href="%s">%s</a> for earlier history)' % (urlreverse('doc_view', args=[doc.draft_name]),doc.draft_name) except NoReverseMatch: pass results.append({'is_text':True, 'date':doc.publication_date, 'text':text}) @@ -210,6 +249,19 @@ def _get_versions(draft, include_replaced=True): def get_ballot(name): r = re.compile("^rfc([1-9][0-9]*)$") m = r.match(name) + + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from doc.models import DocAlias + alias = get_object_or_404(DocAlias, name=name) + d = get_object_or_404(InternetDraft, name=alias.document.name) + try: + if not d.ballot.ballot_issued: + raise Http404 + except BallotInfo.DoesNotExist: + raise Http404 + + return (BallotWrapper(d), RfcWrapper(d) if m else IdWrapper(d)) + if m: rfc_number = int(m.group(1)) rfci = get_object_or_404(RfcIndex, rfc_number=rfc_number) diff --git a/ietf/idrfc/views_edit.py b/ietf/idrfc/views_edit.py index d5a06ebd3..4eacacf6f 100644 --- a/ietf/idrfc/views_edit.py +++ b/ietf/idrfc/views_edit.py @@ -11,17 +11,21 @@ from django.template import RequestContext from django import forms from django.utils.html import strip_tags from django.db.models import Max +from django.conf import settings -from ietf import settings from ietf.utils.mail import send_mail_text from ietf.ietfauth.decorators import group_required from ietf.idtracker.templatetags.ietf_filters import in_group +from ietf.ietfauth.decorators import has_role from ietf.idtracker.models import * from ietf.iesg.models import * from ietf.idrfc.mails import * from ietf.idrfc.utils import * from ietf.idrfc.lastcall import request_last_call +from doc.models import Document, DocEvent, StatusDateDocEvent, TelechatDocEvent, save_document_in_history, DocHistory +from name.models import IesgDocStateName, IntendedStdLevelName, DocInfoTagName, get_next_iesg_states, DocStateName +from person.models import Person, Email class ChangeStateForm(forms.Form): state = forms.ModelChoiceField(IDState.objects.all(), empty_label=None, required=True) @@ -58,7 +62,8 @@ def change_state(request, name): request_last_call(request, doc) return render_to_response('idrfc/last_call_requested.html', - dict(doc=doc), + dict(doc=doc, + url=doc.idinternal.get_absolute_url()), context_instance=RequestContext(request)) return HttpResponseRedirect(internal.get_absolute_url()) @@ -79,6 +84,73 @@ def change_state(request, name): next_states=next_states), context_instance=RequestContext(request)) +class ChangeStateFormREDESIGN(forms.Form): + state = forms.ModelChoiceField(IesgDocStateName.objects.all(), empty_label=None, required=True) + # FIXME: no tags yet + #substate = forms.ModelChoiceField(IDSubState.objects.all(), required=False) + comment = forms.CharField(widget=forms.Textarea, required=False) + +@group_required('Area_Director','Secretariat') +def change_stateREDESIGN(request, name): + """Change state of Internet Draft, notifying parties as necessary + and logging the change as a comment.""" + doc = get_object_or_404(Document, docalias__name=name) + if (not doc.latest_event(type="started_iesg_process")) or doc.state_id == "expired": + raise Http404() + + login = request.user.get_profile() + + if request.method == 'POST': + form = ChangeStateForm(request.POST) + if form.is_valid(): + state = form.cleaned_data['state'] + comment = form.cleaned_data['comment'] + if state != doc.iesg_state: + save_document_in_history(doc) + + prev = doc.iesg_state + doc.iesg_state = state + + e = log_state_changed(request, doc, login, prev, comment) + + doc.time = e.time + doc.save() + + email_state_changed(request, doc, e.desc) + email_owner(request, doc, doc.ad, login, e.desc) + + if doc.iesg_state_id == "lc-req": + request_last_call(request, doc) + + return render_to_response('idrfc/last_call_requested.html', + dict(doc=doc, + url=doc.get_absolute_url()), + context_instance=RequestContext(request)) + + return HttpResponseRedirect(doc.get_absolute_url()) + + else: + form = ChangeStateForm(initial=dict(state=doc.iesg_state_id)) + + next_states = get_next_iesg_states(doc.iesg_state) + prev_state = None + + hists = DocHistory.objects.filter(doc=doc).exclude(iesg_state=doc.iesg_state).order_by("-time")[:1] + if hists: + prev_state = hists[0].iesg_state + + return render_to_response('idrfc/change_stateREDESIGN.html', + dict(form=form, + doc=doc, + prev_state=prev_state, + next_states=next_states), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + change_state = change_stateREDESIGN + ChangeStateForm = ChangeStateFormREDESIGN + + def dehtmlify_textarea_text(s): return s.replace("<br>", "\n").replace("<b>", "").replace("</b>", "").replace(" ", " ") @@ -239,7 +311,7 @@ def edit_info(request, name): setattr(obj, attr, r[attr]) diff(doc, 'intended_status', "Intended Status") - diff(doc.idinternal, 'status_date', "Status Date") + diff(doc.idinternal, 'status_date', "Status date") if 'area_acronym' in r and r['area_acronym']: diff(doc.idinternal, 'area_acronym', 'Area acronym') diff(doc.idinternal, 'job_owner', 'Responsible AD') @@ -306,6 +378,226 @@ def edit_info(request, name): login=login), context_instance=RequestContext(request)) +class EditInfoFormREDESIGN(forms.Form): + intended_std_level = forms.ModelChoiceField(IntendedStdLevelName.objects.all(), empty_label=None, required=True) + status_date = forms.DateField(required=False, help_text="Format is YYYY-MM-DD") + via_rfc_editor = forms.BooleanField(required=False, label="Via IRTF or RFC Editor") + ad = forms.ModelChoiceField(Person.objects.filter(email__role__name__in=("ad", "ex-ad")).order_by('email__role__name', 'name'), label="Responsible AD", empty_label=None, required=True) + create_in_state = forms.ModelChoiceField(IesgDocStateName.objects.filter(slug__in=("pub-req", "watching")), empty_label=None, required=True) + notify = forms.CharField(max_length=255, label="Notice emails", help_text="Separate email addresses with commas", required=False) + note = forms.CharField(widget=forms.Textarea, label="IESG note", required=False) + telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False) + returning_item = forms.BooleanField(required=False) + + def __init__(self, *args, **kwargs): + old_ads = kwargs.pop('old_ads') + + super(self.__class__, self).__init__(*args, **kwargs) + + # fix up ad field + choices = self.fields['ad'].choices + ex_ads = dict((e.pk, e) for e in Person.objects.filter(email__role__name="ex-ad").distinct()) + if old_ads: + # separate active ADs from inactive + for i, t in enumerate(choices): + if t[0] in ex_ads: + choices.insert(i, ("", "----------------")) + break + else: + # remove old ones + self.fields['ad'].choices = [t for t in choices if t[0] not in ex_ads] + + # telechat choices + dates = TelechatDates.objects.all()[0].dates() + init = kwargs['initial']['telechat_date'] + if init and init not in dates: + dates.insert(0, init) + + choices = [("", "(not on agenda)")] + for d in dates: + choices.append((d, d.strftime("%Y-%m-%d"))) + + self.fields['telechat_date'].choices = choices + + # returning item is rendered non-standard + self.standard_fields = [x for x in self.visible_fields() if x.name not in ('returning_item',)] + + def clean_status_date(self): + d = self.cleaned_data['status_date'] + if d: + if d < date.today(): + raise forms.ValidationError("Date must not be in the past.") + if d >= date.today() + timedelta(days=365 * 2): + raise forms.ValidationError("Date must be within two years.") + + return d + + def clean_note(self): + # note is stored munged in the database + return self.cleaned_data['note'].replace('\n', '<br>').replace('\r', '').replace(' ', ' ') + + +def get_initial_notify(doc): + # set change state notice to something sensible + receivers = [] + if doc.group.type_id == "individ": + for a in doc.authors.all(): + receivers.append(e.address) + else: + receivers.append("%s-chairs@%s" % (doc.group.acronym, settings.TOOLS_SERVER)) + for editor in Email.objects.filter(role__name="editor", role__group=doc.group): + receivers.append(e.address) + + receivers.append("%s@%s" % (doc.name, settings.TOOLS_SERVER)) + return ", ".join(receivers) + +@group_required('Area_Director','Secretariat') +def edit_infoREDESIGN(request, name): + """Edit various Internet Draft attributes, notifying parties as + necessary and logging changes as document events.""" + doc = get_object_or_404(Document, docalias__name=name) + if doc.state_id == "expired": + raise Http404() + + login = request.user.get_profile() + + new_document = False + if not doc.iesg_state: # FIXME: should probably receive "new document" as argument to view instead of this + new_document = True + doc.iesg_state = IesgDocStateName.objects.get(slug="pub-req") + doc.notify = get_initial_notify(doc) + + e = doc.latest_event(TelechatDocEvent, type="scheduled_for_telechat") + initial_telechat_date = e.telechat_date if e else None + initial_returning_item = bool(e and e.returning_item) + + if request.method == 'POST': + form = EditInfoForm(request.POST, + old_ads=False, + initial=dict(telechat_date=initial_telechat_date)) + if form.is_valid(): + save_document_in_history(doc) + + r = form.cleaned_data + if new_document: + # fix so Django doesn't barf in the diff below because these + # fields can't be NULL + doc.ad = r['ad'] + doc.iesg_state = r['create_in_state'] + + replaces = Document.objects.filter(docalias__relateddocument__source=doc, docalias__relateddocument__relationship="replaces") + if replaces: + # this should perhaps be somewhere else, e.g. the + # place where the replace relationship is established? + e = DocEvent() + e.type = "added_comment" + e.by = Person.objects.get(name="(System)") + e.doc = doc + e.desc = "Earlier history may be found in the Comment Log for <a href=\"%s\">%s</a>" % (replaces[0], replaces[0].get_absolute_url()) + e.save() + + e = DocEvent() + e.type = "started_iesg_process" + e.by = login + e.doc = doc + e.desc = "IESG process started in state <b>%s</b>" % doc.iesg_state.name + e.save() + + orig_ad = doc.ad + + changes = [] + + def desc(attr, new, old): + entry = "%(attr)s has been changed to <b>%(new)s</b> from <b>%(old)s</b>" + if new_document: + entry = "%(attr)s has been changed to <b>%(new)s</b>" + + return entry % dict(attr=attr, new=new, old=old) + + def diff(attr, name): + v = getattr(doc, attr) + if r[attr] != v: + changes.append(desc(name, r[attr], v)) + setattr(doc, attr, r[attr]) + + # update the attributes, keeping track of what we're doing + diff('intended_std_level', "Intended Status") + diff('ad', "Responsible AD") + diff('notify', "State Change Notice email list") + + if r['note'] != doc.note: + if not r['note']: + if doc.note: + changes.append("Note field has been cleared") + else: + if doc.note: + changes.append("Note changed to '%s'" % r['note']) + else: + changes.append("Note added '%s'" % r['note']) + + doc.note = r['note'] + + for c in changes: + e = DocEvent(doc=doc, by=login) + e.type = "changed_document" + e.save() + + update_telechat(request, doc, login, + r['telechat_date'], r['returning_item']) + + e = doc.latest_event(StatusDateDocEvent, type="changed_status_date") + status_date = e.date if e else None + if r["status_date"] != status_date: + e = StatusDateDocEvent(doc=doc, by=login) + e.type ="changed_status_date" + d = desc("Status date", r["status_date"], status_date) + changes.append(d) + e.date = r["status_date"] + e.save() + + if has_role(request.user, 'Secretariat'): + via_rfc = DocInfoTagName.objects.get(slug="via-rfc") + if r['via_rfc_editor']: + doc.tags.add(via_rfc) + else: + doc.tags.remove(via_rfc) + + doc.time = datetime.datetime.now() + + if changes and not new_document: + email_owner(request, doc, orig_ad, login, "\n".join(changes)) + + doc.save() + return HttpResponseRedirect(doc.get_absolute_url()) + else: + e = doc.latest_event(StatusDateDocEvent) + status = e.date if e else None + init = dict(intended_std_level=doc.intended_std_level, + status_date=status, + ad=doc.ad, + notify=doc.notify, + note=dehtmlify_textarea_text(doc.note), + telechat_date=initial_telechat_date, + returning_item=initial_returning_item, + ) + + form = EditInfoForm(old_ads=False, initial=init) + + if not has_role(request.user, 'Secretariat'): + # filter out Via RFC Editor + form.standard_fields = [x for x in form.standard_fields if x.name != "via_rfc_editor"] + + return render_to_response('idrfc/edit_infoREDESIGN.html', + dict(doc=doc, + form=form, + user=request.user, + login=login), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + EditInfoForm = EditInfoFormREDESIGN + edit_info = edit_infoREDESIGN + @group_required('Area_Director','Secretariat') def request_resurrect(request, name): @@ -327,9 +619,37 @@ def request_resurrect(request, name): return HttpResponseRedirect(doc.idinternal.get_absolute_url()) return render_to_response('idrfc/request_resurrect.html', - dict(doc=doc), + dict(doc=doc, + back_url=doc.idinternal.get_absolute_url()), context_instance=RequestContext(request)) +@group_required('Area_Director','Secretariat') +def request_resurrectREDESIGN(request, name): + """Request resurrect of expired Internet Draft.""" + doc = get_object_or_404(Document, docalias__name=name) + if doc.state_id != "expired": + raise Http404() + + login = request.user.get_profile() + + if request.method == 'POST': + email_resurrect_requested(request, doc, login) + + e = DocEvent(doc=doc, by=login) + e.type = "requested_resurrect" + e.desc = "Resurrection was requested" + e.save() + + return HttpResponseRedirect(doc.get_absolute_url()) + + return render_to_response('idrfc/request_resurrect.html', + dict(doc=doc, + back_url=doc.get_absolute_url()), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + request_resurrect = request_resurrectREDESIGN + @group_required('Secretariat') def resurrect(request, name): """Resurrect expired Internet Draft.""" @@ -354,9 +674,45 @@ def resurrect(request, name): return HttpResponseRedirect(doc.idinternal.get_absolute_url()) return render_to_response('idrfc/resurrect.html', - dict(doc=doc), + dict(doc=doc, + back_url=doc.idinternal.get_absolute_url()), context_instance=RequestContext(request)) +@group_required('Secretariat') +def resurrectREDESIGN(request, name): + """Resurrect expired Internet Draft.""" + doc = get_object_or_404(Document, docalias__name=name) + if doc.state_id != "expired": + raise Http404() + + login = request.user.get_profile() + + if request.method == 'POST': + save_document_in_history(doc) + + e = doc.latest_event(type__in=('requested_resurrect', "completed_resurrect")) + if e and e.type == 'requested_resurrect': + email_resurrection_completed(request, doc, requester=e.by) + + e = DocEvent(doc=doc, by=login) + e.type = "completed_resurrect" + e.desc = "Resurrection was completed" + e.save() + + doc.state = DocStateName.objects.get(slug="active") + doc.time = datetime.datetime.now() + doc.save() + return HttpResponseRedirect(doc.get_absolute_url()) + + return render_to_response('idrfc/resurrect.html', + dict(doc=doc, + back_url=doc.get_absolute_url()), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + resurrect = resurrectREDESIGN + + class AddCommentForm(forms.Form): comment = forms.CharField(required=True, widget=forms.Textarea) @@ -382,5 +738,40 @@ def add_comment(request, name): return render_to_response('idrfc/add_comment.html', dict(doc=doc, - form=form), + form=form, + back_url=doc.idinternal.get_absolute_url()), context_instance=RequestContext(request)) + +@group_required('Area_Director','Secretariat') +def add_commentREDESIGN(request, name): + """Add comment to Internet Draft.""" + doc = get_object_or_404(Document, docalias__name=name) + if not doc.iesg_state: + raise Http404() + + login = request.user.get_profile() + + if request.method == 'POST': + form = AddCommentForm(request.POST) + if form.is_valid(): + c = form.cleaned_data['comment'] + + e = DocEvent(doc=doc, by=login) + e.type = "added_comment" + e.desc = c + e.save() + + email_owner(request, doc, doc.ad, login, + "A new comment added by %s" % login.name) + return HttpResponseRedirect(doc.get_absolute_url()) + else: + form = AddCommentForm() + + return render_to_response('idrfc/add_comment.html', + dict(doc=doc, + form=form, + back_url=doc.get_absolute_url()), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + add_comment = add_commentREDESIGN diff --git a/ietf/idrfc/views_search.py b/ietf/idrfc/views_search.py index 435dd48e9..ae836a7de 100644 --- a/ietf/idrfc/views_search.py +++ b/ietf/idrfc/views_search.py @@ -30,7 +30,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import re +import re, datetime from django import forms from django.shortcuts import render_to_response from django.db.models import Q @@ -38,9 +38,10 @@ from django.template import RequestContext from django.views.decorators.cache import cache_page from ietf.idtracker.models import IDState, IESGLogin, IDSubState, Area, InternetDraft, Rfc, IDInternal, IETFWG from ietf.idrfc.models import RfcIndex -from django.http import Http404, HttpResponse, HttpResponsePermanentRedirect +from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponsePermanentRedirect from ietf.idrfc.idrfc_wrapper import IdWrapper,RfcWrapper,IdRfcWrapper from ietf.utils import normalize_draftname +from django.conf import settings def addInputEvents(widget): widget.attrs["oninput"] = 'inputEvent()' @@ -267,7 +268,246 @@ def search_query(query_original, sort_by=None): meta['advanced'] = True return (results,meta) -def genParamURL(request, ignore_list): +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from doc.models import * + from person.models import * + from group.models import * + + class SearchForm(forms.Form): + name = forms.CharField(required=False) + addInputEvents(name.widget) # consider moving this to jQuery client-side instead + rfcs = forms.BooleanField(required=False,initial=True) + activeDrafts = forms.BooleanField(required=False,initial=True) + oldDrafts = forms.BooleanField(required=False,initial=False) + lucky = forms.BooleanField(required=False,initial=False) + + by = forms.ChoiceField(choices=[(x,x) for x in ('author','group','area','ad','state')], required=False, initial='wg', label='Foobar') + author = forms.CharField(required=False) + addInputEvents(author.widget) + group = forms.CharField(required=False) + addInputEvents(group.widget) + area = forms.ModelChoiceField(Group.objects.filter(type="area", state="active").order_by('name'), empty_label="any area", required=False) + addInputEvents(area.widget) + ad = forms.ChoiceField(choices=(), required=False) + addInputEvents(ad.widget) + state = forms.ModelChoiceField(IesgDocStateName.objects.all(), empty_label="any state", required=False) + addInputEvents(state.widget) + subState = forms.ChoiceField(choices=(), required=False) + addInputEvents(subState.widget) + + def __init__(self, *args, **kwargs): + super(SearchForm, self).__init__(*args, **kwargs) + responsible = Document.objects.values_list('ad', flat=True).distinct() + active_ads = list(Person.objects.filter(email__role__name="ad", + email__role__group__type="area", + email__role__group__state="active").distinct()) + inactive_ads = list(Person.objects.filter(pk__in=responsible) + .exclude(pk__in=[x.pk for x in active_ads])) + extract_last_name = lambda x: x.name_parts()[3] + active_ads.sort(key=extract_last_name) + inactive_ads.sort(key=extract_last_name) + + self.fields['ad'].choices = c = [('', 'any AD')] + [(ad.pk, ad.name) for ad in active_ads] + [('', '------------------')] + [(ad.pk, ad.name) for ad in inactive_ads] + self.fields['subState'].choices = [('', 'any substate'), ('0', 'no substate')] + [(n.slug, n.name) for n in DocInfoTagName.objects.filter(slug__in=('point', 'ad-f-up', 'need-rev', 'extpty'))] + def clean_name(self): + value = self.cleaned_data.get('name','') + return normalize_draftname(value) + def clean(self): + q = self.cleaned_data + # Reset query['by'] if needed + for k in ('author','group','area','ad'): + if (q['by'] == k) and (k not in q or not q[k]): + q['by'] = None + if (q['by'] == 'state') and (not 'state' in q or not 'subState' in q or not (q['state'] or q['subState'])): + q['by'] = None + # Reset other fields + for k in ('author','group','area','ad'): + if q['by'] != k: + self.data[k] = "" + q[k] = "" + if q['by'] != 'state': + self.data['state'] = "" + self.data['subState'] = "" + q['state'] = "" + q['subState'] = "" + return q + + def search_query(query_original, sort_by=None): + query = dict(query_original.items()) + drafts = query['activeDrafts'] or query['oldDrafts'] + if (not drafts) and (not query['rfcs']): + return ([], {}) + + # Non-ASCII strings don't match anything; this check + # is currently needed to avoid complaints from MySQL. + # FIXME: this should be fixed with MySQL if it's still a problem? + for k in ['name','author','group']: + try: + tmp = str(query.get(k, '')) + except: + query[k] = '*NOSUCH*' + + # Start by search InternetDrafts + idresults = [] + rfcresults = [] + MAX = 500 + + docs = InternetDraft.objects.all() + + # name + if query["name"]: + docs = docs.filter(Q(docalias__name__icontains=query["name"]) | + Q(title__icontains=query["name"])).distinct() + + # rfc/active/old check buttons + allowed = [] + disallowed = [] + + def add(allow, states): + l = allowed if allow else disallowed + l.extend(states) + + add(query["rfcs"], ['rfc']) + add(query["activeDrafts"], ['active']) + add(query["oldDrafts"], ['repl', 'expired', 'auth-rm', 'ietf-rm']) + + docs = docs.filter(state__in=allowed).exclude(state__in=disallowed) + + # radio choices + by = query["by"] + if by == "author": + # FIXME: this is full name, not last name as hinted in the HTML + docs = docs.filter(authors__person__name__icontains=query["author"]) + elif by == "group": + docs = docs.filter(group__acronym=query["group"]) + elif by == "area": + docs = docs.filter(Q(group__parent=query["area"]) | + Q(ad__email__role__name="ad", + ad__email__role__group=query["area"])) + elif by == "ad": + docs = docs.filter(ad=query["ad"]) + elif by == "state": + if query["state"]: + docs = docs.filter(iesg_state=query["state"]) + if query["subState"]: + docs = docs.filter(tags=query["subState"]) + + # evaluate and fill in values with aggregate queries to avoid + # too many individual queries + results = list(docs.select_related("state", "iesg_state", "ad", "ad__person", "std_level", "intended_std_level", "group")[:MAX]) + + rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", document__in=[r.pk for r in results]).values_list("document_id", "name")) + # canonical name + for r in results: + if r.pk in rfc_aliases: + # lambda weirdness works around lambda binding in local for loop scope + r.canonical_name = (lambda x: lambda: x)(rfc_aliases[r.pk]) + else: + r.canonical_name = (lambda x: lambda: x)(r.name) + + result_map = dict((r.pk, r) for r in results) + + # events + event_types = ("published_rfc", + "changed_ballot_position", + "started_iesg_process", + "new_revision") + for d in rfc_aliases.keys(): + for e in event_types: + setattr(result_map[d], e, None) + + for e in DocEvent.objects.filter(doc__in=rfc_aliases.keys(), type__in=event_types).order_by('-time'): + r = result_map[e.doc_id] + if not getattr(r, e.type): + # sets e.g. r.published_date = e for use in proxy wrapper + setattr(r, e.type, e) + + # obsoleted/updated by + for d in rfc_aliases: + r = result_map[d] + r.obsoleted_by_list = [] + r.updated_by_list = [] + + xed_by = RelatedDocument.objects.filter(target__name__in=rfc_aliases.values(), relationship__in=("obs", "updates")).select_related('target__document_id') + rel_rfc_aliases = dict(DocAlias.objects.filter(name__startswith="rfc", document__in=[rel.source_id for rel in xed_by]).values_list('document_id', 'name')) + for rel in xed_by: + r = result_map[rel.target.document_id] + if rel.relationship_id == "obs": + attr = "obsoleted_by_list" + else: + attr = "updated_by_list" + + getattr(r, attr).append(int(rel_rfc_aliases[rel.source_id][3:])) + + + # sort + def sort_key(d): + res = [] + + canonical = d.canonical_name() + if canonical.startswith('rfc'): + rfc_num = int(canonical[3:]) + else: + rfc_num = None + + if rfc_num != None: + res.append(2) + elif d.state_id == "active": + res.append(1) + else: + res.append(3) + + if sort_by == "title": + res.append(d.title) + elif sort_by == "date": + res.append(str(d.revision_date or datetime.date(1990, 1, 1))) + elif sort_by == "status": + if rfc_num != None: + res.append(rfc_num) + else: + res.append(d.state) + elif sort_by == "ipr": + res.append(d.name) + elif sort_by == "ad": + if rfc_num != None: + res.append(rfc_num) + elif d.state_id == "active": + if d.iesg_state: + res.append(d.iesg_state.order) + else: + res.append(0) + else: + if rfc_num != None: + res.append(rfc_num) + else: + res.append(canonical) + + return res + + results.sort(key=sort_key) + + meta = {} + if len(docs) == MAX: + meta['max'] = MAX + if query['by']: + meta['advanced'] = True + + # finally wrap in old wrappers + + wrapped_results = [] + for r in results: + draft = None + rfc = None + if not r.name.startswith('rfc'): + draft = IdWrapper(r) + if r.name.startswith('rfc') or r.pk in rfc_aliases: + rfc = RfcWrapper(r) + wrapped_results.append(IdRfcWrapper(draft, rfc)) + + return (wrapped_results, meta) + + +def generate_query_string(request, ignore_list): """Recreates the parameter string from the given request, and returns it as a string. Any parameter names present in ignore_list shall not be put @@ -284,7 +524,7 @@ def search_results(request): return search_main(request) form = SearchForm(dict(request.REQUEST.items())) if not form.is_valid(): - return HttpResponse("form not valid?", mimetype="text/plain") + return HttpResponseBadRequest("form not valid?", mimetype="text/plain") sort_by = None if "sortBy" in request.GET: @@ -294,7 +534,7 @@ def search_results(request): meta['searching'] = True meta['by'] = form.cleaned_data['by'] - meta['rqps'] = genParamURL(request, ['sortBy']) + meta['rqps'] = generate_query_string(request, ['sortBy']) # With a later Django we can do this from the template (incude with tag) # Pass the headers and their sort key names meta['hdrs'] = [{'htitle': 'Document', 'htype':'doc'}, @@ -329,12 +569,19 @@ def search_main(request): def by_ad(request, name): ad_id = None ad_name = None - for i in IESGLogin.objects.filter(user_level__in=[1,2]): - iname = str(i).lower().replace(' ','.') - if name == iname: - ad_id = i.id - ad_name = str(i) - break + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + for p in Person.objects.filter(email__role__name__in=("ad", "ex-ad")): + if name == p.name.lower().replace(" ", "."): + ad_id = p.id + ad_name = p.name + break + else: + for i in IESGLogin.objects.filter(user_level__in=[1,2]): + iname = str(i).lower().replace(' ','.') + if name == iname: + ad_id = i.id + ad_name = str(i) + break if not ad_id: raise Http404 form = SearchForm({'by':'ad','ad':ad_id, @@ -347,11 +594,17 @@ def by_ad(request, name): @cache_page(15*60) # 15 minutes def all(request): - active = InternetDraft.objects.all().filter(status=1).order_by("filename").values('filename') - rfc1 = InternetDraft.objects.all().filter(status=3).order_by("filename").values('filename','rfc_number') - rfc_numbers1 = InternetDraft.objects.all().filter(status=3).values_list('rfc_number', flat=True) - rfc2 = RfcIndex.objects.all().exclude(rfc_number__in=rfc_numbers1).order_by('rfc_number').values('rfc_number','draft') - dead = InternetDraft.objects.all().exclude(status__in=[1,3]).order_by("filename").select_related('status__status') + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + active = (dict(filename=n) for n in InternetDraft.objects.filter(state="active").order_by("name").values_list('name', flat=True)) + rfc1 = (dict(filename=d, rfc_number=int(n[3:])) for d, n in DocAlias.objects.filter(document__state="rfc", name__startswith="rfc").exclude(document__name__startswith="rfc").order_by("document__name").values_list('document__name','name').distinct()) + rfc2 = (dict(rfc_number=r, draft=None) for r in sorted(int(n[3:]) for n in Document.objects.filter(type="draft", name__startswith="rfc").values_list('name', flat=True))) + dead = InternetDraft.objects.exclude(state__in=("active", "rfc")).select_related("state").order_by("name") + else: + active = InternetDraft.objects.all().filter(status=1).order_by("filename").values('filename') + rfc1 = InternetDraft.objects.all().filter(status=3).order_by("filename").values('filename','rfc_number') + rfc_numbers1 = InternetDraft.objects.all().filter(status=3).values_list('rfc_number', flat=True) + rfc2 = RfcIndex.objects.all().exclude(rfc_number__in=rfc_numbers1).order_by('rfc_number').values('rfc_number','draft') + dead = InternetDraft.objects.all().exclude(status__in=[1,3]).order_by("filename").select_related('status__status') return render_to_response('idrfc/all.html', {'active':active, 'rfc1':rfc1, 'rfc2':rfc2, 'dead':dead}, context_instance=RequestContext(request)) @cache_page(15*60) # 15 minutes @@ -366,7 +619,7 @@ def in_last_call(request): for p in InternetDraft.objects.all().filter(idinternal__primary_flag=1).filter(idinternal__cur_state__state='In Last Call'): if (p.idinternal.rfc_flag): - lcdocs.append(IdRfcWrapper(None,RfCWrapper(p))) + lcdocs.append(IdRfcWrapper(None,RfcWrapper(p))) else: lcdocs.append(IdRfcWrapper(IdWrapper(p),None)) diff --git a/ietf/idtracker/admin.py b/ietf/idtracker/admin.py index 495574c60..66bba8725 100644 --- a/ietf/idtracker/admin.py +++ b/ietf/idtracker/admin.py @@ -1,14 +1,16 @@ #coding: utf-8 from django.contrib import admin +from django.conf import settings from ietf.idtracker.models import * - -class AcronymAdmin(admin.ModelAdmin): - list_display=('acronym', 'name') -admin.site.register(Acronym, AcronymAdmin) -class AreaAdmin(admin.ModelAdmin): - list_display=('area_acronym', 'status') -admin.site.register(Area, AreaAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + class AcronymAdmin(admin.ModelAdmin): + list_display=('acronym', 'name') + admin.site.register(Acronym, AcronymAdmin) + + class AreaAdmin(admin.ModelAdmin): + list_display=('area_acronym', 'status') + admin.site.register(Area, AreaAdmin) class AreaDirectorAdmin(admin.ModelAdmin): raw_id_fields=['person'] @@ -22,9 +24,10 @@ class AreaWGURLAdmin(admin.ModelAdmin): pass admin.site.register(AreaWGURL, AreaWGURLAdmin) -class BallotInfoAdmin(admin.ModelAdmin): - pass -admin.site.register(BallotInfo, BallotInfoAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + class BallotInfoAdmin(admin.ModelAdmin): + pass + admin.site.register(BallotInfo, BallotInfoAdmin) class ChairsHistoryAdmin(admin.ModelAdmin): list_display=('person', 'chair_type', 'start_year', 'end_year') @@ -51,12 +54,13 @@ class IDIntendedStatusAdmin(admin.ModelAdmin): pass admin.site.register(IDIntendedStatus, IDIntendedStatusAdmin) -class IDInternalAdmin(admin.ModelAdmin): - ordering=['draft'] - list_display=['pk', 'rfc_flag', 'token_email', 'note', 'tracker_link', 'draft_link'] - search_fields=['draft__filename'] - raw_id_fields=['draft','ballot'] -admin.site.register(IDInternal, IDInternalAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + class IDInternalAdmin(admin.ModelAdmin): + ordering=['draft'] + list_display=['pk', 'rfc_flag', 'token_email', 'note', 'tracker_link', 'draft_link'] + search_fields=['draft__filename'] + raw_id_fields=['draft','ballot'] + admin.site.register(IDInternal, IDInternalAdmin) class IDNextStateAdmin(admin.ModelAdmin): pass @@ -86,13 +90,15 @@ class IESGLoginAdmin(admin.ModelAdmin): ordering=['user_level', 'last_name'] list_display=('login_name', 'first_name', 'last_name', 'user_level') raw_id_fields=['person'] -admin.site.register(IESGLogin, IESGLoginAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + admin.site.register(IESGLogin, IESGLoginAdmin) class IETFWGAdmin(admin.ModelAdmin): list_display=('group_acronym', 'group_type', 'status', 'area_acronym', 'start_date', 'concluded_date', 'chairs_link') search_fields=['group_acronym__acronym', 'group_acronym__name'] list_filter=['status', 'group_type'] -admin.site.register(IETFWG, IETFWGAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + admin.site.register(IETFWG, IETFWGAdmin) class WGChairAdmin(admin.ModelAdmin): list_display = ('person_link', 'group_link') @@ -102,12 +108,13 @@ class IRTFAdmin(admin.ModelAdmin): pass admin.site.register(IRTF, IRTFAdmin) -class InternetDraftAdmin(admin.ModelAdmin): - list_display=('filename', 'revision', 'title', 'status') - search_fields=['filename', 'title'] - list_filter=['status'] - raw_id_fields=['replaced_by'] -admin.site.register(InternetDraft, InternetDraftAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + class InternetDraftAdmin(admin.ModelAdmin): + list_display=('filename', 'revision', 'title', 'status') + search_fields=['filename', 'title'] + list_filter=['status'] + raw_id_fields=['replaced_by'] + admin.site.register(InternetDraft, InternetDraftAdmin) class PersonOrOrgInfoAdmin(admin.ModelAdmin): list_display = ['person_or_org_tag', 'last_name', 'first_name', ] @@ -123,7 +130,8 @@ class RfcAdmin(admin.ModelAdmin): fieldsets=((None, {'fields': ('rfc_number', 'title', 'group_acronym', 'area_acronym', 'status', 'comments', 'last_modified_date')}), ('Metadata', {'fields': (('online_version', 'txt_page_count'), ('fyi_number', 'std_number')), 'classes': 'collapse'}), ('Standards Track Dates', {'fields': ('rfc_published_date', ('proposed_date', 'draft_date'), ('standard_date', 'historic_date')), 'classes': 'collapse'}), ('Last Call / Ballot Info', {'fields': ('intended_status', ('lc_sent_date', 'lc_expiration_date'), ('b_sent_date', 'b_approve_date')), 'classes': 'collapse'})) list_display=['rfc_number', 'title'] search_fields=['title'] -admin.site.register(Rfc, RfcAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + admin.site.register(Rfc, RfcAdmin) class RfcIntendedStatusAdmin(admin.ModelAdmin): pass diff --git a/ietf/idtracker/feeds.py b/ietf/idtracker/feeds.py index be373032f..00bf8fff5 100644 --- a/ietf/idtracker/feeds.py +++ b/ietf/idtracker/feeds.py @@ -1,5 +1,6 @@ # Copyright The IETF Trust 2007, All Rights Reserved +from django.conf import settings from django.contrib.syndication.feeds import Feed, FeedDoesNotExist from django.utils.feedgenerator import Atom1Feed from ietf.idtracker.models import IDInternal @@ -12,6 +13,9 @@ class DocumentComments(Feed): if len(bits) != 1: raise IDInternal.DoesNotExist rfc = re.match('rfc(\d+)', bits[0]) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return IDInternal.objects.get(docalias__name=bits[0]) + if rfc: return IDInternal.objects.get(draft=int(rfc.group(1)), rfc_flag=1) else: @@ -21,6 +25,9 @@ class DocumentComments(Feed): # filename is a function for RFCs and an attribute for I-Ds. # This works transparently for templates but is not transparent # for python. + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return "I-D Tracker comments for %s" % obj.filename + if obj.rfc_flag: filename = obj.document().filename() else: @@ -39,8 +46,7 @@ class DocumentComments(Feed): return obj.public_comments().order_by("-date","-id") def item_pubdate(self, item): - time = datetime.time(*[(t and int(t) or 0) for t in item.time.split(":")]) - return datetime.datetime.combine(item.date, time) + return item.datetime() def item_author_name(self, item): return item.get_author() @@ -52,7 +58,10 @@ class InLastCall(Feed): link = "/idtracker/status/last-call/" def items(self): - ret = list(IDInternal.objects.filter(primary_flag=1).filter(cur_state__state='In Last Call')) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + ret = list(IDInternal.objects.filter(iesg_state='lc')) + else: + ret = list(IDInternal.objects.filter(primary_flag=1).filter(cur_state__state='In Last Call')) ret.sort(key=lambda item: (item.document().lc_expiration_date or datetime.date.today())) return ret diff --git a/ietf/idtracker/models.py b/ietf/idtracker/models.py index dba05c278..c91eabfd2 100644 --- a/ietf/idtracker/models.py +++ b/ietf/idtracker/models.py @@ -183,6 +183,9 @@ class InternetDraft(models.Model): return "<%s>" % (self.filename_with_rev()) def filename_with_rev(self): return "%s-%s.txt" % (self.filename, self.revision_display()) + def name(self): + # small hack to make model forward-compatible with new schema + return self.filename def group_acronym(self): return self.group.acronym def group_ml_archive(self): @@ -255,11 +258,11 @@ class PersonOrOrgInfo(models.Model): date_created = models.DateField(auto_now_add=True, null=True) created_by = models.CharField(blank=True, null=True, max_length=8) address_type = models.CharField(blank=True, null=True, max_length=4) - def save(self): + def save(self, **kwargs): self.first_name_key = self.first_name.upper() self.middle_initial_key = self.middle_initial.upper() self.last_name_key = self.last_name.upper() - super(PersonOrOrgInfo, self).save() + super(PersonOrOrgInfo, self).save(**kwargs) def __str__(self): # For django.VERSION 0.96 if self.first_name == '' and self.last_name == '': @@ -273,16 +276,9 @@ class PersonOrOrgInfo(models.Model): def email(self, priority=1, type=None): name = unicode(self) email = '' - types = type and [ type ] or [ "INET", "Prim", None ] - for type in types: - try: - if type: - email = self.emailaddress_set.get(priority=priority, type=type).address - else: - email = self.emailaddress_set.get(priority=priority).address - break - except (EmailAddress.DoesNotExist, AssertionError): - pass + addresses = self.emailaddress_set.filter(address__contains="@").order_by('priority')[:1] + if addresses: + email = addresses[0].address.replace('<', '').replace('>', '') return (name, email) # Added by Sunny Lee to display person's affiliation - 5/26/2007 def affiliation(self, priority=1): @@ -410,6 +406,9 @@ class Rfc(models.Model): return "%s.txt" % ( self.filename() ) def filename(self): return "rfc%d" % ( self.rfc_number ) + def name(self): + # small hack to make model forward-compatible with new schema + return self.filename() def revision(self): return "RFC" def revision_display(self): @@ -1137,6 +1136,22 @@ class DocumentWrapper(object): def __init__(self, document): self.document = document +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + InternetDraftOld = InternetDraft + IDInternalOld = IDInternal + RfcOld = Rfc + BallotInfoOld = BallotInfo + IDStateOld = IDState + IDSubStateOld = IDSubState + AreaOld = Area + AcronymOld = Acronym + IESGLoginOld = IESGLogin + IETFWGOld = IETFWG + from redesign.doc.proxy import InternetDraft, IDInternal, BallotInfo, Rfc + from redesign.name.proxy import IDState, IDSubState + from redesign.group.proxy import Area, Acronym, IETFWG + from redesign.person.proxy import IESGLogin + # changes done by convert-096.py:changed maxlength to max_length # removed core diff --git a/ietf/idtracker/sitemaps.py b/ietf/idtracker/sitemaps.py index 7281c4cdc..650a4a94a 100644 --- a/ietf/idtracker/sitemaps.py +++ b/ietf/idtracker/sitemaps.py @@ -1,12 +1,16 @@ # Copyright The IETF Trust 2007, All Rights Reserved # from django.contrib.sitemaps import Sitemap +from django.conf import settings from ietf.idtracker.models import IDInternal, InternetDraft class IDTrackerMap(Sitemap): changefreq = "always" def items(self): - return IDInternal.objects.exclude(draft=999999) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return IDInternal.objects.all() + else: + return IDInternal.objects.exclude(draft=999999) class DraftMap(Sitemap): changefreq = "always" diff --git a/ietf/idtracker/templatetags/ietf_filters.py b/ietf/idtracker/templatetags/ietf_filters.py index 8830e1ee0..321bc7d6a 100644 --- a/ietf/idtracker/templatetags/ietf_filters.py +++ b/ietf/idtracker/templatetags/ietf_filters.py @@ -2,6 +2,7 @@ import textwrap from django import template +from django.conf import settings from django.utils.html import escape, fix_ampersands from django.template.defaultfilters import linebreaksbr, wordwrap, stringfilter from django.template import resolve_variable @@ -407,8 +408,18 @@ def startswith(x, y): # based on http://www.djangosnippets.org/snippets/847/ by 'whiteinge' @register.filter def in_group(user, groups): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return has_role(user, groups.replace("Area_Director", "Area Director")) + return user and user.is_authenticated() and bool(user.groups.filter(name__in=groups.split(',')).values('name')) +@register.filter +def has_role(user, role_names): + from ietf.ietfauth.decorators import has_role + if not user: + return False + return has_role(user, role_names.split(',')) + @register.filter def stable_dictsort(value, arg): """ diff --git a/ietf/idtracker/tests.py b/ietf/idtracker/tests.py index 613be5eb7..d9b63ba7a 100644 --- a/ietf/idtracker/tests.py +++ b/ietf/idtracker/tests.py @@ -25,18 +25,18 @@ class IdTrackerUrlTestCase(SimpleUrlTestCase): else: return content -class WGRoleTest(django.test.TestCase): - fixtures = ['wgtest'] - - def setUp(self): - from ietf.idtracker.models import IETFWG - self.xmas = IETFWG.objects.get(group_acronym__acronym='xmas') - self.snow = IETFWG.objects.get(group_acronym__acronym='snow') - - def test_roles(self): - print " Testing WG roles" - self.assertEquals(self.xmas.wgchair_set.all()[0].role(), 'xmas WG Chair') - self.assertEquals(self.snow.wgchair_set.all()[0].role(), 'snow BOF Chair') - self.assertEquals(self.xmas.wgsecretary_set.all()[0].role(), 'xmas WG Secretary') - self.assertEquals(self.xmas.wgtechadvisor_set.all()[0].role(), 'xmas Technical Advisor') - print "OK" +# class WGRoleTest(django.test.TestCase): +# fixtures = ['wgtest'] +# +# def setUp(self): +# from ietf.idtracker.models import IETFWG +# self.xmas = IETFWG.objects.get(group_acronym__acronym='xmas') +# self.snow = IETFWG.objects.get(group_acronym__acronym='snow') +# +# def test_roles(self): +# print " Testing WG roles" +# self.assertEquals(self.xmas.wgchair_set.all()[0].role(), 'xmas WG Chair') +# self.assertEquals(self.snow.wgchair_set.all()[0].role(), 'snow BOF Chair') +# self.assertEquals(self.xmas.wgsecretary_set.all()[0].role(), 'xmas WG Secretary') +# self.assertEquals(self.xmas.wgtechadvisor_set.all()[0].role(), 'xmas Technical Advisor') +# print "OK" diff --git a/ietf/idtracker/testurl.list b/ietf/idtracker/testurl.list index 97e6b3c3e..c2b06ee30 100644 --- a/ietf/idtracker/testurl.list +++ b/ietf/idtracker/testurl.list @@ -6,13 +6,8 @@ 200 /idtracker/status/last-call/ 301 /idtracker/rfc3847/ -301 /idtracker/12689/ 301 /idtracker/draft-ietf-isis-link-attr/ -301 /idtracker/draft-ietf-isis-link-attr/comment/65232/ 301 /idtracker/draft-eronen-tls-psk/ # no IESG information -301 /idtracker/comment/65232/ -301 /idtracker/ballot/1760/ -404 /idtracker/ballot/1723/ # dangling ballot, does not link to any doc 301 /idtracker/ 200 /feed/comments/draft-ietf-isis-link-attr/ 200 /feed/comments/rfc3373/ @@ -27,5 +22,4 @@ # Test case for missing comment time (bug fixed in changeset 1733) 200 /feed/comments/draft-ietf-msec-newtype-keyid/ -200,heavy /sitemap-drafts.xml 200,heavy /sitemap-idtracker.xml diff --git a/ietf/idtracker/views.py b/ietf/idtracker/views.py index 825ab0f62..d525274d8 100644 --- a/ietf/idtracker/views.py +++ b/ietf/idtracker/views.py @@ -2,11 +2,12 @@ # Create your views here. from django.http import HttpResponsePermanentRedirect, Http404 +from django.conf import settings from django.template import RequestContext from django.shortcuts import get_object_or_404, render_to_response from django.views.generic.list_detail import object_detail, object_list from ietf.idtracker.models import InternetDraft, IDInternal, IDState, IDSubState, BallotInfo, DocumentComment -import re +import re, datetime def state_desc(request, state, is_substate=0): if int(state) == 100: @@ -27,15 +28,30 @@ IESG to do anything with the document. context_instance=RequestContext(request)) def status(request): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + drafts = list(IDInternal.objects.exclude(iesg_state=None).exclude(iesg_state__in=('pub', 'dead', 'watching', 'rfcqueue')).order_by('iesg_state__order')) + drafts.sort(key=lambda d: (d.cur_state_id, d.status_date or datetime.date.min, d.b_sent_date or datetime.date.min)) + # sadly we can't use the generic view because it only works with a queryset... + return render_to_response('idtracker/status_of_items.html', dict(object_list=drafts, title="IESG Status of Items"), context_instance=RequestContext(request)) + queryset = IDInternal.objects.filter(primary_flag=1).exclude(cur_state__state__in=('RFC Ed Queue', 'RFC Published', 'AD is watching', 'Dead')).order_by('cur_state', 'status_date', 'ballot') return object_list(request, template_name="idtracker/status_of_items.html", queryset=queryset, extra_context={'title': 'IESG Status of Items'}) def last_call(request): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + drafts = list(IDInternal.objects.exclude(iesg_state=None).filter(iesg_state__in=('lc', 'writeupw', 'goaheadw')).order_by('iesg_state__order')) + drafts.sort(key=lambda d: (d.cur_state_id, d.status_date or datetime.date.min, d.b_sent_date or datetime.date.min)) + # sadly we can't use the generic view because it only works with a queryset... + return render_to_response('idtracker/status_of_items.html', dict(object_list=drafts, title="Documents in Last Call", lastcall=1), context_instance=RequestContext(request)) + queryset = IDInternal.objects.filter(primary_flag=1).filter(cur_state__state__in=('In Last Call', 'Waiting for Writeup', 'Waiting for AD Go-Ahead')).order_by('cur_state', 'status_date', 'ballot') return object_list(request, template_name="idtracker/status_of_items.html", queryset=queryset, extra_context={'title': 'Documents in Last Call', 'lastcall': 1}) def redirect_id(request, object_id): '''Redirect from historical document ID to preferred filename url.''' + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + raise Http404() # we don't store the numbers anymore + doc = get_object_or_404(InternetDraft, id_document_tag=object_id) return HttpResponsePermanentRedirect("/doc/"+doc.filename+"/") @@ -46,6 +62,9 @@ def redirect_filename(request, filename): return HttpResponsePermanentRedirect("/doc/"+filename+"/") def redirect_ballot(request, object_id): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + raise Http404() # we don't store the numbers anymore + ballot = get_object_or_404(BallotInfo, pk=object_id) ids = ballot.drafts.filter(primary_flag=1) if len(ids) == 0: @@ -57,6 +76,9 @@ def redirect_ballot(request, object_id): return HttpResponsePermanentRedirect("/doc/"+id.draft.filename+"/#ballot") def redirect_comment(request, object_id): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + raise Http404() # we don't store the numbers anymore + comment = get_object_or_404(DocumentComment, pk=object_id) id = comment.document if id.rfc_flag: diff --git a/ietf/iesg/feeds.py b/ietf/iesg/feeds.py index bd326f9d8..bf0e89353 100644 --- a/ietf/iesg/feeds.py +++ b/ietf/iesg/feeds.py @@ -1,5 +1,6 @@ # Copyright The IETF Trust 2007, 2008, All Rights Reserved +from django.conf import settings from django.contrib.syndication.feeds import Feed from django.utils.feedgenerator import Atom1Feed from ietf.idtracker.models import IDInternal @@ -11,12 +12,24 @@ class IESGAgenda(Feed): feed_type = Atom1Feed def items(self): - return IDInternal.objects.filter(agenda=1).order_by('telechat_date') + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from doc.models import TelechatDocEvent + drafts = IDInternal.objects.filter(docevent__telechatdocevent__telechat_date__gte=datetime.date.min).distinct() + for d in drafts: + d.latest_telechat_event = d.latest_event(TelechatDocEvent, type="scheduled_for_telechat") + drafts = [d for d in drafts if d.latest_telechat_event.telechat_date] + drafts.sort(key=lambda d: d.latest_telechat_event.telechat_date) + return drafts + + return IDInternal.objects.filter(agenda=1).order_by('telechat_date') def item_categories(self, item): return [ str(item.telechat_date) ] def item_pubdate(self, item): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return item.latest_telechat_event.time + f = item.comments().filter(comment_text__startswith='Placed on agenda for telechat') try: comment = f[0] @@ -28,4 +41,7 @@ class IESGAgenda(Feed): def item_author_name(self, item): return str( item.job_owner ) def item_author_email(self, item): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return item.ad.email_address() + return item.job_owner.person.email()[1] diff --git a/ietf/iesg/models.py b/ietf/iesg/models.py index f93fd7f28..899aaee1c 100644 --- a/ietf/iesg/models.py +++ b/ietf/iesg/models.py @@ -107,6 +107,7 @@ class WGAction(models.Model): (22, "WG Rechartering::Under evaluation for IETF Review"), (23, "WG Rechartering::Proposed for Approval") ) + # note that with the new schema, Acronym is monkey-patched and is really Group group_acronym = models.ForeignKey(Acronym, db_column='group_acronym_id', primary_key=True, unique=True) note = models.TextField(blank=True,null=True) status_date = models.DateField() diff --git a/ietf/iesg/tests.py b/ietf/iesg/tests.py index 193148514..3fbe4bf1d 100644 --- a/ietf/iesg/tests.py +++ b/ietf/iesg/tests.py @@ -54,57 +54,63 @@ class RescheduleOnAgendaTestCase(django.test.TestCase): self.assertEquals(draft.idinternal.comments().count(), comments_before + 1) self.assertTrue("Telechat" in draft.idinternal.comments()[0].comment_text) -class RescheduleInEditTestCase(django.test.TestCase): - fixtures = ['base', 'draft'] +class RescheduleOnAgendaTestCaseREDESIGN(django.test.TestCase): + fixtures = ['names'] def test_reschedule(self): - draft = InternetDraft.objects.get(filename="draft-ietf-mipshop-pfmipv6") - draft.idinternal.telechat_date = TelechatDates.objects.all()[0].dates()[0] - draft.idinternal.agenda = True - draft.idinternal.returning_item = True - draft.idinternal.save() - - form_id = draft.idinternal.draft_id - telechat_date_before = draft.idinternal.telechat_date + from ietf.utils.test_data import make_test_data + from redesign.person.models import Person + from doc.models import TelechatDocEvent - url = urlreverse('ietf.idrfc.views_edit.edit_info', kwargs={"name":"draft-ietf-mipshop-pfmipv6",}) - self.client.login(remote_user="klm") + draft = make_test_data() + + # add to schedule + e = TelechatDocEvent(type="scheduled_for_telechat") + e.doc = draft + e.by = Person.objects.get(name="Aread Irector") + e.telechat_date = TelechatDates.objects.all()[0].date1 + e.returning_item = True + e.save() + + form_id = draft.pk + telechat_date_before = e.telechat_date + + url = urlreverse('ietf.iesg.views.agenda_documents') + + self.client.login(remote_user="secretary") # normal get r = self.client.get(url) self.assertEquals(r.status_code, 200) q = PyQuery(r.content) - self.assertEquals(len(q('form select[name=telechat_date]')), 1) - self.assertEquals(len(q('form input[name=returning_item]')), 1) + + self.assertEquals(len(q('form select[name=%s-telechat_date]' % form_id)), 1) + self.assertEquals(len(q('form input[name=%s-clear_returning_item]' % form_id)), 1) # reschedule - comments_before = draft.idinternal.comments().count() + events_before = draft.docevent_set.count() d = TelechatDates.objects.all()[0].dates()[2] - r = self.client.post(url, { 'telechat_date': d.strftime("%Y-%m-%d"), - 'returning_item': "0", - 'job_owner': "49", - 'area_acronym': draft.idinternal.area_acronym_id, - 'note': draft.idinternal.note, - 'state_change_notice_to': draft.idinternal.state_change_notice_to, - 'intended_status': "6", }) - self.assertEquals(r.status_code, 302) + r = self.client.post(url, { '%s-telechat_date' % form_id: d.strftime("%Y-%m-%d"), + '%s-clear_returning_item' % form_id: "1" }) + + self.assertEquals(r.status_code, 200) # check that it moved below the right header in the DOM on the # agenda docs page - url = urlreverse('ietf.iesg.views.agenda_documents') - r = self.client.get(url) d_header_pos = r.content.find("IESG telechat %s" % d.strftime("%Y-%m-%d")) - draft_pos = r.content.find(draft.filename) + draft_pos = r.content.find(draft.name) self.assertTrue(d_header_pos < draft_pos) - draft = InternetDraft.objects.get(filename="draft-ietf-mipshop-pfmipv6") - self.assertEquals(draft.idinternal.telechat_date, d) - self.assertTrue(not draft.idinternal.returning_item) - self.assertEquals(draft.idinternal.comments().count(), comments_before + 1) - self.assertTrue("Telechat" in draft.idinternal.comments()[0].comment_text) + self.assertTrue(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat")) + self.assertEquals(draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").telechat_date, d) + self.assertTrue(not draft.latest_event(TelechatDocEvent, "scheduled_for_telechat").returning_item) + self.assertEquals(draft.docevent_set.count(), events_before + 1) +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + RescheduleOnAgendaTestCase = RescheduleOnAgendaTestCaseREDESIGN + class ManageTelechatDatesTestCase(django.test.TestCase): fixtures = ['base', 'draft'] @@ -146,6 +152,56 @@ class ManageTelechatDatesTestCase(django.test.TestCase): self.assertTrue(dates.date4 == new_date) self.assertTrue(dates.date1 == old_date2) +class ManageTelechatDatesTestCaseREDESIGN(django.test.TestCase): + fixtures = ['names'] + + def test_set_dates(self): + from ietf.utils.test_data import make_test_data + make_test_data() + + dates = TelechatDates.objects.all()[0] + url = urlreverse('ietf.iesg.views.telechat_dates') + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form input[name=date1]')), 1) + + # post + new_date = dates.date1 + timedelta(days=7) + + r = self.client.post(url, dict(date1=new_date.isoformat(), + date2=new_date.isoformat(), + date3=new_date.isoformat(), + date4=new_date.isoformat(), + )) + self.assertEquals(r.status_code, 200) + + dates = TelechatDates.objects.all()[0] + self.assertTrue(dates.date1 == new_date) + + def test_rollup_dates(self): + from ietf.utils.test_data import make_test_data + make_test_data() + + dates = TelechatDates.objects.all()[0] + url = urlreverse('ietf.iesg.views.telechat_dates') + login_testing_unauthorized(self, "secretary", url) + + old_date2 = dates.date2 + new_date = dates.date4 + timedelta(days=14) + r = self.client.post(url, dict(rollup_dates="1")) + self.assertEquals(r.status_code, 200) + + dates = TelechatDates.objects.all()[0] + self.assertTrue(dates.date4 == new_date) + self.assertTrue(dates.date1 == old_date2) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + ManageTelechatDatesTestCase = ManageTelechatDatesTestCaseREDESIGN + class WorkingGroupActionsTestCase(django.test.TestCase): fixtures = ['base', 'wgactions'] @@ -255,7 +311,152 @@ class WorkingGroupActionsTestCase(django.test.TestCase): self.assertEquals(r.status_code, 200) self.assertTrue('(sieve)' not in r.content) + +class WorkingGroupActionsTestCaseREDESIGN(django.test.TestCase): + fixtures = ['names'] + + def setUp(self): + super(self.__class__, self).setUp() + + curdir = os.path.dirname(os.path.abspath(__file__)) + self.evaldir = os.path.join(curdir, "tmp-testdir") + os.mkdir(self.evaldir) + src = os.path.join(curdir, "fixtures", "sieve-charter.txt") + shutil.copy(src, self.evaldir) + + settings.IESG_WG_EVALUATION_DIR = self.evaldir + + def tearDown(self): + super(self.__class__, self).tearDown() + shutil.rmtree(self.evaldir) + + + def test_working_group_actions(self): + from ietf.utils.test_data import make_test_data + + make_test_data() + + url = urlreverse('iesg_working_group_actions') + login_testing_unauthorized(self, "secretary", url) + + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + for wga in WGAction.objects.all(): + self.assertTrue(wga.group_acronym.name in r.content) + + self.assertTrue('(sieve)' in r.content) + + def test_delete_wgaction(self): + from ietf.utils.test_data import make_test_data + + make_test_data() + + wga = WGAction.objects.all()[0] + url = urlreverse('iesg_edit_working_group_action', kwargs=dict(wga_id=wga.pk)) + login_testing_unauthorized(self, "secretary", url) + + r = self.client.post(url, dict(delete="1")) + self.assertEquals(r.status_code, 302) + self.assertTrue(not WGAction.objects.filter(pk=wga.pk)) + + def test_edit_wgaction(self): + from ietf.utils.test_data import make_test_data + from redesign.person.models import Person + + make_test_data() + + wga = WGAction.objects.all()[0] + url = urlreverse('iesg_edit_working_group_action', kwargs=dict(wga_id=wga.pk)) + login_testing_unauthorized(self, "secretary", url) + + # normal get + r = self.client.get(url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertEquals(len(q('form select[name=token_name]')), 1) + self.assertEquals(len(q('form select[name=telechat_date]')), 1) + + # change + dates = TelechatDates.objects.all()[0] + token_name = Person.objects.get(name="Ad No1").name_parts()[1] + old = wga.pk + r = self.client.post(url, dict(status_date=dates.date1.isoformat(), + token_name=token_name, + category="23", + note="Testing.", + telechat_date=dates.date4.isoformat())) + self.assertEquals(r.status_code, 302) + + wga = WGAction.objects.get(pk=old) + self.assertEquals(wga.status_date, dates.date1) + self.assertEquals(wga.token_name, token_name) + self.assertEquals(wga.category, 23) + self.assertEquals(wga.note, "Testing.") + self.assertEquals(wga.telechat_date, dates.date4) + + def test_add_possible_wg(self): + from ietf.utils.test_data import make_test_data + from redesign.person.models import Person + from redesign.group.models import Group + + make_test_data() + + url = urlreverse('iesg_working_group_actions') + login_testing_unauthorized(self, "secretary", url) + + r = self.client.post(url, dict(add="1", + filename='sieve-charter.txt')) + self.assertEquals(r.status_code, 302) + + # now we got back a URL we can use for adding, but first make + # sure we got a proposed group with the acronym + group = Group.objects.create( + name="Sieve test test", + acronym="sieve", + state_id="proposed", + type_id="wg", + parent=None + ) + + add_url = r['Location'] + r = self.client.get(add_url) + self.assertEquals(r.status_code, 200) + q = PyQuery(r.content) + self.assertTrue('(sieve)' in r.content) + self.assertEquals(len(q('form select[name=token_name]')), 1) + self.assertEquals(q('form input[name=status_date]')[0].get("value"), "2010-05-07") + self.assertEquals(len(q('form select[name=telechat_date]')), 1) + + wgas_before = WGAction.objects.all().count() + dates = TelechatDates.objects.all()[0] + token_name = Person.objects.get(name="Ad No1").name_parts()[1] + r = self.client.post(add_url, + dict(status_date=dates.date1.isoformat(), + token_name=token_name, + category="23", + note="Testing.", + telechat_date=dates.date4.isoformat())) + self.assertEquals(r.status_code, 302) + self.assertEquals(wgas_before + 1, WGAction.objects.all().count()) + + def test_delete_possible_wg(self): + from ietf.utils.test_data import make_test_data + + make_test_data() + + url = urlreverse('iesg_working_group_actions') + login_testing_unauthorized(self, "secretary", url) + + r = self.client.post(url, dict(delete="1", + filename='sieve-charter.txt')) + self.assertEquals(r.status_code, 200) + + self.assertTrue('(sieve)' not in r.content) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + WorkingGroupActionsTestCase = WorkingGroupActionsTestCaseREDESIGN class IesgUrlTestCase(SimpleUrlTestCase): diff --git a/ietf/iesg/testurl.list b/ietf/iesg/testurl.list index 47ee7ca6b..1f7734032 100644 --- a/ietf/iesg/testurl.list +++ b/ietf/iesg/testurl.list @@ -20,8 +20,5 @@ 200 /iesg/ann/new/ # This takes ~ 300s: #200 /iesg/ann/prev/ -200 /iesg/ann/2422/ -200 /iesg/ann/1563/ -404 /iesg/ann/567/ 200 /feed/iesg-agenda/ diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index 186995551..1867a8bf1 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -60,6 +60,18 @@ def date_threshold(): return ret def inddocs(request): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + queryset_list_ind = [d for d in InternetDraft.objects.filter(tags__slug="via-rfc", docevent__type="iesg_approved").distinct() if d.latest_event(type__in=("iesg_disapproved", "iesg_approved")).type == "iesg_approved"] + queryset_list_ind.sort(key=lambda d: d.b_approve_date, reverse=True) + + queryset_list_ind_dnp = [d for d in IDInternal.objects.filter(tags__slug="via-rfc", docevent__type="iesg_disapproved").distinct() if d.latest_event(type__in=("iesg_disapproved", "iesg_approved")).type == "iesg_disapproved"] + queryset_list_ind_dnp.sort(key=lambda d: d.dnp_date, reverse=True) + + return render_to_response('iesg/independent_doc.html', + dict(object_list=queryset_list_ind, + object_list_dnp=queryset_list_ind_dnp), + context_instance=RequestContext(request)) + queryset_list_ind = InternetDraft.objects.filter(idinternal__via_rfc_editor=1, idinternal__rfc_flag=0, idinternal__noproblem=1, idinternal__dnp=0).order_by('-b_approve_date') queryset_list_ind_dnp = IDInternal.objects.filter(via_rfc_editor = 1,rfc_flag=0,dnp=1).order_by('-dnp_date') return object_list(request, queryset=queryset_list_ind, template_name='iesg/independent_doc.html', allow_empty=True, extra_context={'object_list_dnp':queryset_list_ind_dnp }) @@ -95,6 +107,61 @@ def wgdocs(request,cat): queryset_list_doc.append(sub_item2) return render_to_response( 'iesg/ietf_doc.html', {'object_list': queryset_list, 'object_list_doc':queryset_list_doc, 'is_recent':is_recent}, context_instance=RequestContext(request) ) +def wgdocsREDESIGN(request,cat): + is_recent = 0 + proto_actions = [] + doc_actions = [] + threshold = date_threshold() + + proto_levels = ["bcp", "ds", "ps", "std"] + doc_levels = ["exp", "inf"] + + if cat == 'new': + is_recent = 1 + + drafts = InternetDraft.objects.filter(docevent__type="iesg_approved", docevent__time__gte=threshold, intended_std_level__in=proto_levels + doc_levels).exclude(tags__slug="via-rfc").distinct() + for d in drafts: + if d.b_approve_date and d.b_approve_date >= threshold: + if d.intended_std_level_id in proto_levels: + proto_actions.append(d) + elif d.intended_std_level_id in doc_levels: + doc_actions.append(d) + + elif cat == 'prev': + # proto + start_date = datetime.date(1997, 12, 1) + + drafts = InternetDraft.objects.filter(docevent__type="iesg_approved", docevent__time__lt=threshold, docevent__time__gte=start_date, intended_std_level__in=proto_levels).exclude(tags__slug="via-rfc").distinct() + + for d in drafts: + if d.b_approve_date and start_date <= d.b_approve_date < threshold: + proto_actions.append(d) + + # doc + start_date = datetime.date(1998, 10, 15) + + drafts = InternetDraft.objects.filter(docevent__type="iesg_approved", docevent__time__lt=threshold, docevent__time__gte=start_date, intended_std_level__in=doc_levels).exclude(tags__slug="via-rfc").distinct() + + for d in drafts: + if d.b_approve_date and start_date <= d.b_approve_date < threshold: + doc_actions.append(d) + else: + raise Http404 + + proto_actions.sort(key=lambda d: d.b_approve_date, reverse=True) + doc_actions.sort(key=lambda d: d.b_approve_date, reverse=True) + + return render_to_response('iesg/ietf_doc.html', + dict(object_list=proto_actions, + object_list_doc=doc_actions, + is_recent=is_recent, + title_prefix="Recent" if is_recent else "Previous"), + context_instance=RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + wgdocs = wgdocsREDESIGN + + def get_doc_section(id): states = [16,17,18,19,20,21] if id.document().intended_status.intended_status_id in [1,2,6,7]: @@ -119,14 +186,63 @@ def get_doc_section(id): s = s + "1" return s -def agenda_docs(date, next_agenda): - if next_agenda: - matches = IDInternal.objects.filter(telechat_date=date, primary_flag=1, agenda=1) +def get_doc_sectionREDESIGN(id): + states = [16,17,18,19,20,21] + if id.intended_std_level_id in ["bcp", "ds", "ps", "std"]: + s = "2" else: - matches = IDInternal.objects.filter(telechat_date=date, primary_flag=1) - idmatches = matches.filter(rfc_flag=0).order_by('ballot') - rfcmatches = matches.filter(rfc_flag=1).order_by('ballot') - res = {} + s = "3" + + g = id.document().group_acronym() + if g and str(g) != 'none': + s = s + "1" + elif (s == "3") and id.via_rfc_editor: + s = s + "3" + else: + s = s + "2" + if not id.rfc_flag and id.cur_state.document_state_id not in states: + s = s + "3" + elif id.returning_item: + s = s + "2" + else: + s = s + "1" + return s + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + get_doc_section = get_doc_sectionREDESIGN + +def agenda_docs(date, next_agenda): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from doc.models import TelechatDocEvent + + matches = IDInternal.objects.filter(docevent__telechatdocevent__telechat_date=date) + + idmatches = [] + rfcmatches = [] + + for m in matches: + if m.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date != date: + continue + + if next_agenda and not m.agenda: + continue + + if m.docalias_set.filter(name__startswith="rfc"): + rfcmatches.append(m) + else: + idmatches.append(m) + + idmatches.sort(key=lambda d: d.start_date or datetime.date.min) + rfcmatches.sort(key=lambda d: d.start_date or datetime.date.min) + else: + if next_agenda: + matches = IDInternal.objects.filter(telechat_date=date, primary_flag=1, agenda=1) + else: + matches = IDInternal.objects.filter(telechat_date=date, primary_flag=1) + idmatches = matches.filter(rfc_flag=0).order_by('ballot') + rfcmatches = matches.filter(rfc_flag=1).order_by('ballot') + + res = dict(("s%s%s%s" % (i, j, k), []) for i in range(2, 5) for j in range (1, 4) for k in range(1, 4)) for id in list(idmatches)+list(rfcmatches): section_key = "s"+get_doc_section(id) if section_key not in res: @@ -189,11 +305,12 @@ def agenda_txt(request): def agenda_scribe_template(request): date = TelechatDates.objects.all()[0].date1 docs = agenda_docs(date, True) - return render_to_response('iesg/scribe_template.html', {'date':str(date), 'docs':docs}, context_instance=RequestContext(request) ) + return render_to_response('iesg/scribe_template.html', {'date':str(date), 'docs':docs, 'USE_DB_REDESIGN_PROXY_CLASSES': settings.USE_DB_REDESIGN_PROXY_CLASSES}, context_instance=RequestContext(request) ) def _agenda_moderator_package(request): data = _agenda_data(request) data['ad_names'] = [str(x) for x in IESGLogin.active_iesg()] + data['ad_names'].sort(key=lambda x: x.split(' ')[-1]) return render_to_response("iesg/moderator_package.html", data, context_instance=RequestContext(request)) @group_required('Area_Director','Secretariat') @@ -224,7 +341,13 @@ def agenda_documents_txt(request): dates = TelechatDates.objects.all()[0].dates() docs = [] for date in dates: - docs.extend(IDInternal.objects.filter(telechat_date=date, primary_flag=1, agenda=1)) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from doc.models import TelechatDocEvent + for d in IDInternal.objects.filter(docevent__telechatdocevent__telechat_date=date): + if d.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date == date: + docs.append(d) + else: + docs.extend(IDInternal.objects.filter(telechat_date=date, primary_flag=1, agenda=1)) t = loader.get_template('iesg/agenda_documents.txt') c = Context({'docs':docs}) return HttpResponse(t.render(c), mimetype='text/plain') @@ -259,12 +382,20 @@ def handle_reschedule_form(request, idinternal, dates): if request.method == 'POST': form = RescheduleForm(request.POST, **formargs) if form.is_valid(): - update_telechat(request, idinternal, - form.cleaned_data['telechat_date']) - if form.cleaned_data['clear_returning_item']: - idinternal.returning_item = False - idinternal.event_date = datetime.date.today() - idinternal.save() + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + login = request.user.get_profile() + update_telechat(request, idinternal, login, + form.cleaned_data['telechat_date'], + False if form.cleaned_data['clear_returning_item'] else None) + idinternal.time = datetime.datetime.now() + idinternal.save() + else: + update_telechat(request, idinternal, + form.cleaned_data['telechat_date']) + if form.cleaned_data['clear_returning_item']: + idinternal.returning_item = False + idinternal.event_date = datetime.date.today() + idinternal.save() else: form = RescheduleForm(**formargs) @@ -273,7 +404,16 @@ def handle_reschedule_form(request, idinternal, dates): def agenda_documents(request): dates = TelechatDates.objects.all()[0].dates() - idinternals = list(IDInternal.objects.filter(telechat_date__in=dates,primary_flag=1,agenda=1).order_by('rfc_flag', 'ballot')) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from doc.models import TelechatDocEvent + idinternals = [] + for d in IDInternal.objects.filter(docevent__telechatdocevent__telechat_date__in=dates): + if d.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date in dates: + idinternals.append(d) + + idinternals.sort(key=lambda d: (d.rfc_flag, d.start_date)) + else: + idinternals = list(IDInternal.objects.filter(telechat_date__in=dates,primary_flag=1,agenda=1).order_by('rfc_flag', 'ballot')) for i in idinternals: i.reschedule_form = handle_reschedule_form(request, i, dates) @@ -294,7 +434,10 @@ def agenda_documents(request): w.iprUrl = "/ipr/search?option=document_search&id_document_tag=" + str(w.id.tracker_id) iprs = IprDraft.objects.filter(document=w.id.tracker_id) else: - ri = RfcIndex.objects.get(rfc_number=i.draft_id) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + ri = i + else: + ri = RfcIndex.objects.get(rfc_number=i.draft_id) w = RfcWrapper(ri) w.iprUrl = "/ipr/search?option=rfc_search&rfc_search=" + str(w.rfc.rfc_number) iprs = IprRfc.objects.filter(document=w.rfc.rfc_number) @@ -307,7 +450,14 @@ def agenda_documents(request): def telechat_docs_tarfile(request,year,month,day): from tempfile import mkstemp date=datetime.date(int(year),int(month),int(day)) - docs= IDInternal.objects.filter(telechat_date=date, primary_flag=1, agenda=1) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from doc.models import TelechatDocEvent + docs = [] + for d in IDInternal.objects.filter(docevent__telechatdocevent__telechat_date=date): + if d.latest_event(TelechatDocEvent, type="scheduled_for_telechat").telechat_date == date: + docs.append(d) + else: + docs= IDInternal.objects.filter(telechat_date=date, primary_flag=1, agenda=1) response = HttpResponse(mimetype='application/octet-stream') response['Content-Disposition'] = 'attachment; filename=telechat-%s-%s-%s-docs.tgz'%(year, month, day) tarstream = tarfile.open('','w:gz',response) @@ -330,6 +480,29 @@ def telechat_docs_tarfile(request,year,month,day): return response def discusses(request): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + res = [] + + for d in IDInternal.objects.filter(iesg_state__in=("pub-req", "ad-eval", "review-e", "lc-req", "lc", "writeupw", "goaheadw", "iesg-eva", "defer", "watching"), docevent__ballotpositiondocevent__pos="discuss").distinct(): + found = False + for p in d.positions.all(): + if p.discuss: + found = True + break + + if not found: + continue + + if d.rfc_flag: + doc = RfcWrapper(d) + else: + doc = IdWrapper(draft=d) + + if doc.in_ietf_process() and doc.ietf_process.has_active_iesg_ballot(): + res.append(doc) + + return direct_to_template(request, 'iesg/discusses.html', {'docs':res}) + positions = Position.objects.filter(discuss=1) res = [] try: diff --git a/ietf/ietfauth/decorators.py b/ietf/ietfauth/decorators.py index a1c3db2fc..c1b1d66a6 100644 --- a/ietf/ietfauth/decorators.py +++ b/ietf/ietfauth/decorators.py @@ -31,6 +31,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from django.utils.http import urlquote +from django.conf import settings +from django.db.models import Q from django.contrib.auth.decorators import _CheckLogin from django.http import HttpResponseRedirect, HttpResponseForbidden @@ -62,3 +64,48 @@ def group_required(*group_names): def decorate(view_func): return _CheckLogin403(view_func, lambda u: bool(u.groups.filter(name__in=group_names)), "Restricted to group%s %s" % ("s" if len(group_names) != 1 else "", ",".join(group_names))) return decorate + + +def has_role(user, role_names): + """Determines whether user has any of the given standard roles + given. Role names must be a list or, in case of a single value, a + string.""" + if isinstance(role_names, str) or isinstance(role_names, unicode): + role_names = [ role_names ] + + if not user or not user.is_authenticated(): + return False + + from redesign.person.models import Person + + try: + person = user.get_profile() + except Person.DoesNotExist: + return False + + role_qs = { + "Area Director": Q(email__person=person, name="ad"), + "Secretariat": Q(email__person=person, name="secr", group__acronym="secretariat") + } + + filter_expr = Q() + for r in role_names: + filter_expr |= role_qs[r] + + from redesign.group.models import Role + return bool(Role.objects.filter(filter_expr)[:1]) + +def role_required(*role_names): + """View decorator for checking that the user is logged in and + belongs to (at least) one of the listed roles. Users who are not + logged in are redirected to the login page; users who don't have + one of the roles get a "403" page. + """ + def decorate(view_func): + return _CheckLogin403(view_func, + lambda u: has_role(u, role_names), + "Restricted to role%s %s" % ("s" if len(role_names) != 1 else "", ",".join(role_names))) + return decorate + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + group_required = lambda *group_names: role_required(*[n.replace("Area_Director", "Area Director") for n in group_names]) diff --git a/ietf/ietfauth/models.py b/ietf/ietfauth/models.py index 7f97c8853..12b0ea73c 100644 --- a/ietf/ietfauth/models.py +++ b/ietf/ietfauth/models.py @@ -70,6 +70,20 @@ class IetfUserProfile(models.Model): except: return None + def email(self): + # quick hack to bind new and old schema together for the time being + try: + l = IESGLogin.objects.get(login_name=self.user.username) + if l.person: + person = l.person + else: + person = PersonOrOrgInfo.objects.get(first_name=l.first_name, + last_name=l.last_name) + except IESGLogin.DoesNotExist, PersonOrOrgInfo.DoesNotExist: + person = None + from person.models import Email + return Email.objects.get(address=person.email()[1]) + def __str__(self): return "IetfUserProfile(%s)" % (self.user,) diff --git a/ietf/ietfauth/tests.py b/ietf/ietfauth/tests.py index e4c6d6675..c9e2d8371 100644 --- a/ietf/ietfauth/tests.py +++ b/ietf/ietfauth/tests.py @@ -31,6 +31,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import unittest +from django.conf import settings from django.contrib.auth.models import User from django.test.client import Client from ietf.utils.test_utils import SimpleUrlTestCase, RealDatabaseTest @@ -41,6 +42,8 @@ class IetfAuthUrlTestCase(SimpleUrlTestCase): def testUrls(self): self.doTestUrls(__file__) +# this test case should really work on a test database instead of the +# real one class IetfAuthTestCase(unittest.TestCase,RealDatabaseTest): def setUp(self): self.setUpRealDatabase() @@ -61,7 +64,7 @@ class IetfAuthTestCase(unittest.TestCase,RealDatabaseTest): response = c.get(nexturl[2], {}, False, REMOTE_USER=username) self.assertEquals(response.status_code, 200) - self.assert_("Roles/Groups:" in response.content) + self.assert_("User name" in response.content) return response def testLogin(self): @@ -95,4 +98,7 @@ class IetfAuthTestCase(unittest.TestCase,RealDatabaseTest): self.assert_("IETF_Chair" in groups) print "OK" - + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + # this test doesn't make any sense anymore + IetfAuthTestCase.testGroups = lambda x: None diff --git a/ietf/ietfauth/views.py b/ietf/ietfauth/views.py index e350791a8..5750d23cc 100644 --- a/ietf/ietfauth/views.py +++ b/ietf/ietfauth/views.py @@ -69,4 +69,21 @@ def ietf_loggedin(request): @login_required def profile(request): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from person.models import Person + from group.models import Role + + roles = [] + person = None + try: + person = request.user.get_profile() + roles = Role.objects.filter(email__person=person).distinct() + except Person.DoesNotExist: + pass + + return render_to_response('registration/profileREDESIGN.html', + dict(roles=roles, + person=person), + context_instance=RequestContext(request)) + return render_to_response('registration/profile.html', context_instance=RequestContext(request)) diff --git a/ietf/ietfworkflows/templatetags/ietf_streams.py b/ietf/ietfworkflows/templatetags/ietf_streams.py index 7e88cf0f2..a459d74b6 100644 --- a/ietf/ietfworkflows/templatetags/ietf_streams.py +++ b/ietf/ietfworkflows/templatetags/ietf_streams.py @@ -14,6 +14,8 @@ register = template.Library() @register.inclusion_tag('ietfworkflows/stream_state.html', takes_context=True) def stream_state(context, doc): + from django.conf import settings + return settings.TEMPLATE_STRING_IF_INVALID # FIXME: temporary work-around request = context.get('request', None) data = {} stream = get_stream_from_wrapper(doc) @@ -52,6 +54,8 @@ def workflow_history_entry(context, entry): @register.inclusion_tag('ietfworkflows/edit_actions.html', takes_context=True) def edit_actions(context, wrapper): + return None # FIXME: temporary work-around + request = context.get('request', None) user = request and request.user if not user: diff --git a/ietf/ietfworkflows/utils.py b/ietf/ietfworkflows/utils.py index 8f6575443..e523f1ac5 100644 --- a/ietf/ietfworkflows/utils.py +++ b/ietf/ietfworkflows/utils.py @@ -284,6 +284,15 @@ def update_stream(obj, comment, person, to_stream, extra_notify=[]): def get_full_info_for_draft(draft): + return dict(# FIXME: temporary work-around + streamed=settings.TEMPLATE_STRING_IF_INVALID, + stream=settings.TEMPLATE_STRING_IF_INVALID, + workflow=settings.TEMPLATE_STRING_IF_INVALID, + tags=[settings.TEMPLATE_STRING_IF_INVALID], + state=settings.TEMPLATE_STRING_IF_INVALID, + shepherd=draft.shepherd, + ) + return dict( streamed=get_streamed_draft(draft), stream=get_stream_from_draft(draft), diff --git a/ietf/ipr/admin.py b/ietf/ipr/admin.py index 4f66fb079..700ecba65 100644 --- a/ietf/ipr/admin.py +++ b/ietf/ipr/admin.py @@ -1,5 +1,6 @@ #coding: utf-8 from django.contrib import admin +from django.conf import settings from ietf.ipr.models import * class IprContactAdmin(admin.ModelAdmin): @@ -14,7 +15,8 @@ admin.site.register(IprDetail, IprDetailAdmin) class IprDraftAdmin(admin.ModelAdmin): pass -admin.site.register(IprDraft, IprDraftAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + admin.site.register(IprDraft, IprDraftAdmin) class IprLicensingAdmin(admin.ModelAdmin): pass @@ -26,7 +28,8 @@ admin.site.register(IprNotification, IprNotificationAdmin) class IprRfcAdmin(admin.ModelAdmin): pass -admin.site.register(IprRfc, IprRfcAdmin) +if not settings.USE_DB_REDESIGN_PROXY_CLASSES: + admin.site.register(IprRfc, IprRfcAdmin) class IprSelecttypeAdmin(admin.ModelAdmin): pass diff --git a/ietf/ipr/models.py b/ietf/ipr/models.py index c895c453a..314ab13f0 100644 --- a/ietf/ipr/models.py +++ b/ietf/ipr/models.py @@ -1,6 +1,7 @@ # Copyright The IETF Trust 2007, All Rights Reserved from django.db import models +from django.conf import settings #from django import newforms as forms from ietf.idtracker.views import InternetDraft from ietf.idtracker.models import Rfc @@ -117,6 +118,8 @@ class IprDetail(models.Model): def __unicode__(self): return self.title.decode("latin-1", 'replace') def docs(self): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + return list(IprDraftProxy.objects.filter(ipr=self)) return list(self.drafts.all()) + list(self.rfcs.all()) @models.permalink def get_absolute_url(self): @@ -155,8 +158,8 @@ class IprContact(models.Model): class IprDraft(models.Model): - ipr = models.ForeignKey(IprDetail, related_name='drafts') - document = models.ForeignKey(InternetDraft, db_column='id_document_tag', related_name="ipr") + ipr = models.ForeignKey(IprDetail, related_name='drafts_old' if settings.USE_DB_REDESIGN_PROXY_CLASSES else 'drafts') + document = models.ForeignKey(InternetDraft, db_column='id_document_tag', related_name="ipr_draft_old" if settings.USE_DB_REDESIGN_PROXY_CLASSES else "ipr") revision = models.CharField(max_length=2) def __str__(self): return "%s which applies to %s-%s" % ( self.ipr, self.document, self.revision ) @@ -174,8 +177,8 @@ class IprNotification(models.Model): db_table = 'ipr_notifications' class IprRfc(models.Model): - ipr = models.ForeignKey(IprDetail, related_name='rfcs') - document = models.ForeignKey(Rfc, db_column='rfc_number', related_name="ipr") + ipr = models.ForeignKey(IprDetail, related_name='rfcs_old' if settings.USE_DB_REDESIGN_PROXY_CLASSES else 'rfcs') + document = models.ForeignKey(Rfc, db_column='rfc_number', related_name="ipr_rfc_old" if settings.USE_DB_REDESIGN_PROXY_CLASSES else "ipr") def __str__(self): return "%s applies to RFC%04d" % ( self.ipr, self.document_id ) class Meta: @@ -190,6 +193,68 @@ class IprUpdate(models.Model): class Meta: db_table = 'ipr_updates' + +if settings.USE_DB_REDESIGN_PROXY_CLASSES or hasattr(settings, "IMPORTING_IPR"): + from doc.models import DocAlias + + class IprDocAlias(models.Model): + ipr = models.ForeignKey(IprDetail, related_name='documents') + doc_alias = models.ForeignKey(DocAlias) + rev = models.CharField(max_length=2, blank=True) + def __unicode__(self): + if self.rev: + return u"%s which applies to %s-%s" % (self.ipr, self.document, self.revision) + else: + return u"%s which applies to %s" % (self.ipr, self.document) + + # proxy stuff + IprDraftOld = IprDraft + IprRfcOld = IprRfc + + from redesign.proxy_utils import TranslatingManager + + class IprDraftProxy(IprDocAlias): + objects = TranslatingManager(dict(document="doc_alias__name")) + + # document = models.ForeignKey(InternetDraft, db_column='id_document_tag', "ipr") + # document = models.ForeignKey(Rfc, db_column='rfc_number', related_name="ipr") + @property + def document(self): + from redesign.doc.proxy import DraftLikeDocAlias + return DraftLikeDocAlias.objects.get(pk=self.doc_alias_id) + + #revision = models.CharField(max_length=2) + @property + def revision(self): + return self.rev + + class Meta: + proxy = True + + IprDraft = IprDraftProxy + + class IprRfcProxy(IprDocAlias): + objects = TranslatingManager(dict(document=lambda v: ("doc_alias__name", "rfc%s" % v))) + + # document = models.ForeignKey(InternetDraft, db_column='id_document_tag', "ipr") + # document = models.ForeignKey(Rfc, db_column='rfc_number', related_name="ipr") + @property + def document(self): + from redesign.doc.proxy import DraftLikeDocAlias + return DraftLikeDocAlias.objects.get(pk=self.doc_alias_id) + + #revision = models.CharField(max_length=2) + @property + def revision(self): + return self.rev + + class Meta: + proxy = True + + IprRfc = IprRfcProxy + + + # changes done by convert-096.py:changed maxlength to max_length # removed core # removed edit_inline diff --git a/ietf/ipr/new.py b/ietf/ipr/new.py index 0917ef62d..7d360e71e 100644 --- a/ietf/ipr/new.py +++ b/ietf/ipr/new.py @@ -102,11 +102,19 @@ def new(request, type, update=None, submitter=None): setattr(self, contact, ContactForm(prefix=contact[:4], initial=contact_initial.get(contact, {}), *args, **kwnoinit)) rfclist_initial = "" if update: - rfclist_initial = " ".join(["RFC%d" % rfc.document_id for rfc in update.rfcs.all()]) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from ietf.ipr.models import IprDocAlias + rfclist_initial = " ".join(a.doc_alias.name.upper() for a in IprDocAlias.objects.filter(doc_alias__name__startswith="rfc", ipr=update)) + else: + rfclist_initial = " ".join(["RFC%d" % rfc.document_id for rfc in update.rfcs.all()]) self.base_fields["rfclist"] = forms.CharField(required=False, initial=rfclist_initial) draftlist_initial = "" if update: - draftlist_initial = " ".join([draft.document.filename + (draft.revision and "-%s" % draft.revision or "") for draft in update.drafts.all()]) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from ietf.ipr.models import IprDocAlias + draftlist_initial = " ".join(a.doc_alias.name + ("-%s" % a.rev if a.rev else "") for a in IprDocAlias.objects.filter(ipr=update).exclude(doc_alias__name__startswith="rfc")) + else: + draftlist_initial = " ".join([draft.document.filename + (draft.revision and "-%s" % draft.revision or "") for draft in update.drafts.all()]) self.base_fields["draftlist"] = forms.CharField(required=False, initial=draftlist_initial) if section_list.get("holder_contact", False): self.base_fields["hold_contact_is_submitter"] = forms.BooleanField(required=False) @@ -134,7 +142,11 @@ def new(request, type, update=None, submitter=None): rfclist = rfclist.strip().split() for rfc in rfclist: try: - Rfc.objects.get(rfc_number=int(rfc)) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from redesign.doc.models import DocAlias + DocAlias.objects.get(name="rfc%s" % int(rfc)) + else: + Rfc.objects.get(rfc_number=int(rfc)) except: raise forms.ValidationError("Unknown RFC number: %s - please correct this." % rfc) rfclist = " ".join(rfclist) @@ -155,7 +167,13 @@ def new(request, type, update=None, submitter=None): filename = draft rev = None try: - id = InternetDraft.objects.get(filename=filename) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from redesign.doc.models import DocAlias + id = DocAlias.objects.get(name=filename) + # proxy attribute for code below + id.revision = id.document.rev + else: + id = InternetDraft.objects.get(filename=filename) except Exception, e: log("Exception: %s" % e) raise forms.ValidationError("Unknown Internet-Draft: %s - please correct this." % filename) @@ -263,15 +281,32 @@ def new(request, type, update=None, submitter=None): # Save IprDraft(s) for draft in form.cleaned_data["draftlist"].split(): - id = InternetDraft.objects.get(filename=draft[:-3]) - iprdraft = models.IprDraft(document=id, ipr=instance, revision=draft[-2:]) - iprdraft.save() + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + name = draft[:-3] + rev = draft[-2:] + + from redesign.doc.models import DocAlias + models.IprDocAlias.objects.create( + doc_alias=DocAlias.objects.get(name=name), + ipr=instance, + rev=rev) + else: + id = InternetDraft.objects.get(filename=draft[:-3]) + iprdraft = models.IprDraft(document=id, ipr=instance, revision=draft[-2:]) + iprdraft.save() # Save IprRfc(s) for rfcnum in form.cleaned_data["rfclist"].split(): - rfc = Rfc.objects.get(rfc_number=int(rfcnum)) - iprrfc = models.IprRfc(document=rfc, ipr=instance) - iprrfc.save() + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from redesign.doc.models import DocAlias + models.IprDocAlias.objects.create( + doc_alias=DocAlias.objects.get(name="rfc%s" % int(rfcnum)), + ipr=instance, + rev="") + else: + rfc = Rfc.objects.get(rfc_number=int(rfcnum)) + iprrfc = models.IprRfc(document=rfc, ipr=instance) + iprrfc.save() send_mail(request, settings.IPR_EMAIL_TO, ('IPR Submitter App', 'ietf-ipr@ietf.org'), 'New IPR Submission Notification', "ipr/new_update_email.txt", {"ipr": instance, "update": update}) return render("ipr/submitted.html", {"update": update}, context_instance=RequestContext(request)) diff --git a/ietf/ipr/related.py b/ietf/ipr/related.py index 9c4881771..2b4c68567 100644 --- a/ietf/ipr/related.py +++ b/ietf/ipr/related.py @@ -1,5 +1,7 @@ # Copyright The IETF Trust 2007, All Rights Reserved +from django.conf import settings +from django.db.models import Q from ietf.idtracker.models import InternetDraft, Rfc inverse = { @@ -77,3 +79,59 @@ def related_docs(doc, found = []): set_relation(doc, 'is_draft_of', item) found = related_docs(item, found) return found + +def related_docsREDESIGN(alias, _): + """Get related document aliases to given alias through depth-first search.""" + from redesign.doc.models import RelatedDocument + from redesign.doc.proxy import DraftLikeDocAlias + + mapping = dict( + updates='that updated', + obs='that obsoleted', + replaces='that replaced', + ) + inverse_mapping = dict( + updates='that was updated by', + obs='that was obsoleted by', + replaces='that was replaced by', + ) + + res = [ alias ] + remaining = [ alias ] + while remaining: + a = remaining.pop() + related = RelatedDocument.objects.filter(relationship__in=mapping.keys()).filter(Q(source=a.document) | Q(target=a)) + for r in related: + if r.source == a.document: + found = DraftLikeDocAlias.objects.filter(pk=r.target_id) + inverse = True + else: + found = DraftLikeDocAlias.objects.filter(document=r.source) + inverse = False + + for x in found: + if not x in res: + x.related = a + x.relation = (inverse_mapping if inverse else mapping)[r.relationship_id] + res.append(x) + remaining.append(x) + + # there's one more source of relatedness, a draft can have been published + aliases = DraftLikeDocAlias.objects.filter(document=a.document).exclude(pk__in=[x.pk for x in res]) + for oa in aliases: + rel = None + if a.name.startswith("rfc") and oa.name.startswith("draft"): + rel = "that was published as" + elif a.name.startswith("draft") and oa.name.startswith("rfc"): + rel = "which came from" + + if rel: + oa.related = a + oa.relation = rel + res.append(oa) + remaining.append(oa) + + return res + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + related_docs = related_docsREDESIGN diff --git a/ietf/ipr/search.py b/ietf/ipr/search.py index 5af8e9dd4..865a6e498 100644 --- a/ietf/ipr/search.py +++ b/ietf/ipr/search.py @@ -24,7 +24,11 @@ def mark_last_doc(iprs): def iprs_from_docs(docs): iprs = [] for doc in docs: - if isinstance(doc, InternetDraft): + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from ietf.ipr.models import IprDocAlias + disclosures = [ x.ipr for x in IprDocAlias.objects.filter(doc_alias=doc, ipr__status__in=[1,3]) ] + + elif isinstance(doc, InternetDraft): disclosures = [ item.ipr for item in IprDraft.objects.filter(document=doc, ipr__status__in=[1,3]) ] elif isinstance(doc, Rfc): disclosures = [ item.ipr for item in IprRfc.objects.filter(document=doc, ipr__status__in=[1,3]) ] @@ -50,7 +54,11 @@ def patent_file_search(url, q): return False def search(request, type="", q="", id=""): - wgs = IETFWG.objects.filter(group_type__group_type_id=1).exclude(group_acronym__acronym='2000').select_related().order_by('acronym.acronym') + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from group.models import Group + wgs = Group.objects.filter(type="wg").exclude(acronym="2000").select_related().order_by("acronym") + else: + wgs = IETFWG.objects.filter(group_type__group_type_id=1).exclude(group_acronym__acronym='2000').select_related().order_by('acronym.acronym') args = request.REQUEST.items() if args: for key, value in args: @@ -70,20 +78,32 @@ def search(request, type="", q="", id=""): if type == "document_search": if q: q = normalize_draftname(q) - start = InternetDraft.objects.filter(filename__contains=q) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from redesign.doc.proxy import DraftLikeDocAlias + start = DraftLikeDocAlias.objects.filter(name__contains=q, name__startswith="draft") + else: + start = InternetDraft.objects.filter(filename__contains=q) if id: - try: - id = int(id,10) - except: - id = -1 - start = InternetDraft.objects.filter(id_document_tag=id) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from redesign.doc.proxy import DraftLikeDocAlias + start = DraftLikeDocAlias.objects.filter(name=id) + else: + try: + id = int(id,10) + except: + id = -1 + start = InternetDraft.objects.filter(id_document_tag=id) if type == "rfc_search": if q: try: q = int(q, 10) except: q = -1 - start = Rfc.objects.filter(rfc_number=q) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from redesign.doc.proxy import DraftLikeDocAlias + start = DraftLikeDocAlias.objects.filter(name__contains=q, name__startswith="rfc") + else: + start = Rfc.objects.filter(rfc_number=q) if start.count() == 1: first = start[0] doc = str(first) @@ -142,12 +162,20 @@ def search(request, type="", q="", id=""): # Search by wg acronym # Document list with IPRs elif type == "wg_search": - try: - docs = list(InternetDraft.objects.filter(group__acronym=q)) - except: - docs = [] - docs += [ draft.replaced_by for draft in docs if draft.replaced_by_id ] - docs += list(Rfc.objects.filter(group_acronym=q)) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from redesign.doc.proxy import DraftLikeDocAlias + try: + docs = list(DraftLikeDocAlias.objects.filter(document__group__acronym=q)) + docs += list(DraftLikeDocAlias.objects.filter(document__relateddocument__target__in=docs, document__relateddocument__relationship="replaces")) + except: + docs = [] + else: + try: + docs = list(InternetDraft.objects.filter(group__acronym=q)) + except: + docs = [] + docs += [ draft.replaced_by for draft in docs if draft.replaced_by_id ] + docs += list(Rfc.objects.filter(group_acronym=q)) docs = [ doc for doc in docs if doc.ipr.count() ] iprs, docs = iprs_from_docs(docs) @@ -158,11 +186,18 @@ def search(request, type="", q="", id=""): # Search by rfc and id title # Document list with IPRs elif type == "title_search": - try: - docs = list(InternetDraft.objects.filter(title__icontains=q)) - except: - docs = [] - docs += list(Rfc.objects.filter(title__icontains=q)) + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from redesign.doc.proxy import DraftLikeDocAlias + try: + docs = list(DraftLikeDocAlias.objects.filter(document__title__icontains=q)) + except: + docs = [] + else: + try: + docs = list(InternetDraft.objects.filter(title__icontains=q)) + except: + docs = [] + docs += list(Rfc.objects.filter(title__icontains=q)) docs = [ doc for doc in docs if doc.ipr.count() ] iprs, docs = iprs_from_docs(docs) diff --git a/ietf/ipr/tests.py b/ietf/ipr/tests.py index e179a66c9..9cda36100 100644 --- a/ietf/ipr/tests.py +++ b/ietf/ipr/tests.py @@ -48,6 +48,8 @@ class IprUrlTestCase(SimpleUrlTestCase): else: return content +# this test should be ported to run on a test database instead of the +# real database, and possibly expanded class NewIprTestCase(unittest.TestCase,RealDatabaseTest): SPECIFIC_DISCLOSURE = { 'legal_name':'Testing Only Please Ignore', @@ -58,6 +60,7 @@ class NewIprTestCase(unittest.TestCase,RealDatabaseTest): 'ietf_telephone':'555-555-0101', 'ietf_email':'test.participant@example.com', 'rfclist':'1149', + 'draftlist':'draft-burdis-http-sasl-00', 'patents':'none', 'date_applied':'never', 'country':'nowhere', diff --git a/ietf/ipr/views.py b/ietf/ipr/views.py index 6b6932376..17aab5539 100644 --- a/ietf/ipr/views.py +++ b/ietf/ipr/views.py @@ -48,6 +48,34 @@ def list_drafts(request): context_instance=RequestContext(request)), mimetype="text/plain") +def list_draftsREDESIGN(request): + from ipr.models import IprDocAlias + + docipr = {} + + for o in IprDocAlias.objects.filter(ipr__status=1).select_related("doc_alias"): + name = o.doc_alias.name + if name.startswith("rfc"): + name = name.upper() + + if not name in docipr: + docipr[name] = [] + + docipr[name].append(o.ipr_id) + + docs = [ dict(name=name, iprs=sorted(iprs)) for name, iprs in docipr.iteritems() ] + + # drafts.html is not an HTML file + return HttpResponse(render_to_string("ipr/drafts.html", + dict(docs=docs), + context_instance=RequestContext(request)), + mimetype="text/plain") + + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + list_drafts = list_draftsREDESIGN + + # Details views def show(request, ipr_id=None, removed=None): @@ -93,6 +121,12 @@ def show(request, ipr_id=None, removed=None): except: # if file does not exist, iframe is used instead pass + + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + from ietf.ipr.models import IprDraft, IprRfc + ipr.drafts = IprDraft.objects.filter(ipr=ipr).exclude(doc_alias__name__startswith="rfc").order_by("id") + ipr.rfcs = IprRfc.objects.filter(ipr=ipr).filter(doc_alias__name__startswith="rfc").order_by("id") + return render("ipr/details.html", {"ipr": ipr, "section_list": section_list}, context_instance=RequestContext(request)) diff --git a/ietf/mailinglists/urls.py b/ietf/mailinglists/urls.py index acef9834e..2787cf30e 100644 --- a/ietf/mailinglists/urls.py +++ b/ietf/mailinglists/urls.py @@ -3,8 +3,10 @@ from django.conf.urls.defaults import patterns from ietf.idtracker.models import IETFWG +http_archive_wg_queryset = IETFWG.objects.filter(email_archive__startswith='http') + urlpatterns = patterns('django.views.generic.list_detail', - (r'^wg/$', 'object_list', { 'queryset': IETFWG.objects.filter(email_archive__startswith='http'), 'template_name': 'mailinglists/wgwebmail_list.html' }), + (r'^wg/$', 'object_list', { 'queryset': http_archive_wg_queryset, 'template_name': 'mailinglists/wgwebmail_list.html' }), ) urlpatterns += patterns('', (r'^nonwg/$', 'django.views.generic.simple.redirect_to', { 'url': 'http://www.ietf.org/list/nonwg.html'}), diff --git a/ietf/meeting/models.py b/ietf/meeting/models.py index ca2f5d54a..88bc0e229 100644 --- a/ietf/meeting/models.py +++ b/ietf/meeting/models.py @@ -1,3 +1,139 @@ -# Copyright The IETF Trust 2007, All Rights Reserved +# old meeting models can be found in ../proceedings/models.py + +import pytz + +from django.db import models +from timedeltafield import TimedeltaField + +from redesign.group.models import Group +from redesign.person.models import Person +from redesign.name.models import TimeSlotTypeName, SessionStatusName, ConstraintName + +countries = pytz.country_names.items() +countries.sort(lambda x,y: cmp(x[1], y[1])) + +timezones = [(name, name) for name in pytz.common_timezones] +timezones.sort() + +class Meeting(models.Model): + # Number is not an integer any more, in order to be able to accomodate + # interim meetings (and other variations?) + number = models.CharField(max_length=64) + # Date is useful when generating a set of timeslot for this meeting, but + # is not used to determine date for timeslot instances thereafter, as + # they have their own datetime field. + date = models.DateField() + city = models.CharField(blank=True, max_length=255) + country = models.CharField(blank=True, max_length=2, choices=countries) + # We can't derive time-zone from country, as there are some that have + # more than one timezone, and the pytz module doesn't provide timezone + # lookup information for all relevant city/country combinations. + time_zone = models.CharField(blank=True, max_length=255, choices=timezones) + venue_name = models.CharField(blank=True, max_length=255) + venue_addr = models.TextField(blank=True) + break_area = models.CharField(blank=True, max_length=255) + reg_area = models.CharField(blank=True, max_length=255) + + def __str__(self): + return "IETF-%s" % (self.number) + def get_meeting_date (self,offset): + return self.date + datetime.timedelta(days=offset) + # cut-off dates (draft submission cut-of, wg agenda cut-off, minutes + # submission cut-off), and more, are probably methods of this class, + # rather than fields on a Proceedings class. + + @classmethod + def get_first_cut_off(cls): + date = cls.objects.all().order_by('-date')[0].date + offset = datetime.timedelta(days=settings.FIRST_CUTOFF_DAYS) + return date - offset + + @classmethod + def get_second_cut_off(cls): + date = cls.objects.all().order_by('-date')[0].date + offset = datetime.timedelta(days=settings.SECOND_CUTOFF_DAYS) + return date - offset + + @classmethod + def get_ietf_monday(cls): + date = cls.objects.all().order_by('-date')[0].date + return date + datetime.timedelta(days=-date.weekday(), weeks=1) + + +class Room(models.Model): + meeting = models.ForeignKey(Meeting) + name = models.CharField(max_length=255) + + def __unicode__(self): + return self.name + +class TimeSlot(models.Model): + """ + Everything that would appear on the meeting agenda of a meeting is mapped + to a time slot, including breaks (i.e., also NonSession+NonSessionRef. + Sessions are connected to TimeSlots during scheduling. + A template function to populate a meeting with an appropriate set of TimeSlots + is probably also needed. + """ + meeting = models.ForeignKey(Meeting) + type = models.ForeignKey(TimeSlotTypeName) + name = models.CharField(max_length=255) + time = models.DateTimeField() + duration = TimedeltaField() + location = models.ForeignKey(Room, blank=True, null=True) + show_location = models.BooleanField(default=True) + + def __unicode__(self): + location = self.get_location() + if not location: + location = "(no location)" + + return u"%s: %s-%s %s, %s" % (self.meeting.number, self.time.strftime("%m-%d %H:%M"), (self.time + self.duration).strftime("%H:%M"), self.name, location) + + def get_location(self): + location = self.location + if location: + location = location.name + elif self.type_id == "reg": + location = self.meeting.reg_area + elif self.type_id == "break": + location = self.meeting.break_area + + if not self.show_location: + location = "" + + return location + + +class Constraint(models.Model): + """Specifies a constraint on the scheduling between source and + target, e.g. some kind of conflict.""" + meeting = models.ForeignKey(Meeting) + source = models.ForeignKey(Group, related_name="constraint_source_set") + target = models.ForeignKey(Group, related_name="constraint_target_set") + name = models.ForeignKey(ConstraintName) + + def __unicode__(self): + return u"%s %s %s" % (self.source, self.name.lower(), self.target) + +class Session(models.Model): + meeting = models.ForeignKey(Meeting) + timeslot = models.ForeignKey(TimeSlot, null=True, blank=True) # Null until session has been scheduled + group = models.ForeignKey(Group) # The group type determines the session type. BOFs also need to be added as a group. + attendees = models.IntegerField(null=True, blank=True) + agenda_note = models.CharField(blank=True, max_length=255) + # + requested = models.DateTimeField() + requested_by = models.ForeignKey(Person) + requested_duration = TimedeltaField() + comments = models.TextField() + # + status = models.ForeignKey(SessionStatusName) + scheduled = models.DateTimeField(null=True, blank=True) + modified = models.DateTimeField(null=True, blank=True) + +# Agendas, Minutes and Slides are all mapped to Document. + +# IESG history is extracted from GroupHistory, rather than hand coded in a +# separate table. -# Meeting models can be found under ../proceedings/ diff --git a/ietf/meeting/timedeltafield.py b/ietf/meeting/timedeltafield.py new file mode 100644 index 000000000..cc2e538c5 --- /dev/null +++ b/ietf/meeting/timedeltafield.py @@ -0,0 +1,191 @@ +# -*- coding: iso-8859-1 -*- +# $Id: TimedeltaField.py 1787 2011-04-20 07:09:57Z tguettler $ +# $HeadURL: svn+ssh://svnserver/svn/djangotools/trunk/dbfields/TimedeltaField.py $ + +# from http://djangosnippets.org/snippets/1060/ with some fixes + +# Python +import datetime + +# Django +from django import forms +from django.db import models +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy as _ + +#Djangotools +#from djangotools.utils.southutils import add_introspection_rules_from_baseclass + +SECS_PER_DAY=3600*24 + +class TimedeltaField(models.Field): + u''' + Store Python's datetime.timedelta in an integer column. + Most database systems only support 32 bit integers by default. + ''' + __metaclass__ = models.SubfieldBase + empty_strings_allowed = False + + def __init__(self, *args, **kwargs): + super(TimedeltaField, self).__init__(*args, **kwargs) + + def to_python(self, value): + if (value is None) or isinstance(value, datetime.timedelta): + return value + + try: + # else try to convert to int (e.g. from string) + value = int(value) + except (TypeError, ValueError): + raise exceptions.ValidationError( + _("This value must be an integer or a datetime.timedelta.")) + + return datetime.timedelta(seconds=value) + + def get_internal_type(self): + return 'IntegerField' + + def get_db_prep_lookup(self, lookup_type, value, connection=None, prepared=False): + raise NotImplementedError() # SQL WHERE + + def get_db_prep_save(self, value, connection=None, prepared=False): + if (value is None) or isinstance(value, int): + return value + return SECS_PER_DAY*value.days+value.seconds + + def formfield(self, *args, **kwargs): + defaults={'form_class': TimedeltaFormField} + defaults.update(kwargs) + return super(TimedeltaField, self).formfield(*args, **defaults) + + def value_to_string(self, obj): + value = self._get_val_from_obj(obj) + return self.get_db_prep_value(value) + +#South Plugin registrieren +#add_introspection_rules_from_baseclass(TimedeltaField, ["^djangotools\.dbfields\.TimedeltaField"]) + +class TimedeltaFormField(forms.Field): + default_error_messages = { + 'invalid': _(u'Enter a whole number.'), + } + + def __init__(self, *args, **kwargs): + defaults={'widget': TimedeltaWidget} + defaults.update(kwargs) + super(TimedeltaFormField, self).__init__(*args, **defaults) + + def clean(self, value): + # value comes from Timedelta.Widget.value_from_datadict(): tuple of strings + super(TimedeltaFormField, self).clean(value) + assert len(value)==len(self.widget.inputs), (value, self.widget.inputs) + i=0 + for value, multiply in zip(value, self.widget.multiply): + try: + i+=int(value)*multiply + except ValueError, TypeError: + raise forms.ValidationError(self.error_messages['invalid']) + return i + +class TimedeltaWidget(forms.Widget): + INPUTS=['days', 'hours', 'minutes', 'seconds'] + MULTIPLY=[60*60*24, 60*60, 60, 1] + def __init__(self, attrs=None): + self.widgets=[] + if not attrs: + attrs={} + inputs=attrs.get('inputs', self.INPUTS) + multiply=[] + for input in inputs: + assert input in self.INPUTS, (input, self.INPUT) + self.widgets.append(forms.TextInput(attrs=attrs)) + multiply.append(self.MULTIPLY[self.INPUTS.index(input)]) + self.inputs=inputs + self.multiply=multiply + super(TimedeltaWidget, self).__init__(attrs) + + def render(self, name, value, attrs): + if value is None: + values=[0 for i in self.inputs] + elif isinstance(value, datetime.timedelta): + values=split_seconds(value.days*SECS_PER_DAY+value.seconds, self.inputs, self.multiply) + elif isinstance(value, int): + # initial data from model + values=split_seconds(value, self.inputs, self.multiply) + else: + assert isinstance(value, tuple), (value, type(value)) + assert len(value)==len(self.inputs), (value, self.inputs) + values=value + id=attrs.pop('id') + assert not attrs, attrs + rendered=[] + for input, widget, val in zip(self.inputs, self.widgets, values): + rendered.append(u'%s %s' % (_(input), widget.render('%s_%s' % (name, input), val))) + return mark_safe('<div id="%s">%s</div>' % (id, ' '.join(rendered))) + + def value_from_datadict(self, data, files, name): + # Don't throw ValidationError here, just return a tuple of strings. + ret=[] + for input, multi in zip(self.inputs, self.multiply): + ret.append(data.get('%s_%s' % (name, input), 0)) + return tuple(ret) + + def _has_changed(self, initial_value, data_value): + # data_value comes from value_from_datadict(): A tuple of strings. + if initial_value is None: + return bool(set(data_value)!=set([u'0'])) + assert isinstance(initial_value, datetime.timedelta), initial_value + initial=tuple([unicode(i) for i in split_seconds(initial_value.days*SECS_PER_DAY+initial_value.seconds, self.inputs, self.multiply)]) + assert len(initial)==len(data_value), (initial, data_value) + return bool(initial!=data_value) + +def main(): + assert split_seconds(1000000)==[11, 13, 46, 40] + + field=TimedeltaField() + + td=datetime.timedelta(days=10, seconds=11) + s=field.get_db_prep_save(td) + assert isinstance(s, int), (s, type(s)) + td_again=field.to_python(s) + assert td==td_again, (td, td_again) + + td=datetime.timedelta(seconds=11) + s=field.get_db_prep_save(td) + td_again=field.to_python(s) + assert td==td_again, (td, td_again) + + field=TimedeltaFormField() + assert field.widget._has_changed(datetime.timedelta(seconds=0), (u'0', u'0', u'0', u'0',)) is False + assert field.widget._has_changed(None, (u'0', u'0', u'0', u'0',)) is False + assert field.widget._has_changed(None, (u'0', u'0')) is False + assert field.widget._has_changed(datetime.timedelta(days=1, hours=2, minutes=3, seconds=4), (u'1', u'2', u'3', u'4',)) is False + + for secs, soll, kwargs in [ + (100, [0, 0, 1, 40], dict()), + (100, ['0days', '0hours', '1minutes', '40seconds'], dict(with_unit=True)), + (100, ['1minutes', '40seconds'], dict(with_unit=True, remove_leading_zeros=True)), + (100000, ['1days', '3hours'], dict(inputs=['days', 'hours'], with_unit=True, remove_leading_zeros=True)), + ]: + ist=split_seconds(secs, **kwargs) + if ist!=soll: + raise Exception('geg=%s soll=%s ist=%s kwargs=%s' % (secs, soll, ist, kwargs)) + + print "unittest OK" + +def split_seconds(secs, inputs=TimedeltaWidget.INPUTS, multiply=TimedeltaWidget.MULTIPLY, + with_unit=False, remove_leading_zeros=False): + ret=[] + assert len(inputs)<=len(multiply), (inputs, multiply) + for input, multi in zip(inputs, multiply): + count, secs = divmod(secs, multi) + if remove_leading_zeros and not ret and not count: + continue + if with_unit: + ret.append('%s%s' % (count, input)) + else: + ret.append(count) + return ret + +if __name__=='__main__': + main() diff --git a/ietf/proceedings/models.py b/ietf/proceedings/models.py index 850a68948..201e0762d 100644 --- a/ietf/proceedings/models.py +++ b/ietf/proceedings/models.py @@ -3,6 +3,7 @@ from django.db import models from django.conf import settings from ietf.idtracker.models import Acronym, PersonOrOrgInfo, IRTF, AreaGroup, Area, IETFWG +from ietf.utils.broken_foreign_key import BrokenForeignKey import datetime #from ietf.utils import log @@ -188,7 +189,7 @@ class NonSessionRef(models.Model): class NonSession(models.Model): non_session_id = models.AutoField(primary_key=True) - day_id = models.IntegerField(blank=True, null=True) + day_id = models.IntegerField(blank=True, null=True) # NULL means all days non_session_ref = models.ForeignKey(NonSessionRef) meeting = models.ForeignKey(Meeting, db_column='meeting_num') time_desc = models.CharField(blank=True, max_length=75) @@ -363,18 +364,18 @@ class WgMeetingSession(models.Model, ResolveAcronym): ts_status_id = models.IntegerField(null=True, blank=True) requested_date = models.DateField(null=True, blank=True) approved_date = models.DateField(null=True, blank=True) - requested_by = models.ForeignKey(PersonOrOrgInfo, db_column='requested_by') + requested_by = BrokenForeignKey(PersonOrOrgInfo, db_column='requested_by', null=True, null_values=(0, 888888)) scheduled_date = models.DateField(null=True, blank=True) last_modified_date = models.DateField(null=True, blank=True) ad_comments = models.TextField(blank=True,null=True) sched_room_id1 = models.ForeignKey(MeetingRoom, db_column='sched_room_id1', null=True, blank=True, related_name='here1') - sched_time_id1 = models.ForeignKey(MeetingTime, db_column='sched_time_id1', null=True, blank=True, related_name='now1') + sched_time_id1 = BrokenForeignKey(MeetingTime, db_column='sched_time_id1', null=True, blank=True, related_name='now1') sched_date1 = models.DateField(null=True, blank=True) sched_room_id2 = models.ForeignKey(MeetingRoom, db_column='sched_room_id2', null=True, blank=True, related_name='here2') - sched_time_id2 = models.ForeignKey(MeetingTime, db_column='sched_time_id2', null=True, blank=True, related_name='now2') + sched_time_id2 = BrokenForeignKey(MeetingTime, db_column='sched_time_id2', null=True, blank=True, related_name='now2') sched_date2 = models.DateField(null=True, blank=True) sched_room_id3 = models.ForeignKey(MeetingRoom, db_column='sched_room_id3', null=True, blank=True, related_name='here3') - sched_time_id3 = models.ForeignKey(MeetingTime, db_column='sched_time_id3', null=True, blank=True, related_name='now3') + sched_time_id3 = BrokenForeignKey(MeetingTime, db_column='sched_time_id3', null=True, blank=True, related_name='now3') sched_date3 = models.DateField(null=True, blank=True) special_agenda_note = models.CharField(blank=True, max_length=255) combined_room_id1 = models.ForeignKey(MeetingRoom, db_column='combined_room_id1', null=True, blank=True, related_name='here4') diff --git a/ietf/settings.py b/ietf/settings.py index e00f93077..0f389ec02 100644 --- a/ietf/settings.py +++ b/ietf/settings.py @@ -8,9 +8,12 @@ import os import syslog syslog.openlog("django", syslog.LOG_PID, syslog.LOG_LOCAL0) - BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +import sys +sys.path.append(os.path.abspath(BASE_DIR + "/..")) +sys.path.append(os.path.abspath(BASE_DIR + "/../redesign")) + DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -121,6 +124,11 @@ INSTALLED_APPS = ( 'south', 'workflows', 'permissions', + 'redesign.person', + 'redesign.name', + 'redesign.group', + 'redesign.doc', + 'redesign.issue', 'ietf.announcements', 'ietf.idindex', 'ietf.idtracker', @@ -229,6 +237,13 @@ MAX_DAILY_SUBMISSION = 1000 MAX_DAILY_SUBMISSION_SIZE = 2000 # End of ID Submission Tool settings +# DB redesign +USE_DB_REDESIGN_PROXY_CLASSES = True + +if USE_DB_REDESIGN_PROXY_CLASSES: + AUTH_PROFILE_MODULE = 'person.Person' + AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.RemoteUserBackend', ) + # Put SECRET_KEY in here, or any other sensitive or site-specific # changes. DO NOT commit settings_local.py to svn. from settings_local import * diff --git a/ietf/templates/announcements/message_detail.html b/ietf/templates/announcements/message_detail.html new file mode 100644 index 000000000..29dbbb4be --- /dev/null +++ b/ietf/templates/announcements/message_detail.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load ietf_filters %} + +{% block title %}Announcement: {{ message.time|date:"F j, Y" }} -- {{ message.subject|escape }}{% endblock %} + +{% block content %} +<h1>NomCom Message</h1> +<p> +From: {{ message.frm|escape }}<br/> +To: {{ message.to|escape }}<br/> +Date: {{ message.time|date:"F j, Y" }}<br/> +Subject: {{ message.subject|escape }} +</p> +<hr width="400" align="left" /> +<pre> +{{ message.body|escape }} +</pre> +{% endblock %} diff --git a/ietf/templates/announcements/nomcomREDESIGN.html b/ietf/templates/announcements/nomcomREDESIGN.html new file mode 100644 index 000000000..7870db472 --- /dev/null +++ b/ietf/templates/announcements/nomcomREDESIGN.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} +{% load ietf_filters %} +{% block title %}IAB/IESG Nominating Committee{% endblock %} +{% block content %} + +<h1>IAB/IESG Nominating Committee</h1> + +<h3>Current Committee Chair: <a href="mailto:{{ curr_chair.address }}">{{ curr_chair.get_name }}</a></h3> + +{% for regime in regimes %} + <hr> + <h1>Messages from {{ regime.group.start_year }} - {{ regime.group.end_year }}</h1> + <h4>Committee Chair: <a href="mailto:{{ regime.chair.address }}">{{ regime.chair.get_name }}</a></h4> + <table class="ietf-table"> + <tr> + <th width="10%">Date</th> + <th width="60%">Subject</th> + <th width="30%">Sent To</th> + </tr> + {% for ann in regime.announcements %} + <tr> + <td>{{ ann.time|date:"Y-M-d" }}</td> + <td style="max-width:50%"><a href="/ann/nomcom/{{ ann.id }}/">{{ ann.subject|escape }}</a></td> + <td>{{ ann.to_name }}</td> + <tr> + {% endfor %} + </table> +{% endfor %} + +<hr> + +{# somebody ought to import these announcements in the DB instead of this mess #} + +<h3>Messages from 2003-2004 NomCom</h3> +Committee Chair: <A HREF="mailto:richdr@microsoft.com">Rich Draves</A> +<br><br><li><a href="http://www.ietf.org/old/2009/nomcom/msg08-25-2003.txt">IETF Nominations Committee Chair Announcement</a> August 25, 2003 +<LI><a href="http://www.ietf.org/old/2009/nomcom/msg09.22.txt">NomCom call for volunteers</a> September 22, 2003 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/select-announce_03.txt">Selection of the Nominations Committee</A> +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg10.06.txt">NomCom Volunteer List</a> October 06, 2004 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg1010.txt">NomCom Selection</a> October 10, 2003 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg10.17.txt">Call for Nominees</a> October 17, 2003 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg10.24.txt">NomCom members</a> October 24, 2003 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg11.07.txt">NomCom at IETF</a> November 07, 2003 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg11.14.txt">NomCom News</a> November 14, 2003 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg11.26.txt">Reminder - nominations to replace Randy Bush</a> November 26, 2003 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg12.01.txt">Randy Bush replacement schedule</a> December 01, 2003 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg01.14.txt">Randy Bush replacement</a> January 14, 2004 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg02.13.txt">NomCom results</a> February 13, 2004 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg09.28.txt">Call for Security AD nominations</a> September 28, 2004 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg11.07.04.txt">Steve Bellovin replacement</a> November 07, 2004 + +<h3>Messages from 2002-2003 NomCom</h3> + +Committee Chair: <A HREF="mailto:PRoberts@MEGISTO.com">Phil Roberts</A> +<br><br> +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg19765.html">First Call for Volunteers</A> July 30, 2002 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/select-announce.txt">Selection of the Nominations Committee</A> +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg0918.txt">Announcement of the Nominations Committee</A> September 18, 2002 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg10.21.txt">Announcement of IESG and IAB Nominations Requests</A> October 21, 2002 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg11.05.txt">Announcement of IESG and IAB Nominations Requests</A> November 5, 2002 +<LI><A HREF="http://www.ietf.org/old/2009/nomcom/msg11.12.txt">Announcement of IESG and IAB Nominations Requests</A> November 12, 2002 +<LI><a href="http://www.ietf.org/old/2009/nomcom/msg02.27.txt">IETF Nomcom Announcement</a> February 27, 2003 +<LI><a href="http://www.ietf.org/old/2009/nomcom/msg06.11.txt">Announcement of IESG and IAB Nominations Request</a> June 11, 2003 +<LI><a href="http://www.ietf.org/old/2009/nomcom/msg07.15.txt">Nomcom result announcement</a> July 15, 2003 + +<h3>Historical Information</h3> + +<li><a href="http://www.ietf.org/nomcom/committee.html">IAB/IESG Nominating Committee Members (by year)</a> + +<h3>References</h3> + +<LI><A HREF="http://www.ietf.org/rfc/rfc2026.txt">The Internet Standards Process (RFC 2026)</A> +<LI><A HREF="http://www.ietf.org/rfc/rfc3777.txt">IAB and IESG Selection, Confirmation, and Recall Process: Operation of the Nominating and Recall Committees (RFC 3777) (Also BCP10)</A> +<LI><A HREF="http://www.ietf.org/rfc/rfc3797.txt">Publicly Verifiable Nominations Committee (NomCom) Random Selection (RFC 3797)</A> + +{% endblock %} diff --git a/ietf/templates/idrfc/add_comment.html b/ietf/templates/idrfc/add_comment.html index 38b60b303..54208d886 100644 --- a/ietf/templates/idrfc/add_comment.html +++ b/ietf/templates/idrfc/add_comment.html @@ -16,7 +16,7 @@ form.add-comment .actions { {% block content %} <h1>Add comment on {{ doc }}</h1> -<p>The comment will be added to the comment trail.</p> +<p>The comment will be added to the history trail.</p> <form class="add-comment" action="" method="POST"> <table> @@ -24,7 +24,7 @@ form.add-comment .actions { <tr> <td></td> <td class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back</a> + <a href="{{ back_url }}">Back</a> <input type="submit" value="Add comment"/> </td> </tr> diff --git a/ietf/templates/idrfc/approve_ballot.html b/ietf/templates/idrfc/approve_ballot.html index 3960f1253..f7a4c8ef1 100644 --- a/ietf/templates/idrfc/approve_ballot.html +++ b/ietf/templates/idrfc/approve_ballot.html @@ -30,7 +30,7 @@ form.approve-ballot .announcement { </div> <div class="actions"> - <a href="{% url doc_ballot_approvaltext name=doc.filename %}">Back</a> + <a href="{% url doc_ballot_approvaltext name=doc.name %}">Back</a> {% ifequal action "to_announcement_list" %} <input type="submit" value="Send out the announcement and close ballot"/> {% endifequal %} diff --git a/ietf/templates/idrfc/ballot_approvaltext.html b/ietf/templates/idrfc/ballot_approvaltext.html index d6b4ecadc..f58ae42a6 100644 --- a/ietf/templates/idrfc/ballot_approvaltext.html +++ b/ietf/templates/idrfc/ballot_approvaltext.html @@ -19,7 +19,7 @@ form #id_approval_text { {{ approval_text_form.approval_text }} <div class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back</a> + <a href="{{ back_url }}">Back</a> <input type="submit" name="save_approval_text" value="Save Approval Announcement Text" /> <input type="submit" name="regenerate_approval_text" value="Regenerate Approval Announcement Text" /> </div> @@ -29,7 +29,7 @@ form #id_approval_text { {% if user|in_group:"Secretariat" %} <p> {% if can_announce %} -<a href="{% url doc_approve_ballot name=doc.filename %}">Approve ballot</a> +<a href="{% url doc_approve_ballot name=doc.name %}">Approve ballot</a> {% endif %} </p> {% endif %} diff --git a/ietf/templates/idrfc/ballot_comment_mail.txt b/ietf/templates/idrfc/ballot_comment_mail.txt index 011d4a8aa..cd442df96 100644 --- a/ietf/templates/idrfc/ballot_comment_mail.txt +++ b/ietf/templates/idrfc/ballot_comment_mail.txt @@ -16,13 +16,13 @@ There is no DISCUSS or COMMENT text associated with this position. DISCUSS: ---------------------------------------------------------------------- -{{ discuss|safe }} +{{ discuss|safe|wordwrap:73 }} {% endif %}{% if comment %}---------------------------------------------------------------------- COMMENT: ---------------------------------------------------------------------- -{{ comment|safe }} +{{ comment|safe|wordwrap:73 }} {% endif %} {% endautoescape %} diff --git a/ietf/templates/idrfc/ballot_issued.html b/ietf/templates/idrfc/ballot_issued.html index 0c62d5f7f..45a8464e3 100644 --- a/ietf/templates/idrfc/ballot_issued.html +++ b/ietf/templates/idrfc/ballot_issued.html @@ -8,6 +8,6 @@ <p>Ballot has been sent out.</p> <div class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back to document</a> + <a href="{{ back_url }}">Back to document</a> </div> {% endblock %} diff --git a/ietf/templates/idrfc/ballot_lastcalltext.html b/ietf/templates/idrfc/ballot_lastcalltext.html index 62861bc14..135af9b64 100644 --- a/ietf/templates/idrfc/ballot_lastcalltext.html +++ b/ietf/templates/idrfc/ballot_lastcalltext.html @@ -25,7 +25,7 @@ form #id_last_call_text { {% endif %} <div class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back</a> + <a href="{{ back_url }}">Back</a> <input type="submit" name="save_last_call_text" value="Save Last Call Text" /> <input type="submit" name="regenerate_last_call_text" value="Regenerate Last Call Text" /> {% if can_request_last_call and not need_intended_status %} @@ -38,7 +38,7 @@ form #id_last_call_text { {% if user|in_group:"Secretariat" %} <p> {% if can_make_last_call %} -<a href="{% url doc_make_last_call name=doc.filename %}">Make Last Call</a> +<a href="{% url doc_make_last_call name=doc.name %}">Make Last Call</a> {% endif %} </p> diff --git a/ietf/templates/idrfc/ballot_writeupnotesREDESIGN.html b/ietf/templates/idrfc/ballot_writeupnotesREDESIGN.html new file mode 100644 index 000000000..3cab31e74 --- /dev/null +++ b/ietf/templates/idrfc/ballot_writeupnotesREDESIGN.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}Ballot writeup and notes for {{ doc }}{% endblock %} + +{% block morecss %} +form #id_ballot_writeup { + width: 700px; + height: 600px; +} +{% endblock %} + +{% block content %} +<h1>Ballot writeup and notes for {{ doc }}</h1> + + +<form action="" method="POST"> + + <p>(Technical Summary, Working Group Summary, Document Quality, + Personnel, RFC Editor Note, IRTF Note, IESG Note, IANA Note)</p> + + <p>This text will be appended to all announcements and messages to + the IRTF or RFC Editor.</p> + + {{ ballot_writeup_form.ballot_writeup }} + + {% if not approval %}<p style="font-style:italic">Ballot cannot be issued before <a href="{% url doc_ballot_approvaltext name=doc.name %}">announcement text</a> is added.</p>{% endif %} + + <div class="actions"> + <a href="{{ back_url }}">Back</a> + <input type="submit" name="save_ballot_writeup" value="Save Ballot Writeup" /> + <input style="margin-left: 8px" type="submit" {% if not approval %}disabled="disabled"{% endif %} name="issue_ballot" value="Save and {% if ballot_issued %}Re-{% endif %}Issue Ballot" /> + </div> +</form> + + +{% endblock%} diff --git a/ietf/templates/idrfc/change_stateREDESIGN.html b/ietf/templates/idrfc/change_stateREDESIGN.html new file mode 100644 index 000000000..9e88a309b --- /dev/null +++ b/ietf/templates/idrfc/change_stateREDESIGN.html @@ -0,0 +1,64 @@ +{% extends "base.html" %} + +{% block title %}Change state of {{ doc }}{% endblock %} + +{% block morecss %} +form.change-state select { + width: 22em; +} +form.change-state .actions { + text-align: right; + padding-top: 10px; +} +.next-states, +.prev-state { + margin-bottom: 30px; +} +.next-states form, +.prev-state form { + display: inline; + margin-right: 10px; +} +{% endblock %} + +{% block content %} +<h1>Change state of {{ doc }}</h1> + +<p class="helptext">For help on the states, see the <a href="{% url help_states %}">state table</a>.</p> + +<form class="change-state" action="" method="post"> + <table> + {{ form.as_table }} + <tr> + <td colspan="2" class="actions"> + <a href="{{ doc.get_absolute_url }}">Back</a> + <input type="submit" value="Save"/> + </td> + </tr> + </table> +</form> + +{% if next_states %} +<h3>Or jump directly to</h3> + +<div class="next-states"> + {% for n in next_states %} + <form action="" method="post"> + <input type="hidden" name="state" value="{{ n.slug }}" /> + <input type="submit" value="{{ n.name }}" /> + </form> + {% endfor %} +</div> +{% endif %} + +{% if prev_state %} +<h3>Or revert to previous state</h3> + +<div class="prev-state"> + <form action="" method="post"> + <input type="hidden" name="state" value="{{ prev_state.slug }}" /> + <input type="submit" value="Back to {{ prev_state.name }}" /> + </form> +</div> +{% endif %} +{% endblock %} diff --git a/ietf/templates/idrfc/defer_ballot.html b/ietf/templates/idrfc/defer_ballot.html index f65ebcd5b..db4d65abc 100644 --- a/ietf/templates/idrfc/defer_ballot.html +++ b/ietf/templates/idrfc/defer_ballot.html @@ -11,7 +11,7 @@ <p>The ballot will then be on the IESG agenda of {{ telechat_date }}.</p> <div class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back</a> + <a href="{{ back_url }}">Back</a> <input type="submit" value="Defer ballot"/> </div> </form> diff --git a/ietf/templates/idrfc/doc_history.html b/ietf/templates/idrfc/doc_history.html index b634f0629..8ec6b72e1 100644 --- a/ietf/templates/idrfc/doc_history.html +++ b/ietf/templates/idrfc/doc_history.html @@ -65,8 +65,12 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. {{ c.info.text|fill:"80"|safe|urlize|linebreaksbr|keep_spacing|sanitize_html|safe }} </div> {% else %} +{% if c.info.dontmolest %} +{{ c.info.text|safe }} +{% else %} {{ c.info.text|fill:"80"|safe|urlize|linebreaksbr|keep_spacing|sanitize_html|safe }} {% endif %} +{% endif %} </td> {% endif %} diff --git a/ietf/templates/idrfc/edit_infoREDESIGN.html b/ietf/templates/idrfc/edit_infoREDESIGN.html new file mode 100644 index 000000000..1ade3e8df --- /dev/null +++ b/ietf/templates/idrfc/edit_infoREDESIGN.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} + +{% block title %}Edit info on {{ doc }}{% endblock %} + +{% block morecss %} +form.edit-info #id_notify { + width: 600px; +} +form.edit-info #id_note { + width: 600px; + height: 150px; +} +form.edit-info .actions { + padding-top: 20px; +} +{% endblock %} + +{% block content %} +{% load ietf_filters %} +<h1>Edit info on {{ doc }}</h1> + +<form class="edit-info" action="" method="POST"> + <table> + {% for field in form.standard_fields %} + <tr> + <th>{{ field.label_tag }}:</th> + <td>{{ field }} + {% ifequal field.name "telechat_date" %}{{ form.returning_item }} {{ form.returning_item.label_tag }} {{ form.returning_item.errors }}{% endifequal %} + {% ifequal field.name "ad" %} + {% if user|in_group:"Area_Director" %} + <label><input type="checkbox" name="ad" value="{{ login.pk }}" /> Assign to me</label> + {% endif %} + {% endifequal %} + {% if field.help_text %}<div class="help">{{ field.help_text }}</div>{% endif %} + {{ field.errors }}</td> + </tr> + {% endfor %} + <tr> + <td></td> + <td class="actions"> + <a href="{{ doc.get_absolute_url }}">Back</a> + <input type="submit" value="Save"/> + </td> + </tr> + </table> +</form> +{% endblock %} diff --git a/ietf/templates/idrfc/edit_positionREDESIGN.html b/ietf/templates/idrfc/edit_positionREDESIGN.html new file mode 100644 index 000000000..f28e8966f --- /dev/null +++ b/ietf/templates/idrfc/edit_positionREDESIGN.html @@ -0,0 +1,80 @@ +{% extends "base.html" %} + +{% block title %}Change position for {{ ad.name }} on {{ doc }}{% endblock %} + +{% block morecss %} +div.ballot-deferred { + margin-top: 8px; + margin-bottom: 8px; +} +form.position-form .position ul { + padding: 0; + margin: 0; +} +form.position-form .position li { + list-style-type: none; + float: left; + padding-right: 10px; +} +form.position-form .last-edited { + font-style: italic; +} +form.position-form .discuss-text { + padding-top: 20px +} +form.position-form #id_discuss_text, +form.position-form #id_comment_text { + width: 700px; + height: 250px; +} +form.position-form .comment-text { + margin-top: 20px; +} +{% endblock %} + +{% block content %} +<h1>Change position for {{ ad.name }} {{ doc }}</h1> + +{% if ballot_deferred %} +<div class="ballot-deferred">Ballot deferred by {{ ballot_deferred.by }} on {{ ballot_deferred.time|date:"Y-m-d" }}.</div> +{% endif %} + +<form class="position-form" action="" method="POST"> + <div> + <span class="position">{{ form.position }}</span> + <span class="actions"> + <input type="submit" name="send_mail" value="Save and send email"/> + <input type="submit" value="Save"/> + {% if ballot_deferred %}<input type="submit" name="Undefer" value="Undefer"/>{% else %}<input type="submit" name="Defer" value="Defer"/>{% endif %} + </span> + </div> + + <div style="clear:left"></div> + + <div class="discuss-widgets" {% ifnotequal form.position.initial "discuss" %}style="display:none"{% endifnotequal %}> + <div class="discuss-text"> + {{ form.discuss.label_tag }}: + {% if old_pos and old_pos.discuss_time %}<span class="last-edited">(last edited {{ old_pos.discuss_time }})</span>{% endif %} + </div> + {{ form.discuss.errors }} + {{ form.discuss }} + </div> + + <div class="comment-text"> + {{ form.comment.label_tag }}: + {% if old_pos and old_pos.comment_time %}<span class="last-edited">(last edited {{ old_pos.comment_time }}){% endif %}</span> + </div> + {{ form.comment }} + + <div class="actions"> + <a href="{{ return_to_url }}">Back</a> + </div> + + {{ form.return_to_url }} + +</form> +{% endblock %} + +{% block content_end %} +<script type="text/javascript" src="/js/doc-edit-position.js"></script> +{% endblock %} diff --git a/ietf/templates/idrfc/expire_textREDESIGN.txt b/ietf/templates/idrfc/expire_textREDESIGN.txt new file mode 100644 index 000000000..c14c7b5f6 --- /dev/null +++ b/ietf/templates/idrfc/expire_textREDESIGN.txt @@ -0,0 +1,7 @@ +{% filter wordwrap:73 %}This Internet-Draft, {{ doc.name }}-{{ doc.rev }}.txt, has expired, and has been deleted from the Internet-Drafts directory. An Internet-Draft expires {{ expire_days }} days from the date that it is posted unless it is replaced by an updated version, or the Secretariat has been notified that the document is under official review by the IESG or has been passed to the RFC Editor for review and/or publication as an RFC. This Internet-Draft was not published as an RFC. + +Internet-Drafts are not archival documents, and copies of Internet-Drafts that have been deleted from the directory are not available. The Secretariat does not have any information regarding the future plans of the author{{ authors|pluralize}} or working group, if applicable, with respect to this deleted Internet-Draft. For more information, or to request a copy of the document, please contact the author{{ authors|pluralize}} directly.{% endfilter %} + +Draft Author{{ authors|pluralize}}: +{% for name, email in authors %}{{ name }}<{{ email }}> +{% endfor %} diff --git a/ietf/templates/idrfc/expire_warning_email.txt b/ietf/templates/idrfc/expire_warning_email.txt new file mode 100644 index 000000000..138b3c223 --- /dev/null +++ b/ietf/templates/idrfc/expire_warning_email.txt @@ -0,0 +1,7 @@ +{% load ietf_filters %}{% autoescape off %}The following draft will expire soon: + +Name: {{ doc.name|clean_whitespace }} +Title: {{ doc.title}} +State: {{ state }} +Expires: {{ expiration }} (in {{ expiration|timeuntil }}) +{% endautoescape %} diff --git a/ietf/templates/idrfc/id_expired_email.txt b/ietf/templates/idrfc/id_expired_email.txt index 5ea37273c..5eda3f0ac 100644 --- a/ietf/templates/idrfc/id_expired_email.txt +++ b/ietf/templates/idrfc/id_expired_email.txt @@ -1,5 +1,5 @@ {{ doc.file_tag|safe }} was just expired. -This draft is in the state {{ doc.idstate }} in ID Tracker. +This draft is in the state "{{ state }}" in the ID Tracker. Thanks, diff --git a/ietf/templates/idrfc/issue_ballot_mailREDESIGN.txt b/ietf/templates/idrfc/issue_ballot_mailREDESIGN.txt new file mode 100644 index 000000000..bced9e27c --- /dev/null +++ b/ietf/templates/idrfc/issue_ballot_mailREDESIGN.txt @@ -0,0 +1,40 @@ +{% autoescape off %}To: Internet Engineering Steering Group <iesg@ietf.org> +From: IESG Secretary <iesg-secretary@ietf.org> +Reply-To: IESG Secretary <iesg-secretary@ietf.org> +Subject: Evaluation: {{ doc.file_tag }} to {{ status }} + +{% filter wordwrap:73 %}Evaluation for {{ doc.file_tag }} can be found at {{ doc_url }} + +{% if last_call_expires %}Last call to expire on: {{ last_call_expires }} + +{% endif %}{% endfilter %} + Please return the full line with your position. + + Yes No-Objection Discuss Abstain +{% for fmt in active_ad_positions %}{{ fmt }} +{% endfor %}{% if inactive_ad_positions %} + +{% for fmt in inactive_ad_positions %}{{ fmt }} +{% endfor %}{% endif %} + +"Yes" or "No-Objection" positions from 2/3 of non-recused ADs, +with no "Discuss" positions, are needed for approval. + +DISCUSSES AND COMMENTS +====================== +{% filter wordwrap:79 %}{% for pos in ad_feedback %}{{ pos.ad.get_name }}: + +{% if pos.discuss %}Discuss [{{ pos.discuss_time|date:"Y-m-d" }}]: +{{ pos.discuss }} + +{% endif %}{% if pos.comment %}Comment [{{ pos.comment_time|date:"Y-m-d" }}]: +{{ pos.comment }} + +{% endif %} +{% endfor %}{% endfilter %} +---- following is a DRAFT of message to be sent AFTER approval --- +{{ approval_text }}{% if ballot_writeup %} + +{{ ballot_writeup }} +{% endif %} +{% endautoescape%} diff --git a/ietf/templates/idrfc/last_call_requested.html b/ietf/templates/idrfc/last_call_requested.html index d61884ff5..f7b3c571b 100644 --- a/ietf/templates/idrfc/last_call_requested.html +++ b/ietf/templates/idrfc/last_call_requested.html @@ -12,6 +12,6 @@ secretariat takes appropriate steps. This may take up to one business day, as it involves a person taking action.</p> <div class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back</a> + <a href="{{ url }}">Back</a> </div> {% endblock %} diff --git a/ietf/templates/idrfc/make_last_callREDESIGN.html b/ietf/templates/idrfc/make_last_callREDESIGN.html new file mode 100644 index 000000000..b81d8ad2e --- /dev/null +++ b/ietf/templates/idrfc/make_last_callREDESIGN.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block title %}Make Last Call for {{ doc.name }}{% endblock %} + +{% block morecss %} +form.approve-ballot pre { + margin: 0; + padding: 4px; + border-top: 4px solid #eee; + border-bottom: 4px solid #eee; +} +form.approve-ballot .announcement { + overflow-x: auto; + overflow-y: scroll; + width: 800px; + height: 400px; + border: 1px solid #bbb; +} +{% endblock %} + +{% block content %} +<h1>Make Last Call for {{ doc.name }}</h1> + +<p>Make last call for following draft:</p> + +<div>{{ doc.file_tag }} ({{ doc.group.acronym }}) - {{ doc.intended_std_level.name }}</div> + +<form style="margin-top:20px" action="" method="POST"> + <table> + {{ form.as_table }} + </table> + + <div class="actions"> + <a href="{{ doc.get_absolute_url }}">Back</a> + <input type="reset" value="Reset"> + <input type="submit" value="Make Last Call"/> + </div> +</form> +{% endblock %} diff --git a/ietf/templates/idrfc/request_resurrect.html b/ietf/templates/idrfc/request_resurrect.html index 78145e4c9..caad47463 100644 --- a/ietf/templates/idrfc/request_resurrect.html +++ b/ietf/templates/idrfc/request_resurrect.html @@ -12,7 +12,7 @@ I-D.</p> <div class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back</a> + <a href="{{ back_url }}">Back</a> <input type="submit" value="Request resurrect"/> </div> </form> diff --git a/ietf/templates/idrfc/resurrect.html b/ietf/templates/idrfc/resurrect.html index 1879c0b37..fb89feacb 100644 --- a/ietf/templates/idrfc/resurrect.html +++ b/ietf/templates/idrfc/resurrect.html @@ -11,7 +11,7 @@ <p>This will change the status to Active{% if doc.idinternal.resurrect_requested_by %} and email a notice to {{ doc.idinternal.resurrect_requested_by }}{% endif %}.</p> <div class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back</a> + <a href="{{ back_url }}">Back</a> <input type="submit" value="Resurrect"/> </div> </form> diff --git a/ietf/templates/idrfc/resurrect_completed_email.txt b/ietf/templates/idrfc/resurrect_completed_email.txt index c6ae1994a..c5ab652e7 100644 --- a/ietf/templates/idrfc/resurrect_completed_email.txt +++ b/ietf/templates/idrfc/resurrect_completed_email.txt @@ -1,5 +1,5 @@ -{% autoescape off %}As you requsted, the Internet Draft {{ doc.file_tag|safe }} +{% autoescape off %}As you requsted, the Internet Draft {{ doc.file_tag }} has been resurrected. -ID Tracker URL: {{ url|safe }} +ID Tracker URL: {{ url }} {% endautoescape %} diff --git a/ietf/templates/idrfc/resurrect_request_email.txt b/ietf/templates/idrfc/resurrect_request_email.txt index 8cf06c671..1f69cd0c8 100644 --- a/ietf/templates/idrfc/resurrect_request_email.txt +++ b/ietf/templates/idrfc/resurrect_request_email.txt @@ -1,4 +1,4 @@ -{% autoescape off %}I-D that is requested to be resurrected: {{ doc.file_tag|safe }} -Requested by: {{ by|safe }} -ID Tracker URL: {{ url|safe }} +{% autoescape off %}I-D that is requested to be resurrected: {{ doc.file_tag }} +Requested by: {{ by }} +ID Tracker URL: {{ url }} {% endautoescape %} diff --git a/ietf/templates/idrfc/send_ballot_commentREDESIGN.html b/ietf/templates/idrfc/send_ballot_commentREDESIGN.html new file mode 100644 index 000000000..8c15cd1e3 --- /dev/null +++ b/ietf/templates/idrfc/send_ballot_commentREDESIGN.html @@ -0,0 +1,47 @@ +{% extends "base.html" %} +{% load ietf_filters %} +{% block title %}Send ballot position email for {{ ad }}{% endblock %} + +{% block morecss %} +form.send-ballot pre { + margin: 0; + padding: 4px; + border-top: 4px solid #eee; + border-bottom: 4px solid #eee; +} +{% endblock %} + +{% block content %} +<h1>Send ballot position email for {{ ad }}</h1> + +<form class="send-ballot" action="" method="POST"> + <table> + <tr><th>From:</th> <td>{{ frm }}</td></tr> + <tr><th>To:</th> <td>{{ to }}</td></tr> + <tr> + <th>Cc:<br/> + <span class="help">separated<br/> by comma</span></th> + <td><input type="text" name="cc" value="" size="75" /><br/> + {% if doc.notify %} + <label> + <input type="checkbox" name="cc_state_change" value="1" checked="checked" /> + {{ doc.notify }} + </label> + {% endif %} + </td> + </tr> + <tr><th>Subject:</th> <td>{{ subject }}</td></tr> + <tr> + <th>Body:</th> + <td><pre>{{ body|wrap_text }}</pre></td> + </tr> + <tr> + <td></td> + <td class="actions"> + <a href="{{ back_url }}">Back</a> + <input type="submit" value="Send"/> + </td> + </tr> + </table> +</form> +{% endblock %} diff --git a/ietf/templates/idrfc/undefer_ballot.html b/ietf/templates/idrfc/undefer_ballot.html index 2b8e02482..d1c7bc038 100644 --- a/ietf/templates/idrfc/undefer_ballot.html +++ b/ietf/templates/idrfc/undefer_ballot.html @@ -11,7 +11,7 @@ <p>The ballot will then be on the IESG agenda of {{ telechat_date }}.</p> <div class="actions"> - <a href="{{ doc.idinternal.get_absolute_url }}">Back</a> + <a href="{{ back_url }}">Back</a> <input type="submit" value="Undefer ballot"/> </div> </form> diff --git a/ietf/templates/iesg/scribe_doc.html b/ietf/templates/iesg/scribe_doc.html index b395a0ab6..2945f6f9c 100644 --- a/ietf/templates/iesg/scribe_doc.html +++ b/ietf/templates/iesg/scribe_doc.html @@ -48,11 +48,20 @@ Some parts Copyright (c) 2009 The IETF Trust, all rights reserved. {% endif %}{% for ipr in doc.obj.draft.ipr.all %}{% ifequal ipr.ipr.status 1 %} <br>IPR: <a href="http://datatracker.ietf.org/ipr/{{ ipr.ipr.ipr_id }}/">{{ ipr.ipr.title|escape }}</a>{% endifequal %} {% endfor %} {% if doc.obj.ballot.active %}<br><b>Discusses/comments</b> (from <a href="http://datatracker.ietf.org/idtracker/ballot/{{ doc.obj.ballot.ballot}}/">ballot {{doc.obj.ballot.ballot }})</a>: <ol> +{% if USE_DB_REDESIGN_PROXY_CLASSES %} +{% for p in doc.obj.active_positions|dictsort:"ad.name" %}{% if p.pos %}{% ifequal p.pos.pos_id "discuss" %}<li><a href="#{{doc.obj.document.filename}}+{{p.pos.ad|slugify}}+discuss">{{ p.pos.ad.name }}: Discuss [{{ p.pos.discuss_time.date }}]</a>: + <br>... +{% endifequal %}{% if p.pos.comment %} <li><a href="#{{doc.obj.document.filename}}+{{position.ad|slugify}}+comment">{{ p.pos.ad.name }}: Comment [{{ p.pos.comment_time.date }}]</a>: + <br>... +{% endif %}{% endif %}{% endfor %} +{% else %} {% for position in doc.obj.ballot.positions.all|dictsort:"ad.last_name" %}{% ifequal position.discuss 1 %} <li><a href="#{{doc.obj.document.filename}}+{{position.ad|slugify}}+discuss">{{ position.ad }}:{% for item in doc.obj.ballot.discusses.all %}{% ifequal position.ad item.ad %} Discuss [{{ item.date }}]</a>: <br>... {% endifequal %}{% endfor %}{% endifequal %}{% for item in doc.obj.ballot.comments.all %}{% ifequal position.ad item.ad %} <li><a href="#{{doc.obj.document.filename}}+{{position.ad|slugify}}+comment">{{ position.ad }}: Comment [{{ item.date }}]</a>: <br>... -{% endifequal %}{% endfor %}{% endfor %} </ol> +{% endifequal %}{% endfor %}{% endfor %} +{% endif%} + </ol> {%endif %} <p><b>Telechat</b>: <ul> <li>... diff --git a/ietf/templates/ipr/details.html b/ietf/templates/ipr/details.html index 950a6cea8..e3e4896ca 100644 --- a/ietf/templates/ipr/details.html +++ b/ietf/templates/ipr/details.html @@ -170,7 +170,7 @@ Template for {{ section_list.disclosure_type }}" where the submitter provided <tr class="{% cycle row_parity %}"><td class="iprlabel">I-D Filenames (draft-...):</td><td class="iprdata">{{ ipr.draftlist }}</td></tr> {% else %} {% for doc in ipr.drafts.all %} - <tr class="{% cycle row_parity %}"><td class="iprlabel">Internet-Draft:</td><td class="iprdata">"{{ doc.document.title }}"<br />({{ doc.document.filename }}-{{ doc.revision }})</td></tr> + <tr class="{% cycle row_parity %}"><td class="iprlabel">Internet-Draft:</td><td class="iprdata">"{{ doc.document.title }}"<br />({{ doc.document.filename }}{% if doc.revision %}-{{ doc.revision }}{% endif %})</td></tr> {% endfor %} {% endif %} {% if ipr.other_designations %} diff --git a/ietf/templates/ipr/search_result.html b/ietf/templates/ipr/search_result.html index 9a36cffba..fa3ab3e1a 100644 --- a/ietf/templates/ipr/search_result.html +++ b/ietf/templates/ipr/search_result.html @@ -24,7 +24,7 @@ <td colspan="3"> {% block intro_prefix %}IPR that was submitted by <b><i>{{ q }}</i></b>, and{% endblock %} {% block related %} - {% if not ipr.drafts.count and not ipr.rfcs.count %} + {% if not ipr.docs %} is not related to a specific IETF contribution. {% else %} is related to diff --git a/ietf/templates/registration/profileREDESIGN.html b/ietf/templates/registration/profileREDESIGN.html new file mode 100644 index 000000000..5c6083d6c --- /dev/null +++ b/ietf/templates/registration/profileREDESIGN.html @@ -0,0 +1,31 @@ +{# Copyright The IETF Trust 2007, All Rights Reserved #} +{% extends "base.html" %} + +{% block morecss %} +table.userProfile th { + text-align: left; +} +{% endblock %} + +{% block title %}Profile for {{ user }}{% endblock %} + +{% block content %} +<h1>User information</h1> + +<table class="userProfile"> + <tr> + <th>User name:</th> + <td>{{ user.username }}</td> + </tr> + <tr> + <th>Person:</th> + <td>{{ person.name|default:"?" }}</td> + </tr> + {% for role in roles %} + <tr> + <th>{% if forloop.first %}Roles:{% endif %}</th> + <td>{{ role.name }} in {{ role.group.name }} ({{ role.group.type }})</td> + </tr> + {% endfor %} +</table> +{% endblock %} diff --git a/ietf/templates/wginfo/1wg-summary-by-acronym.txt b/ietf/templates/wginfo/1wg-summary-by-acronym.txt index a7ea79517..d1a0f46be 100644 --- a/ietf/templates/wginfo/1wg-summary-by-acronym.txt +++ b/ietf/templates/wginfo/1wg-summary-by-acronym.txt @@ -2,14 +2,9 @@ IETF Working Group Summary (By Acronym) -The following Area Abreviations are used in this document +The following Area Abbreviations are used in this document {% for area in area_list %} -{{ area }} - {{ area.area_acronym.name }}{% endfor %} +{{ area|upper }} - {{ area.area_acronym.name }}{% endfor %} {% for wg in wg_list|dictsort:"group_acronym.acronym" %}{% if wg.start_date %} {{ wg.group_acronym.name|safe }} ({{ wg }}) -- {{ wg.area.area|upper }} -{% for chair in wg.wgchair_set.all %}{% if forloop.first %} Chair{{ forloop.revcounter|pluralize:": ,s:" }} {% else %} {% endif %}{{ chair.person|safe }} <{{ chair.person.email.1 }}> -{% endfor %} WG Mail: {{ wg.email_address }} - To Join: {{ wg.email_subscribe }}{%if wg.email_keyword %} - In Body: {{ wg.email_keyword|safe }}{% endif %} - Archive: {{ wg.email_archive }} -{% endif %}{% endfor %} +{% include "wginfo/wg_summary.txt" %}{% endif %}{% endfor %} diff --git a/ietf/templates/wginfo/1wg-summary.txt b/ietf/templates/wginfo/1wg-summary.txt index ffe19f733..55d76d1e2 100644 --- a/ietf/templates/wginfo/1wg-summary.txt +++ b/ietf/templates/wginfo/1wg-summary.txt @@ -5,9 +5,4 @@ {{ ad.person }} <{{ ad.person.email.1 }}>{% endfor %} {% endif %}{% if wg.start_date %} {{ wg.group_acronym.name|safe }} ({{ wg }}) -{% for chair in wg.wgchair_set.all %}{% if forloop.first %} Chair{{ forloop.revcounter|pluralize:": ,s:" }} {% else %} {% endif %}{{ chair.person|safe }} <{{ chair.person.email.1 }}> -{% endfor %} WG Mail: {{ wg.email_address }} - To Join: {{ wg.email_subscribe }}{%if wg.email_keyword %} - In Body: {{ wg.email_keyword|safe }}{% endif %} - Archive: {{ wg.email_archive }} -{% endif %}{% endifequal %}{% endfor %}{% endfor %} +{% include "wginfo/wg_summary.txt" %}{% endif %}{% endifequal %}{% endfor %}{% endfor %} diff --git a/ietf/templates/wginfo/wg-charter.txt b/ietf/templates/wginfo/wg-charter.txt index 83ba033ca..c0d8af0d4 100644 --- a/ietf/templates/wginfo/wg-charter.txt +++ b/ietf/templates/wginfo/wg-charter.txt @@ -1,4 +1,4 @@ -{% load ietf_filters %}{{wg.group_acronym.name|safe}} ({{wg}}) +{% if USE_DB_REDESIGN_PROXY_CLASSES %}{% include "wginfo/wg-charterREDESIGN.txt" %}{% else %}{% load ietf_filters %}{{wg.group_acronym.name|safe}} ({{wg}}) {{ wg.group_acronym.name|dashify }}{{ wg.group_acronym.acronym|dashify }}--- Charter @@ -44,4 +44,4 @@ Internet-Drafts: * {{obs.action}} RFC{{obs.rfc_acted_on_id}}{% endfor %}{% for obs in rfc.obsoleted_by%} * {%ifequal obs.action 'Obsoletes'%}OBSOLETED BY{%else%}Updated by{%endifequal%} RFC{{obs.rfc_id}}{% endfor %} {%endfor%} -{%else%}No Requests for Comments{% endif %} +{%else%}No Requests for Comments{% endif %}{% endif %} diff --git a/ietf/templates/wginfo/wg-charterREDESIGN.txt b/ietf/templates/wginfo/wg-charterREDESIGN.txt new file mode 100644 index 000000000..12ab03af6 --- /dev/null +++ b/ietf/templates/wginfo/wg-charterREDESIGN.txt @@ -0,0 +1,47 @@ +{% load ietf_filters %}{{wg.name|safe}} ({{wg.acronym}}) +{{ wg.name|dashify }}{{ wg.acronym|dashify }}--- + + Charter + Last Modified: {{ wg.time.date }} + + Current Status: {{ wg.state.name }} + + Chair{{ wg.chairs|pluralize }}: +{% for chair in wg.chairs %} {{ chair.person.name|safe }} <{{chair.address}}> +{% endfor %} + {{wg.area.area.area_acronym.name}} Directors: +{% for ad in wg.area_directors %} {{ ad.person|safe }} <{{ad.person.email.1}}> +{% endfor %} + {{wg.area.area.area_acronym.name}} Advisor: + {{ wg.areadirector.person.name|safe }} <{{wg.areadirector.address}}> +{% if wg.techadvisors %} + Tech Advisor{{ wg.techadvisors|pluralize }}: +{% for techadvisor in wg.techadvisors %} {{ techadvisor.person.name|safe }} <{{techadvisor.address}}> +{% endfor %}{% endif %}{% if wg.editors %} + Editor{{ wg.editors|pluralize }}: +{% for editor in wg.editors %} {{ editor.person.name|safe }} <{{editor.person.address}}> +{% endfor %}{% endif %}{% if wg.secretaries %} + Secretar{{ wg.secretaries|pluralize:"y,ies" }}: +{% for secretary in wg.secretaries %} {{ secretary.person.name|safe }} <{{secretary.person.address}}> +{% endfor %}{% endif %} + Mailing Lists: + General Discussion: {{ wg.email_address }} + To Subscribe: {{ wg.email_subscribe }} + Archive: {{ wg.email_archive }} + +Description of Working Group: + + {{ wg.charter_text|indent|safe }} + +Goals and Milestones: +{% for milestone in wg.milestones %} {% if milestone.done %}Done {% else %}{{ milestone.expected_due_date|date:"M Y" }}{% endif %} - {{ milestone.desc|safe }} +{% endfor %} +Internet-Drafts: +{% for alias in wg.drafts %} - {{alias.document.title|safe}} [{{alias.name}}-{{alias.document.rev}}] ({{ alias.document.pages }} pages) +{% endfor %} +{% if wg.rfcs %}Requests for Comments: +{% for alias in wg.rfcs %} {{ alias.name.upper }}: {{ alias.document.title|safe}} ({{ alias.document.pages }} pages){% for r in alias.rel %} + * {{ r.action }} {{ r.target.name|upper }}{% endfor %}{% for r in alias.invrel %} + * {% ifequal r.relationsship "obs" %}{{ r.inverse_action|upper }}{% else %}{{ r.action }}{% endifequal %} {{ r.source.canonical_name|upper }}{% endfor %} +{%endfor%} +{%else%}No Requests for Comments{% endif %} diff --git a/ietf/templates/wginfo/wg-dirREDESIGN.html b/ietf/templates/wginfo/wg-dirREDESIGN.html new file mode 100644 index 000000000..3808faa61 --- /dev/null +++ b/ietf/templates/wginfo/wg-dirREDESIGN.html @@ -0,0 +1,92 @@ +{% extends "base.html" %} +{# Copyright The IETF Trust 2009, All Rights Reserved #} +{% comment %} +Portion Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com> + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the Nokia Corporation and/or its + subsidiary(-ies) nor the names of its contributors may be used + to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +{% endcomment %} + +{% block title %}Active IETF Working Groups{% endblock %} + +{% block morecss %} +.ietf-wg-table { width: 100%; max-width:50em; } +.ietf-wg-table tr { vertical-align:top; } +{% endblock morecss %} + +{% block content %} +<h1>Active IETF Working Groups</h1> + +<p>See also: <a href="http://www.ietf.org/wg/concluded/">Concluded +Working Groups (www.ietf.org)</a>, <a href="http://tools.ietf.org/wg/concluded/">Concluded Working Groups (tools.ietf.org)</a>, <a href="http://www.ietf.org/dyn/wg/charter/history/">Historic Charters</a>.</p> + +{% for area in areas %} +<h2 class="ietf-divider" id="{{ area.name|cut:" " }}">{{ area.name }}</h2> + +{% for ad in area.ads %} +{% if forloop.first %} +<p>Area Director{{ forloop.revcounter|pluralize }}:</p> +<p style="margin-left: 2em"> +{% endif %} +<a href="mailto:{{ ad.address }}">{{ ad.person.name }} <{{ ad.address }}></a>{% if not forloop.last %}<br/>{% endif %} +{% if forloop.last %} +</p> +{% endif %} +{% endfor %} + +{% for url in area.urls %} +{% if forloop.first %} +<p>Area Specific Web Page{{ forloop.revcounter|pluralize}}:</p> +<p style="margin-left: 2em"> +{% endif %} +<a href="{{url.url}}">{{ url.name }}</a>{% if not forloop.last %}<br/>{% endif %} +{% if forloop.last %} +</p> +{% endif %} +{% endfor %} + +{% for wg in area.wgs %} +{% if forloop.first %} +<p>Active Working Groups:</p> +<div style="margin-left:2em;"> +<table class="ietf-wg-table"> +{% endif %} +<tr><td width="10%;"><a href="/wg/{{ wg.acronym }}/">{{ wg.acronym }}</a></td><td width="50%">{{ wg.name }}</td> +<td width="39%">{% for chair in wg.chairs %}<a href="mailto:{{ chair.address }}">{{ chair.person.name }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}</td></tr> +{% if forloop.last %} +</table> +</div> +{% endif %} +{% empty %} +<p>No Active Working Groups</p> +{% endfor %}{# wg #} + +{% endfor %}{# area #} +{% endblock %} diff --git a/ietf/templates/wginfo/wg_charterREDESIGN.html b/ietf/templates/wginfo/wg_charterREDESIGN.html new file mode 100644 index 000000000..1fb1e2c5e --- /dev/null +++ b/ietf/templates/wginfo/wg_charterREDESIGN.html @@ -0,0 +1,137 @@ +{% extends "wginfo/wg_base.html" %} +{% comment %} +Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). +All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com> + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the Nokia Corporation and/or its + subsidiary(-ies) nor the names of its contributors may be used + to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +{% endcomment %} +{% load ietf_filters %} +{% block wg_titledetail %}Charter{% endblock %} + +{% block wg_content %} + +<div class="ietf-box ietf-wg-details"> +{% if concluded %} +<span class="ietf-concluded-warning">Note: The data for concluded WGs +is occasionally incorrect.</span> +{% endif %} +<table> +<tr> +<td colspan="2"> +<b>Personnel</b> +</td> +</tr> + +<tr valign="top"> +<td style="width:14ex;">Chair{{ wg.chairs|pluralize }}:</td> +<td> +{% for chair in wg.chairs %} +<a href="mailto:{{ chair.address }}">{{ chair.person.name }} <{{ chair.address }}></a><br/> +{% endfor %} +</td></tr> +<tr><td>Area Director:</td> +<td> +{% if not wg.ad %}?{% else %} +<a href="mailto:{{ wg.ad_email }}">{{ wg.ad.name }} <{{ wg.ad_email }}></a>{% endif %} +</td> +</tr> +{% if wg.techadvisors %} +<tr> +<td>Tech Advisor{{ wg.techadvisors|pluralize }}:</td> +<td> +{% for techadvisor in wg.techadvisors %} +<a href="mailto:{{ techadvisor.address }}">{{ techadvisor.person.name }} <{{ techadvisor.address }}></a><br/> +{% endfor %} +</td></tr> +{% endif %} +{% if wg.editors %} +<td>Editor{{ wg.editors|pluralize }}:</td> +<td> +{% for editor in wg.editors %} +<a href="mailto:{{ editor.address }}">{{ editor.person.name }} <{{ editor.address }}></a><br/> +{% endfor %} +</td></tr> +{% endif %} +{% if wg.secretaries %} +<tr><td>Secretar{{ wg.secretaries|pluralize:"y,ies" }}:</td> +<td> +{% for secretary in wg.secretaries %} +<a href="mailto:{{ secretary.address }}">{{ secretary.person.name }} <{{ secretary.address }}></a><br/> +{% endfor %} +</td></tr> +{% endif %} + +<tr> +<td colspan="2"> +<br/><b>Mailing List</b> +</td> +</tr> + +<tr><td>Address:</td><td>{{ wg.email_address|urlize }}</td></tr> +<tr><td>To Subscribe:</td><td>{{ wg.email_subscribe|urlize }}</td></tr> +<tr><td>Archive:</td><td>{{ wg.clean_email_archive|urlize }}</td></tr> + +{% if not concluded %} +<tr> +<td colspan="2"> +<br/><b>Jabber Chat</b> +</td> +</tr> + +<tr><td>Room Address:</td><td><a href="xmpp:{{wg}}@jabber.ietf.org">xmpp:{{wg}}@jabber.ietf.org</a></td></tr> +<tr><td>Logs:</td><td><a href="http://jabber.ietf.org/logs/{{wg}}/">http://jabber.ietf.org/logs/{{wg}}/</a></td></tr> +{% endif %} + +</table> +</div> + +{% if wg.additional_urls %} +<p>In addition to the charter maintained by the IETF Secretariat, there is additional information about this working group on the Web at: +{% for url in wg.additional_urls %} +<a href="{{ url.url }}">{{ url.description}}</a>{% if not forloop.last %}, {% endif %} +{% endfor %} +</p> +{% endif %} + +<h2>Description of Working Group</h2> +<p>{{ wg.charter_text|escape|format_charter|safe }}</p> + +<h2>Goals and Milestones</h2> +<table> +{% for milestone in wg.milestones %} + <tr> + <td width="80px"> + {% if milestone.done %}Done{% else %}{{ milestone.expected_due_date|date:"M Y" }}{% endif %} + </td> + <td>{{ milestone.desc|escape }} + </td></tr> +{% endfor %} +</table> +{% endblock wg_content %} diff --git a/ietf/templates/wginfo/wg_summary.txt b/ietf/templates/wginfo/wg_summary.txt new file mode 100644 index 000000000..603d9e2f8 --- /dev/null +++ b/ietf/templates/wginfo/wg_summary.txt @@ -0,0 +1,5 @@ +{% for chair in wg.wgchair_set.all %}{% if forloop.first %} Chair{{ forloop.revcounter|pluralize:": ,s:" }} {% else %} {% endif %}{{ chair.person|safe }} <{{ chair.person.email.1 }}> +{% endfor %} WG Mail: {{ wg.email_address }} + To Join: {{ wg.email_subscribe }}{%if wg.email_keyword %} + In Body: {{ wg.email_keyword|safe }}{% endif %} + Archive: {{ wg.email_archive }} diff --git a/ietf/urls.py b/ietf/urls.py index 8fbc3f470..ce66a6c8b 100644 --- a/ietf/urls.py +++ b/ietf/urls.py @@ -37,6 +37,9 @@ sitemaps = { 'nomcom-announcements': NOMCOMAnnouncementsMap, } +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + del sitemaps['drafts'] # not needed, overlaps sitemaps['idtracker'] + urlpatterns = patterns('', (r'^feed/(?P<url>.*)/$', 'django.contrib.syndication.views.feed', { 'feed_dict': feeds}), diff --git a/ietf/utils/test_data.py b/ietf/utils/test_data.py new file mode 100644 index 000000000..4e2f5fa41 --- /dev/null +++ b/ietf/utils/test_data.py @@ -0,0 +1,195 @@ +from django.contrib.auth.models import User + +from ietf.iesg.models import TelechatDates, WGAction +from ietf.ipr.models import IprDetail, IprDocAlias +from redesign.doc.models import * +from redesign.name.models import * +from redesign.group.models import * +from redesign.person.models import * + +def make_test_data(): + # groups + secretariat = Group.objects.create( + name="Secretariat", + acronym="secretariat", + state_id="active", + type_id="ietf", + parent=None) + area = Group.objects.create( + name="Far Future", + acronym="farfut", + state_id="active", + type_id="area", + parent=None) + group = Group.objects.create( + name="Martian Special Interest Group", + acronym="mars", + state_id="active", + type_id="wg", + parent=area, + ) + + # persons + + # system + system_person = Person.objects.create( + id=0, # special value + name="(System)", + ascii="(System)", + address="", + ) + + if system_person.id != 0: # work around bug in Django + Person.objects.filter(id=system_person.id).update(id=0) + system_person = Person.objects.get(id=0) + + Alias.objects.get_or_create(person=system_person, name=system_person.name) + Email.objects.get_or_create(address="", person=system_person) + + # ad + u = User.objects.create(username="ad") + ad = p = Person.objects.create( + name="Aread Irector", + ascii="Aread Irector", + user=u) + email = Email.objects.create( + address="aread@ietf.org", + person=p) + Role.objects.create( + name_id="ad", + group=area, + email=email) + + # create a bunch of ads for swarm tests + for i in range(1, 10): + u = User.objects.create(username="ad%s" % i) + p = Person.objects.create( + name="Ad No%s" % i, + ascii="Ad No%s" % i, + user=u) + email = Email.objects.create( + address="ad%s@ietf.org" % i, + person=p) + Role.objects.create( + name_id="ad" if i <= 5 else "ex-ad", + group=area, + email=email) + + # group chair + p = Person.objects.create( + name="WG Chair Man", + ascii="WG Chair Man", + ) + wgchair = Email.objects.create( + address="wgchairman@ietf.org", + person=p) + Role.objects.create( + name_id="chair", + group=group, + email=wgchair, + ) + + # secretary + u = User.objects.create(username="secretary") + p = Person.objects.create( + name="Sec Retary", + ascii="Sec Retary", + user=u) + email = Email.objects.create( + address="sec.retary@ietf.org", + person=p) + Role.objects.create( + name_id="secr", + group=secretariat, + email=email, + ) + + # draft + draft = Document.objects.create( + name="draft-ietf-test", + time=datetime.datetime.now(), + type_id="draft", + title="Optimizing Martian Network Topologies", + state_id="active", + iesg_state_id="pub-req", + stream_id="ietf", + group=group, + abstract="Techniques for achieving near-optimal Martian networks.", + rev="01", + pages=2, + intended_std_level_id="ps", + ad=ad, + notify="aliens@example.mars", + note="", + ) + + doc_alias = DocAlias.objects.create( + document=draft, + name=draft.name, + ) + + DocumentAuthor.objects.create( + document=draft, + author=Email.objects.get(address="aread@ietf.org"), + order=1 + ) + + # draft has only one event + DocEvent.objects.create( + type="started_iesg_process", + by=ad, + doc=draft, + desc="Added draft", + ) + + # IPR + ipr = IprDetail.objects.create( + title="Statement regarding rights", + legal_name="Native Martians United", + is_pending=0, + applies_to_all=1, + licensing_option=1, + lic_opt_a_sub=2, + lic_opt_b_sub=2, + lic_opt_c_sub=2, + comments="", + lic_checkbox=True, + other_notes="", + status=1, + submitted_date=datetime.date.today(), + ) + + IprDocAlias.objects.create( + ipr=ipr, + doc_alias=doc_alias, + rev="00", + ) + + # telechat dates + t = datetime.date.today() + dates = TelechatDates(date1=t, + date2=t + datetime.timedelta(days=7), + date3=t + datetime.timedelta(days=14), + date4=t + datetime.timedelta(days=21), + ) + super(dates.__class__, dates).save(force_insert=True) # work-around hard-coded save block + + # WG Actions + group = Group.objects.create( + name="Asteroid Mining Equipment Standardization Group", + acronym="ames", + state_id="proposed", + type_id="wg", + parent=area, + ) + WGAction.objects.create( + pk=group.pk, + note="", + status_date=datetime.date.today(), + agenda=1, + token_name="Aread", + category=13, + telechat_date=dates.date2 + ) + + return draft diff --git a/ietf/utils/test_utils.py b/ietf/utils/test_utils.py index 3e70a531b..b77ee9637 100644 --- a/ietf/utils/test_utils.py +++ b/ietf/utils/test_utils.py @@ -38,7 +38,7 @@ import django from django.db import connection from django.test import TestCase from django.test.client import Client -import ietf +import ietf.settings from django.conf import settings from datetime import datetime import urllib2 as urllib @@ -117,7 +117,10 @@ class SimpleUrlTestCase(TestCase,RealDatabaseTest): self.tearDownRealDatabase() def doTestUrls(self, test_filename): - filename = os.path.dirname(os.path.abspath(test_filename))+"/testurl.list" + if test_filename.endswith(".list"): + filename = test_filename + else: + filename = os.path.dirname(os.path.abspath(test_filename))+"/testurl.list" print " Reading "+filename tuples = read_testurls(filename) failures = 0 diff --git a/ietf/wgchairs/forms.py b/ietf/wgchairs/forms.py index cc1490377..9373a0873 100644 --- a/ietf/wgchairs/forms.py +++ b/ietf/wgchairs/forms.py @@ -10,6 +10,7 @@ from ietf.wgchairs.accounts import get_person_for_user from ietf.ietfworkflows.constants import REQUIRED_STATES from ietf.ietfworkflows.utils import (get_default_workflow_for_wg, get_workflow_for_wg, update_tags, FOLLOWUP_TAG, get_state_by_name) +from ietf.ietfworkflows.models import AnnotationTag, State from ietf.idtracker.models import PersonOrOrgInfo from workflows.models import Transition @@ -36,7 +37,7 @@ class RelatedWGForm(forms.Form): class TagForm(RelatedWGForm): - tags = forms.ModelMultipleChoiceField(get_default_workflow_for_wg().annotation_tags.all(), + tags = forms.ModelMultipleChoiceField(AnnotationTag.objects.filter(wgworkflow__name='Default WG Workflow'), widget=forms.CheckboxSelectMultiple, required=False) def save(self): @@ -49,7 +50,7 @@ class TagForm(RelatedWGForm): class StateForm(RelatedWGForm): - states = forms.ModelMultipleChoiceField(get_default_workflow_for_wg().states.all(), + states = forms.ModelMultipleChoiceField(State.objects.filter(wgworkflow__name='Default WG Workflow'), widget=forms.CheckboxSelectMultiple, required=False) def update_transitions(self, workflow): @@ -94,7 +95,7 @@ class DeleteTransitionForm(RelatedWGForm): class TransitionForm(forms.ModelForm): - states = forms.ModelMultipleChoiceField(get_default_workflow_for_wg().states.all()) + states = forms.ModelMultipleChoiceField(State.objects.filter(wgworkflow__name='Default WG Workflow')) class Meta: model = Transition diff --git a/ietf/wginfo/views.py b/ietf/wginfo/views.py index 47c491564..6d893fd7b 100644 --- a/ietf/wginfo/views.py +++ b/ietf/wginfo/views.py @@ -32,15 +32,39 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from ietf.idtracker.models import Area, IETFWG from django.shortcuts import get_object_or_404, render_to_response from django.template import RequestContext, loader from django.http import HttpResponse +from django.conf import settings +from ietf.idtracker.models import Area, IETFWG from ietf.idrfc.views_search import SearchForm, search_query from ietf.idrfc.idrfc_wrapper import IdRfcWrapper from ietf.ipr.models import IprDetail +def fill_in_charter_info(wg, include_drafts=False): + from redesign.person.models import Email + from redesign.doc.models import DocAlias, RelatedDocument + + wg.areadirector = wg.ad.role_email("ad", wg.parent) if wg.ad else None + wg.chairs = Email.objects.filter(role__group=wg, role__name="chair") + wg.techadvisors = Email.objects.filter(role__group=wg, role__name="techadv") + wg.editors = Email.objects.filter(role__group=wg, role__name="editor") + wg.secretaries = Email.objects.filter(role__group=wg, role__name="secr") + wg.milestones = wg.groupmilestone_set.all().order_by('expected_due_date') + + if include_drafts: + aliases = DocAlias.objects.filter(document__type="draft", document__group=wg).select_related('document').order_by("name") + wg.drafts = [] + wg.rfcs = [] + for a in aliases: + if a.name.startswith("draft"): + wg.drafts.append(a) + else: + wg.rfcs.append(a) + a.rel = RelatedDocument.objects.filter(source=a.document).distinct() + a.invrel = RelatedDocument.objects.filter(target=a).distinct() + def wg_summary_acronym(request): areas = Area.active_areas() wgs = IETFWG.objects.filter(status=IETFWG.ACTIVE) @@ -52,16 +76,39 @@ def wg_summary_area(request): def wg_charters(request): wgs = IETFWG.objects.filter(status='1',start_date__isnull=False) - return HttpResponse(loader.render_to_string('wginfo/1wg-charters.txt', {'wg_list': wgs}),mimetype='text/plain; charset=UTF-8') + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + for wg in wgs: + fill_in_charter_info(wg, include_drafts=True) + return HttpResponse(loader.render_to_string('wginfo/1wg-charters.txt', {'wg_list': wgs, 'USE_DB_REDESIGN_PROXY_CLASSES': settings.USE_DB_REDESIGN_PROXY_CLASSES}),mimetype='text/plain; charset=UTF-8') def wg_charters_by_acronym(request): wgs = IETFWG.objects.filter(status='1',start_date__isnull=False) - return HttpResponse(loader.render_to_string('wginfo/1wg-charters-by-acronym.txt', {'wg_list': wgs}),mimetype='text/plain; charset=UTF-8') + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + for wg in wgs: + fill_in_charter_info(wg, include_drafts=True) + return HttpResponse(loader.render_to_string('wginfo/1wg-charters-by-acronym.txt', {'wg_list': wgs, 'USE_DB_REDESIGN_PROXY_CLASSES': settings.USE_DB_REDESIGN_PROXY_CLASSES}),mimetype='text/plain; charset=UTF-8') def wg_dir(request): areas = Area.active_areas() return render_to_response('wginfo/wg-dir.html', {'areas':areas}, RequestContext(request)) +def wg_dirREDESIGN(request): + from redesign.group.models import Group, GroupURL + from redesign.person.models import Email + + areas = Group.objects.filter(type="area", state="active").order_by("name") + for area in areas: + area.ads = sorted(Email.objects.filter(role__group=area, role__name="ad").select_related("person"), key=lambda e: e.person.name_parts()[3]) + area.wgs = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym") + area.urls = area.groupurl_set.all().order_by("name") + for wg in area.wgs: + wg.chairs = sorted(Email.objects.filter(role__group=wg, role__name="chair").select_related("person"), key=lambda e: e.person.name_parts()[3]) + + return render_to_response('wginfo/wg-dirREDESIGN.html', {'areas':areas}, RequestContext(request)) + +if settings.USE_DB_REDESIGN_PROXY_CLASSES: + wg_dir = wg_dirREDESIGN + def wg_documents(request, acronym): wg = get_object_or_404(IETFWG, group_acronym__acronym=acronym, group_type=1) concluded = (wg.status_id != 1) @@ -96,4 +143,13 @@ def wg_documents_html(request, acronym): def wg_charter(request, acronym): wg = get_object_or_404(IETFWG, group_acronym__acronym=acronym, group_type=1) concluded = (wg.status_id != 1) + + if settings.USE_DB_REDESIGN_PROXY_CLASSES: + fill_in_charter_info(wg) + return render_to_response('wginfo/wg_charterREDESIGN.html', + dict(wg=wg, + concluded=concluded, + selected='charter'), + RequestContext(request)) + return render_to_response('wginfo/wg_charter.html', {'wg': wg, 'concluded':concluded, 'selected':'charter'}, RequestContext(request)) diff --git a/permissions/models.py b/permissions/models.py index f012f9f25..478fbdf9d 100644 --- a/permissions/models.py +++ b/permissions/models.py @@ -1,6 +1,3 @@ -# python imports -import sets - # django imports from django.db import models from django.contrib.auth.models import User diff --git a/redesign/__init__.py b/redesign/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/redesign/doc/__init__.py b/redesign/doc/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/redesign/doc/admin.py b/redesign/doc/admin.py new file mode 100644 index 000000000..d662bbb2a --- /dev/null +++ b/redesign/doc/admin.py @@ -0,0 +1,47 @@ +from django.contrib import admin +from models import * +from person.models import * + +class DocumentAdmin(admin.ModelAdmin): + list_display = ['name', 'rev', 'state', 'group', 'pages', 'intended_std_level', 'author_list', 'time'] + search_fields = ['name'] + raw_id_fields = ['authors', 'related', 'group', 'shepherd', 'ad'] +admin.site.register(Document, DocumentAdmin) + +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', 'related', 'group', 'shepherd', 'ad'] +admin.site.register(DocHistory, DocHistoryAdmin) + +class DocAliasAdmin(admin.ModelAdmin): + list_display = [ 'name', 'document_link', ] + search_fields = [ 'name', 'document__name', ] + raw_id_fields = ['document'] +admin.site.register(DocAlias, DocAliasAdmin) + + +# events + +class DocEventAdmin(admin.ModelAdmin): + list_display = ["doc", "type", "by_raw", "time"] + raw_id_fields = ["doc", "by"] + + def by_raw(self, instance): + return instance.by_id + by_raw.short_description = "By" + +admin.site.register(DocEvent, DocEventAdmin) + +admin.site.register(NewRevisionDocEvent, DocEventAdmin) +admin.site.register(WriteupDocEvent, DocEventAdmin) +admin.site.register(StatusDateDocEvent, DocEventAdmin) +admin.site.register(LastCallDocEvent, DocEventAdmin) +admin.site.register(TelechatDocEvent, DocEventAdmin) + +class BallotPositionDocEventAdmin(DocEventAdmin): + raw_id_fields = ["doc", "by", "ad"] + +admin.site.register(BallotPositionDocEvent, BallotPositionDocEventAdmin) + diff --git a/redesign/doc/models.py b/redesign/doc/models.py new file mode 100644 index 000000000..2f3d8cc84 --- /dev/null +++ b/redesign/doc/models.py @@ -0,0 +1,264 @@ +# Copyright The IETF Trust 2007, All Rights Reserved + +from django.db import models +from django.core.urlresolvers import reverse as urlreverse + +from redesign.group.models import * +from redesign.name.models import * +from redesign.person.models import Email, Person +from redesign.util import admin_link + +import datetime + +class DocumentInfo(models.Model): + """Any kind of document. Draft, RFC, Charter, IPR Statement, Liaison Statement""" + time = models.DateTimeField(default=datetime.datetime.now) # should probably have auto_now=True + # Document related + type = models.ForeignKey(DocTypeName, blank=True, null=True) # Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, Review, Issue, Wiki, External ... + title = models.CharField(max_length=255) + # State + state = models.ForeignKey(DocStateName, blank=True, null=True) # Active/Expired/RFC/Replaced/Withdrawn + tags = models.ManyToManyField(DocInfoTagName, blank=True, null=True) # Revised ID Needed, ExternalParty, AD Followup, ... + stream = models.ForeignKey(DocStreamName, blank=True, null=True) # IETF, IAB, IRTF, Independent Submission + group = models.ForeignKey(Group, blank=True, null=True) # WG, RG, IAB, IESG, Edu, Tools + wg_state = models.ForeignKey(WgDocStateName, verbose_name="WG state", blank=True, null=True) # Not/Candidate/Active/Parked/LastCall/WriteUp/Submitted/Dead + iesg_state = models.ForeignKey(IesgDocStateName, verbose_name="IESG state", blank=True, null=True) # + iana_state = models.ForeignKey(IanaDocStateName, verbose_name="IANA state", blank=True, null=True) + rfc_state = models.ForeignKey(RfcDocStateName, verbose_name="RFC state", blank=True, null=True) + # Other + abstract = models.TextField() + rev = models.CharField(verbose_name="revision", max_length=16, blank=True) + pages = models.IntegerField(blank=True, null=True) + intended_std_level = models.ForeignKey(IntendedStdLevelName, blank=True, null=True) + std_level = models.ForeignKey(StdLevelName, blank=True, null=True) + ad = models.ForeignKey(Person, verbose_name="area director", related_name='ad_%(class)s_set', blank=True, null=True) + shepherd = models.ForeignKey(Person, related_name='shepherd_%(class)s_set', blank=True, null=True) + notify = models.CharField(max_length=255, blank=True) + external_url = models.URLField(blank=True) # Should be set for documents with type 'External'. + note = models.TextField(blank=True) + internal_comments = models.TextField(blank=True) + + class Meta: + abstract = True + def author_list(self): + return ", ".join(email.address for email in self.authors.all()) + +class RelatedDocument(models.Model): + source = models.ForeignKey('Document') + target = models.ForeignKey('DocAlias') + relationship = models.ForeignKey(DocRelationshipName) + def action(self): + return self.relationship.name + def inverse_action(): + infinitive = self.relationship.name[:-1] + return u"%sd by" % infinitive + def __unicode__(self): + return u"%s %s %s" % (self.source.name, self.relationship.name.lower(), self.target.name) + +class DocumentAuthor(models.Model): + document = models.ForeignKey('Document') + author = models.ForeignKey(Email, help_text="Email address 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) + + class Meta: + ordering = ["document", "order"] + +class Document(DocumentInfo): + name = models.CharField(max_length=255, primary_key=True) # immutable + related = models.ManyToManyField('DocAlias', through=RelatedDocument, blank=True, related_name="reversely_related_document_set") + authors = models.ManyToManyField(Email, through=DocumentAuthor, blank=True) + + def __unicode__(self): + return self.name + + def get_absolute_url(self): + name = self.name + if self.state == "rfc": + aliases = self.docalias_set.filter(name__startswith="rfc") + if aliases: + name = aliases[0].name + return urlreverse('doc_view', kwargs={ 'name': name }) + + def file_tag(self): + return u"<%s>" % self.filename_with_rev() + + def filename_with_rev(self): + # FIXME: compensate for tombstones? + return u"%s-%s.txt" % (self.name, self.rev) + + def latest_event(self, *args, **filter_args): + """Get latest event of optional Python type and with filter + arguments, e.g. d.latest_event(type="xyz") returns an DocEvent + while d.latest_event(WriteupDocEvent, type="xyz") returns a + WriteupDocEvent event.""" + model = args[0] if args else DocEvent + e = model.objects.filter(doc=self).filter(**filter_args).order_by('-time', '-id')[:1] + return e[0] if e else None + + def canonical_name(self): + name = self.name + if self.type_id == "draft" and self.state_id == "rfc": + a = self.docalias_set.filter(name__startswith="rfc") + if a: + name = a[0].name + return name + + +class RelatedDocHistory(models.Model): + source = models.ForeignKey('DocHistory') + target = models.ForeignKey('DocAlias', related_name="reversely_related_document_history_set") + relationship = models.ForeignKey(DocRelationshipName) + 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() + + def __unicode__(self): + return u"%s %s (%s)" % (self.document.doc.name, self.author.get_name(), self.order) + + class Meta: + ordering = ["document", "order"] + +class DocHistory(DocumentInfo): + doc = models.ForeignKey(Document) # ID of the Document this relates to + # Django 1.2 won't let us define these in the base class, so we have + # to repeat them + related = models.ManyToManyField('DocAlias', through=RelatedDocHistory, blank=True) + authors = models.ManyToManyField(Email, through=DocHistoryAuthor, blank=True) + def __unicode__(self): + return unicode(self.doc.name) + +def save_document_in_history(doc): + def get_model_fields_as_dict(obj): + return dict((field.name, getattr(obj, field.name)) + for field in obj._meta.fields + if field is not obj._meta.pk) + + # copy fields + fields = get_model_fields_as_dict(doc) + fields["doc"] = doc + + dochist = DocHistory(**fields) + dochist.save() + + # copy many to many + for field in doc._meta.many_to_many: + if not field.rel.through: + # just add the attributes + rel = getattr(dochist, field.name) + for item in getattr(doc, field.name).all(): + rel.add(item) + + # copy remaining tricky many to many + def transfer_fields(obj, HistModel): + mfields = get_model_fields_as_dict(item) + # map doc -> dochist + for k, v in mfields.iteritems(): + if v == doc: + mfields[k] = dochist + HistModel.objects.create(**mfields) + + for item in RelatedDocument.objects.filter(source=doc): + transfer_fields(item, RelatedDocHistory) + + for item in DocumentAuthor.objects.filter(document=doc): + transfer_fields(item, DocHistoryAuthor) + + return dochist + +class DocAlias(models.Model): + """This is used for documents that may appear under multiple names, + and in particular for RFCs, which for continuity still keep the + same immutable Document.name, in the tables, but will be referred + to by RFC number, primarily, after achieving RFC status. + """ + document = models.ForeignKey(Document) + name = models.CharField(max_length=255, db_index=True) + def __unicode__(self): + return "%s-->%s" % (self.name, self.document.name) + document_link = admin_link("document") + class Meta: + verbose_name = "document alias" + verbose_name_plural = "document aliases" + + +EVENT_TYPES = [ + # core events + ("new_revision", "Added new revision"), + ("changed_document", "Changed document metadata"), + + # misc document events + ("added_comment", "Added comment"), + ("expired_document", "Expired document"), + ("requested_resurrect", "Requested resurrect"), + ("completed_resurrect", "Completed resurrect"), + ("published_rfc", "Published RFC"), + + # IESG events + ("started_iesg_process", "Started IESG process on document"), + + ("sent_ballot_announcement", "Sent ballot announcement"), + ("changed_ballot_position", "Changed ballot position"), + + ("changed_ballot_approval_text", "Changed ballot approval text"), + ("changed_ballot_writeup_text", "Changed ballot writeup text"), + + ("changed_last_call_text", "Changed last call text"), + ("requested_last_call", "Requested last call"), + ("sent_last_call", "Sent last call"), + + ("changed_status_date", "Changed status date"), + + ("scheduled_for_telechat", "Scheduled for telechat"), + + ("iesg_approved", "IESG approved document (no problem)"), + ("iesg_disapproved", "IESG disapproved document (do not publish)"), + + ("approved_in_minute", "Approved in minute"), + ] + +class DocEvent(models.Model): + """An occurrence for a document, used for tracking who, when and what.""" + time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened") + type = models.CharField(max_length=50, choices=EVENT_TYPES) + by = models.ForeignKey(Person) + doc = models.ForeignKey('doc.Document') + desc = models.TextField() + + def __unicode__(self): + return u"%s %s at %s" % (self.by.name, self.get_type_display().lower(), self.time) + + class Meta: + ordering = ['-time', '-id'] + +class NewRevisionDocEvent(DocEvent): + rev = models.CharField(max_length=16) + +# IESG events +class BallotPositionDocEvent(DocEvent): + ad = models.ForeignKey(Person) + pos = models.ForeignKey(BallotPositionName, verbose_name="position", default="norecord") + discuss = models.TextField(help_text="Discuss text if position is discuss", blank=True) + discuss_time = models.DateTimeField(help_text="Time discuss text was written", blank=True, null=True) + comment = models.TextField(help_text="Optional comment", blank=True) + comment_time = models.DateTimeField(help_text="Time optional comment was written", blank=True, null=True) + +class WriteupDocEvent(DocEvent): + text = models.TextField(blank=True) + +class StatusDateDocEvent(DocEvent): + date = models.DateField(blank=True, null=True) + +class LastCallDocEvent(DocEvent): + expires = models.DateTimeField(blank=True, null=True) + +class TelechatDocEvent(DocEvent): + telechat_date = models.DateField(blank=True, null=True) + returning_item = models.BooleanField(default=False) + diff --git a/redesign/doc/proxy.py b/redesign/doc/proxy.py new file mode 100644 index 000000000..ca36ae527 --- /dev/null +++ b/redesign/doc/proxy.py @@ -0,0 +1,899 @@ +from redesign.doc.models import * +from redesign.person.models import Email +from redesign.proxy_utils import TranslatingManager +from redesign.name.proxy import * + +from django.conf import settings + +import glob, os + + +class InternetDraft(Document): + objects = TranslatingManager(dict(filename="name", + filename__contains="name__contains", + id_document_tag="pk", + status=lambda v: ("state", { 1: 'active', 2: 'expired', 3: 'rfc', 4: 'auth-rm', 5: 'repl', 6: 'ietf-rm'}[v]), + job_owner="ad", + rfc_number=lambda v: ("docalias__name", "rfc%s" % v), + cur_state="iesg_state__order", + idinternal__primary_flag=None, + idinternal__cur_state__state="iesg_state__name", + ), always_filter=dict(type="draft")) + + DAYS_TO_EXPIRE=185 + + # things from InternetDraft + + #id_document_tag = models.AutoField(primary_key=True) + @property + def id_document_tag(self): + return self.name # Will only work for some use cases + #title = models.CharField(max_length=255, db_column='id_document_name') # same name + #id_document_key = models.CharField(max_length=255, editable=False) + @property + def id_document_key(self): + return self.title.upper() + #group = models.ForeignKey(Acronym, db_column='group_acronym_id') + @property + def group(self): + from group.proxy import Acronym as AcronymProxy + g = super(InternetDraft, self).group + return AcronymProxy(g) if g else None + #filename = models.CharField(max_length=255, unique=True) + @property + def filename(self): + return self.name + #revision = models.CharField(max_length=2) + @property + def revision(self): + return self.rev + #revision_date = models.DateField() + @property + def revision_date(self): + if hasattr(self, "new_revision"): + e = self.new_revision + else: + e = self.latest_event(type="new_revision") + return e.time.date() if e else None + # helper function + def get_file_type_matches_from(self, glob_path): + possible_types = [".txt", ".pdf", ".xml", ".ps"] + res = [] + for m in glob.glob(glob_path): + for t in possible_types: + if m.endswith(t): + res.append(t) + return ",".join(res) + #file_type = models.CharField(max_length=20) + @property + def file_type(self): + return self.get_file_type_matches_from(os.path.join(settings.INTERNET_DRAFT_PATH, self.name + "-" + self.rev + ".*")) or ".txt" + #txt_page_count = models.IntegerField() + @property + def txt_page_count(self): + return self.pages + #local_path = models.CharField(max_length=255, blank=True) # unused + #start_date = models.DateField() + @property + def start_date(self): + e = NewRevisionDocEvent.objects.filter(doc=self).order_by("time")[:1] + return e[0].time.date() if e else None + #expiration_date = models.DateField() + @property + def expiration_date(self): + e = self.latest_event(type__in=('expired_document', 'new_revision', "completed_resurrect")) + return e.time.date() if e and e.type == "expired_document" else None + #abstract = models.TextField() # same name + #dunn_sent_date = models.DateField(null=True, blank=True) # unused + #extension_date = models.DateField(null=True, blank=True) # unused + #status = models.ForeignKey(IDStatus) + @property + def status(self): + return IDStatus().from_object(self.state) if self.state else None + + @property + def status_id(self): + return { 'active': 1, 'repl': 5, 'expired': 2, 'rfc': 3, 'auth-rm': 4, 'ietf-rm': 6 }[self.state_id] + + #intended_status = models.ForeignKey(IDIntendedStatus) + @property + def intended_status(self): + return self.intended_std_level + + #lc_sent_date = models.DateField(null=True, blank=True) + @property + def lc_sent_date(self): + e = self.latest_event(type="sent_last_call") + return e.time.date() if e else None + + #lc_changes = models.CharField(max_length=3) # used in DB, unused in Django code? + + #lc_expiration_date = models.DateField(null=True, blank=True) + @property + def lc_expiration_date(self): + e = self.latest_event(LastCallDocEvent, type="sent_last_call") + return e.expires.date() if e else None + + #b_sent_date = models.DateField(null=True, blank=True) + @property + def b_sent_date(self): + e = self.latest_event(type="sent_ballot_announcement") + return e.time.date() if e else None + + #b_discussion_date = models.DateField(null=True, blank=True) # unused + + #b_approve_date = models.DateField(null=True, blank=True) + @property + def b_approve_date(self): + e = self.latest_event(type="iesg_approved") + return e.time.date() if e else None + + #wgreturn_date = models.DateField(null=True, blank=True) # unused + + #rfc_number = models.IntegerField(null=True, blank=True, db_index=True) + @property + def rfc_number(self): + n = self.canonical_name() + return int(n[3:]) if n.startswith("rfc") else None + + #comments = models.TextField(blank=True) # unused + + #last_modified_date = models.DateField() + @property + def last_modified_date(self): + return self.time.date() + + #replaced_by = models.ForeignKey('self', db_column='replaced_by', blank=True, null=True, related_name='replaces_set') + @property + def replaced_by(self): + r = InternetDraft.objects.filter(relateddocument__target__document=self, relateddocument__relationship="replaces") + return r[0] if r else None + + @property + def replaced_by_id(self): + r = self.replaced_by + return r.id_document_tag if r else None + + #replaces = FKAsOneToOne('replaces', reverse=True) + @property + def replaces(self): + r = self.replaces_set + return r[0] if r else None + + @property + def replaces_set(self): + return InternetDraft.objects.filter(docalias__relateddocument__source=self, docalias__relateddocument__relationship="replaces") + + #review_by_rfc_editor = models.BooleanField() + @property + def review_by_rfc_editor(self): + return bool(self.tags.filter(slug='rfc-rev')) + + #expired_tombstone = models.BooleanField() + @property + def expired_tombstone(self): + return bool(self.tags.filter(slug='exp-tomb')) + + def calc_process_start_end(self): + import datetime + start, end = datetime.datetime.min, datetime.datetime.max + e = self.latest_event(type="started_iesg_process") + if e: + start = e.time + if self.state_id == "rfc" and self.name.startswith("draft") and not hasattr(self, "viewing_as_rfc"): + previous_process = self.latest_event(type="started_iesg_process", time__lt=e.time) + if previous_process: + start = previous_process.time + end = e.time + self._process_start = start + self._process_end = end + + @property + def process_start(self): + if not hasattr(self, "_process_start"): + self.calc_process_start_end() + return self._process_start + + @property + def process_end(self): + if not hasattr(self, "_process_end"): + self.calc_process_start_end() + return self._process_end + + #shepherd = BrokenForeignKey('PersonOrOrgInfo', null=True, blank=True, null_values=(0, )) # same name + + #idinternal = FKAsOneToOne('idinternal', reverse=True, query=models.Q(rfc_flag = 0)) + @property + def idinternal(self): + # since IDInternal is now merged into the document, we try to + # guess here + if hasattr(self, "changed_ballot_position"): + e = self.changed_ballot_position + else: + e = self.latest_event(type="changed_ballot_position") + return self if self.iesg_state or e else None + + # reverse relationship + @property + def authors(self): + return IDAuthor.objects.filter(document=self) + + # methods from InternetDraft + def displayname(self): + return self.name + def file_tag(self): + return "<%s>" % self.filename_with_rev() + def filename_with_rev(self): + return "%s-%s.txt" % (self.filename, self.revision_display()) + def group_acronym(self): + return super(Document, self).group.acronym + def group_ml_archive(self): + return self.group.list_archive + def idstate(self): + return self.docstate() + def revision_display(self): + r = int(self.revision) + if self.state_id != 'active' and not self.expired_tombstone: + r = max(r - 1, 0) + return "%02d" % r + def expiration(self): + e = self.latest_event(type__in=("completed_resurrect", "new_revision")) + return e.time.date() + datetime.timedelta(days=self.DAYS_TO_EXPIRE) + def can_expire(self): + # Copying the logic from expire-ids-1 without thinking + # much about it. + if self.review_by_rfc_editor: + return False + idinternal = self.idinternal + if idinternal: + cur_state_id = idinternal.cur_state_id + # 42 is "AD is Watching"; this matches what's in the + # expire-ids-1 perl script. + # A better way might be to add a column to the table + # saying whether or not a document is prevented from + # expiring. + if cur_state_id < 42: + return False + return True + + def clean_abstract(self): + # Cleaning based on what "id-abstracts-text" script does + import re + a = self.abstract + a = re.sub(" *\r\n *", "\n", a) # get rid of DOS line endings + a = re.sub(" *\r *", "\n", a) # get rid of MAC line endings + a = re.sub("(\n *){3,}", "\n\n", a) # get rid of excessive vertical whitespace + a = re.sub("\f[\n ]*[^\n]*\n", "", a) # get rid of page headers + # Get rid of 'key words' boilerplate and anything which follows it: + # (No way that is part of the abstract...) + a = re.sub("(?s)(Conventions [Uu]sed in this [Dd]ocument|Requirements [Ll]anguage)?[\n ]*The key words \"MUST\", \"MUST NOT\",.*$", "", a) + # Get rid of status/copyright boilerplate + a = re.sub("(?s)\nStatus of [tT]his Memo\n.*$", "", a) + # wrap long lines without messing up formatting of Ok paragraphs: + while re.match("([^\n]{72,}?) +", a): + a = re.sub("([^\n]{72,}?) +([^\n ]*)(\n|$)", "\\1\n\\2 ", a) + # Remove leading and trailing whitespace + a = a.strip() + return a + + + # things from IDInternal + + #draft = models.ForeignKey(InternetDraft, primary_key=True, unique=True, db_column='id_document_tag') + @property + def draft(self): + return self + + @property + def draft_id(self): + return self.name + + #rfc_flag = models.IntegerField(null=True) + @property + def rfc_flag(self): + return self.state_id == "rfc" + + #ballot = models.ForeignKey(BallotInfo, related_name='drafts', db_column="ballot_id") + @property + def ballot(self): + if not self.idinternal: + raise BallotInfo.DoesNotExist() + return self + @property + def ballot_id(self): + return self.ballot.name + + #primary_flag = models.IntegerField(blank=True, null=True) + @property + def primary_flag(self): + # left-over from multi-ballot documents which we don't really + # support anymore, just pretend we're always primary + return True + + #group_flag = models.IntegerField(blank=True, default=0) # not used anymore, contained the group acronym_id once upon a time (so it wasn't a flag) + + #token_name = models.CharField(blank=True, max_length=25) + @property + def token_name(self): + return self.ad.name + + #token_email = models.CharField(blank=True, max_length=255) + @property + def token_email(self): + return self.ad.email_address() + + #note = models.TextField(blank=True) # same name + + #status_date = models.DateField(blank=True,null=True) + @property + def status_date(self): + e = self.latest_event(StatusDateDocEvent, type="changed_status_date") + return e.date if e else None + + #email_display = models.CharField(blank=True, max_length=50) # unused + #agenda = models.IntegerField(null=True, blank=True) + @property + def agenda(self): + e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat") + return bool(e and e.telechat_date) + + #cur_state = models.ForeignKey(IDState, db_column='cur_state', related_name='docs') + @property + def cur_state(self): + return IDState().from_object(self.iesg_state) if self.iesg_state else None + + @property + def cur_state_id(self): + return self.iesg_state.order if self.iesg_state else None + + #prev_state = models.ForeignKey(IDState, db_column='prev_state', related_name='docs_prev') + @property + def prev_state(self): + ds = self.dochistory_set.exclude(iesg_state=self.iesg_state).order_by('-time')[:1] + return IDState().from_object(ds[0].iesg_state) if ds else None + + #assigned_to = models.CharField(blank=True, max_length=25) # unused + + #mark_by = models.ForeignKey(IESGLogin, db_column='mark_by', related_name='marked') + @property + def mark_by(self): + e = self.latest_event() + from person.proxy import IESGLogin as IESGLoginProxy + return IESGLoginProxy().from_object(e.by) if e else None + + # job_owner = models.ForeignKey(IESGLogin, db_column='job_owner', related_name='documents') + @property + def job_owner(self): + from person.proxy import IESGLogin as IESGLoginProxy + return IESGLoginProxy().from_object(self.ad) if self.ad else None + + #event_date = models.DateField(null=True) + @property + def event_date(self): + e = self.latest_event() + return e.time if e else None + + #area_acronym = models.ForeignKey(Area) + @property + def area_acronym(self): + from group.proxy import Area + g = super(InternetDraft, self).group # be careful with group which is proxied + if g and g.type_id != "individ": + return Area().from_object(g.parent) + elif self.ad: + # return area for AD + try: + area = Group.objects.get(role__name="ad", role__email=self.ad, state="active") + return Area().from_object(area) + except Group.DoesNotExist: + return None + else: + return None + + #cur_sub_state = BrokenForeignKey(IDSubState, related_name='docs', null=True, blank=True, null_values=(0, -1)) + @property + def cur_sub_state(self): + s = self.tags.filter(slug__in=['extpty', 'need-rev', 'ad-f-up', 'point']) + return IDSubState().from_object(s[0]) if s else None + @property + def cur_sub_state_id(self): + s = self.cur_sub_state + return s.order if s else None + + #prev_sub_state = BrokenForeignKey(IDSubState, related_name='docs_prev', null=True, blank=True, null_values=(0, -1)) + @property + def prev_sub_state(self): + ds = self.dochistory_set.all().order_by('-time')[:1] + substates = ds[0].tags.filter(slug__in=['extpty', 'need-rev', 'ad-f-up', 'point']) if ds else None + return IDSubState().from_object(substates[0]) if substates else None + @property + def prev_sub_state_id(self): + s = self.prev_sub_state + return s.order if s else None + + #returning_item = models.IntegerField(null=True, blank=True) + @property + def returning_item(self): + e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat") + return e.returning_item if e else None + + #telechat_date = models.DateField(null=True, blank=True) + @property + def telechat_date(self): + e = self.latest_event(TelechatDocEvent, type="scheduled_for_telechat") + return e.telechat_date if e else None + + #via_rfc_editor = models.IntegerField(null=True, blank=True) + @property + def via_rfc_editor(self): + return bool(self.tags.filter(slug='via-rfc')) + + #state_change_notice_to = models.CharField(blank=True, max_length=255) + @property + def state_change_notice_to(self): + return self.notify + + #dnp = models.IntegerField(null=True, blank=True) + @property + def dnp(self): + e = self.latest_event(type__in=("iesg_disapproved", "iesg_approved")) + return e != None and e.type == "iesg_disapproved" + + #dnp_date = models.DateField(null=True, blank=True) + @property + def dnp_date(self): + e = self.latest_event(type__in=("iesg_disapproved", "iesg_approved")) + return e.time.date() if e != None and e.type == "iesg_disapproved" else None + + #noproblem = models.IntegerField(null=True, blank=True) + @property + def noproblem(self): + e = self.latest_event(type__in=("iesg_disapproved", "iesg_approved")) + return e != None and e.type == "iesg_approved" + + #resurrect_requested_by = BrokenForeignKey(IESGLogin, db_column='resurrect_requested_by', related_name='docsresurrected', null=True, blank=True) + @property + def resurrect_requested_by(self): + e = self.latest_event(type__in=("requested_resurrect", "completed_resurrect")) + from person.proxy import IESGLogin as IESGLoginProxy + return IESGLoginProxy().from_object(e.by) if e and e.type == "requested_resurrect" else None + + #approved_in_minute = models.IntegerField(null=True, blank=True) + @property + def approved_in_minute(self): + return self.latest_event(type="approved_in_minute") + + + def get_absolute_url(self): + if self.rfc_flag and self.rfc_number: + return "/doc/rfc%d/" % self.rfc_number + else: + return "/doc/%s/" % self.name + + def document(self): + return self + + def comments(self): + return DocumentComment.objects.filter(doc=self).order_by('-time') + + def public_comments(self): + return self.comments() + + def ballot_set(self): + return [self] + def ballot_primary(self): + return [self] + def ballot_others(self): + return [] + def docstate(self): + if self.iesg_state: + return self.iesg_state.name + else: + return "I-D Exists" + def change_state(self, state, sub_state): + self.iesg_state = state + + + # things from BallotInfo + #active = models.BooleanField() + @property + def active(self): + # taken from BallotWrapper + return self.latest_event(type="sent_ballot_announcement") and self.iesg_state and self.iesg_state.name in ['In Last Call', 'Waiting for Writeup', 'Waiting for AD Go-Ahead', 'IESG Evaluation', 'IESG Evaluation - Defer'] and (self.state_id == "rfc" or self.state_id == "active") + + #an_sent = models.BooleanField() + @property + def an_sent(self): + return bool(self.latest_event(type="iesg_approved")) + + #an_sent_date = models.DateField(null=True, blank=True) + @property + def an_sent_date(self): + e = self.latest_event(type="iesg_approved") + return e.time if e else None + + #an_sent_by = models.ForeignKey(IESGLogin, db_column='an_sent_by', related_name='ansent', null=True) + @property + def an_sent_by(self): + e = self.latest_event(type="iesg_approved") + from person.proxy import IESGLogin as IESGLoginProxy + return IESGLoginProxy().from_object(e.by) if e else None + + #defer = models.BooleanField() + @property + def defer(self): + # we're deferred if we're in the deferred state + return self.iesg_state and self.iesg_state.name == "IESG Evaluation - Defer" + + #defer_by = models.ForeignKey(IESGLogin, db_column='defer_by', related_name='deferred', null=True) + @property + def defer_by(self): + e = self.latest_event(type="changed_document", desc__startswith="State changed to <b>IESG Evaluation - Defer</b>") + from person.proxy import IESGLogin as IESGLoginProxy + return IESGLoginProxy().from_object(e.by) if e else None + + #defer_date = models.DateField(null=True, blank=True) + @property + def defer_date(self): + e = self.latest_event(type="changed_document", desc__startswith="State changed to <b>IESG Evaluation - Defer</b>") + return e.time.date() if e else None + + #approval_text = models.TextField(blank=True) + @property + def approval_text(self): + e = self.latest_event(WriteupDocEvent, type="changed_ballot_approval_text") + return e.text if e else "" + + #last_call_text = models.TextField(blank=True) + @property + def last_call_text(self): + e = self.latest_event(WriteupDocEvent, type="changed_last_call_text") + return e.text if e else "" + + #ballot_writeup = models.TextField(blank=True) + @property + def ballot_writeup(self): + e = self.latest_event(WriteupDocEvent, type="changed_ballot_writeup_text") + return e.text if e else "" + + #ballot_issued = models.IntegerField(null=True, blank=True) + @property + def ballot_issued(self): + return bool(self.latest_event(type="sent_ballot_announcement")) + + # def remarks(self): # apparently not used + # remarks = list(self.discusses.all()) + list(self.comments.all()) + # return remarks + def active_positions(self): + """Returns a list of dicts, with AD and Position tuples""" + active_ads = Person.objects.filter(email__role__name="ad", email__role__group__state="active") + res = [] + def add(ad, pos): + from person.proxy import IESGLogin as IESGLoginProxy + res.append(dict(ad=IESGLoginProxy().from_object(ad), pos=Position().from_object(pos) if pos else None)) + + found = set() + for pos in BallotPositionDocEvent.objects.filter(doc=self, type="changed_ballot_position", ad__in=active_ads).select_related('ad').order_by("-time", "-id"): + if pos.ad not in found: + found.add(pos.ad) + add(pos.ad, pos) + + for ad in active_ads: + if ad not in found: + add(ad, None) + + res.sort(key=lambda x: x["ad"].last_name) + + return res + + def needed(self, standardsTrack=True): + """Returns text answering the question what does this document + need to pass?. The return value is only useful if the document + is currently in IESG evaluation.""" + tmp = self.active_positions() + positions = [x["pos"] for x in tmp if x["pos"]] + ads = [x["ad"] for x in tmp] + + yes = noobj = discuss = recuse = 0 + for position in positions: + p = position.pos_id + if p == "yes": + yes += 1 + if p == "noobj": + noobj += 1 + if p == "discuss": + discuss += 1 + if p == "recuse": + recuse += 1 + answer = '' + if yes < 1: + answer += "Needs a YES. " + if discuss > 0: + if discuss == 1: + answer += "Has a DISCUSS. " + else: + answer += "Has %d DISCUSSes. " % discuss + if standardsTrack: + # For standards-track, need positions from 2/3 of the + # non-recused current IESG. + needed = int((len(ads) - recuse) * 2 / 3) + else: + # Info and experimental only need one position. + needed = 1 + have = yes + noobj + discuss + if have < needed: + more = needed - have + if more == 1: + answer += "Needs %d more position. " % more + else: + answer += "Needs %d more positions. " % more + else: + answer += "Has enough positions to pass" + if discuss: + answer += " once DISCUSSes are resolved" + answer += ". " + + return answer.rstrip() + + + # things from RfcIndex + + #rfc_number = models.IntegerField(primary_key=True) # already taken care of + #title = models.CharField(max_length=250) # same name + #authors = models.CharField(max_length=250) # exists already + #rfc_published_date = models.DateField() + @property + def rfc_published_date(self): + if hasattr(self, 'published_rfc'): + e = self.published_rfc + else: + e = self.latest_event(type="published_rfc") + return e.time.date() if e else datetime.date(1990,1,1) + + #current_status = models.CharField(max_length=50,null=True) + @property + def current_status(self): + return self.std_level.name + + #updates = models.CharField(max_length=200,blank=True,null=True) + @property + def updates(self): + return ",".join("RFC%s" % n for n in sorted(d.rfc_number for d in InternetDraft.objects.filter(docalias__relateddocument__source=self, docalias__relateddocument__relationship="updates"))) + + #updated_by = models.CharField(max_length=200,blank=True,null=True) + @property + def updated_by(self): + if not hasattr(self, "updated_by_list"): + self.updated_by_list = [d.rfc_number for d in InternetDraft.objects.filter(relateddocument__target__document=self, relateddocument__relationship="updates")] + return ",".join("RFC%s" % n for n in sorted(self.updated_by_list)) + + #obsoletes = models.CharField(max_length=200,blank=True,null=True) + @property + def obsoletes(self): + return ",".join("RFC%s" % n for n in sorted(d.rfc_number for d in InternetDraft.objects.filter(docalias__relateddocument__source=self, docalias__relateddocument__relationship="obs"))) + + #obsoleted_by = models.CharField(max_length=200,blank=True,null=True) + @property + def obsoleted_by(self): + if not hasattr(self, "obsoleted_by_list"): + self.obsoleted_by_list = [d.rfc_number for d in InternetDraft.objects.filter(relateddocument__target__document=self, relateddocument__relationship="obs")] + return ",".join("RFC%s" % n for n in sorted(self.obsoleted_by_list)) + + #also = models.CharField(max_length=50,blank=True,null=True) + @property + def also(self): + aliases = self.docalias_set.filter(models.Q(name__startswith="bcp") | + models.Q(name__startswith="std") | + models.Q(name__startswith="bcp")) + return aliases[0].name.upper() if aliases else None + + #draft = models.CharField(max_length=200,null=True) # have to ignore this, it's already implemented + + #has_errata = models.BooleanField() + @property + def has_errata(self): + return bool(self.tags.filter(slug="errata")) + + #stream = models.CharField(max_length=15,blank=True,null=True) + @property + def stream(self): + return super(InternetDraft, self).stream.name + + #wg = models.CharField(max_length=15,blank=True,null=True) + @property + def wg(self): + return self.group.acronym + + #file_formats = models.CharField(max_length=20,blank=True,null=True) + @property + def file_formats(self): + return self.get_file_type_matches_from(os.path.join(settings.RFC_PATH, "rfc" + str(self.rfc_number) + ".*")).replace(".", "").replace("txt", "ascii") + + @property + def positions(self): + res = [] + found = set() + for pos in Position.objects.filter(doc=self, type="changed_ballot_position").select_related('ad').order_by("-time", "-id"): + if pos.ad not in found: + found.add(pos.ad) + res.append(pos) + + class Dummy: + def all(self): + return self.res + d = Dummy() + d.res = res + return d + + @property + def ipr(self): + from ipr.models import IprDraftProxy + return IprDraftProxy.objects.filter(doc_alias__document=self.pk) + + class Meta: + proxy = True + +IDInternal = InternetDraft +BallotInfo = InternetDraft +RfcIndex = InternetDraft +Rfc = InternetDraft + + +class IDAuthor(DocumentAuthor): + #document = models.ForeignKey(InternetDraft, db_column='id_document_tag', related_name='authors') # same name + #person = models.ForeignKey(PersonOrOrgInfo, db_column='person_or_org_tag') + @property + def person(self): + return self.author.person + + #author_order = models.IntegerField() + @property + def author_order(self): + return self.order + + def email(self): + return None if self.author.address.startswith("unknown-email") else self.author.address + + def final_author_order(self): + return self.order + + class Meta: + proxy = True + +class DocumentComment(DocEvent): + objects = TranslatingManager(dict(comment_text="desc", + date="time" + )) + + BALLOT_DISCUSS = 1 + BALLOT_COMMENT = 2 + BALLOT_CHOICES = ( + (BALLOT_DISCUSS, 'discuss'), + (BALLOT_COMMENT, 'comment'), + ) + #document = models.ForeignKey(IDInternal) + @property + def document(self): + return self.doc + #rfc_flag = models.IntegerField(null=True, blank=True) + #public_flag = models.BooleanField() #unused + #date = models.DateField(db_column='comment_date', default=datetime.date.today) + @property + def date(self): + return self.time.date() + #time = models.CharField(db_column='comment_time', max_length=20, default=lambda: datetime.datetime.now().strftime("%H:%M:%S")) + #version = models.CharField(blank=True, max_length=3) + @property + def version(self): + e = self.doc.latest_event(NewRevisionDocEvent, type="new_revision", time__lte=self.time) + return e.rev if e else "0" + #comment_text = models.TextField(blank=True) + @property + def comment_text(self): + return self.desc + #created_by = BrokenForeignKey(IESGLogin, db_column='created_by', null=True, null_values=(0, 999)) + #result_state = BrokenForeignKey(IDState, db_column='result_state', null=True, related_name="comments_leading_to_state", null_values=(0, 99)) + #origin_state = models.ForeignKey(IDState, db_column='origin_state', null=True, related_name="comments_coming_from_state") + #ballot = models.IntegerField(null=True, choices=BALLOT_CHOICES) + def get_absolute_url(self): + return "/doc/%s/" % self.doc.name + def get_author(self): + return self.by.name + def get_username(self): + return unicode(self.by) + def get_fullname(self): + return self.by.name + def datetime(self): + return self.time + def doc_id(self): + return self.doc_id + def __str__(self): + return "\"%s...\" by %s" % (self.comment_text[:20], self.get_author()) + + class Meta: + proxy = True + + +class Position(BallotPositionDocEvent): + def from_object(self, base): + for f in base._meta.fields: + if not f.name in ('discuss',): # don't overwrite properties + setattr(self, f.name, getattr(base, f.name)) + return self + + #ballot = models.ForeignKey(BallotInfo, related_name='positions') + @property + def ballot(self): + return self.doc # FIXME: doesn't emulate old interface + + # ad = models.ForeignKey(IESGLogin) # same name + #yes = models.IntegerField(db_column='yes_col') + @property + def yes(self): + return self.pos_id == "yes" + #noobj = models.IntegerField(db_column='no_col') + @property + def noobj(self): + return self.pos_id == "noobj" + #abstain = models.IntegerField() + @property + def abstain(self): + return self.pos_id == "abstain" + #approve = models.IntegerField(default=0) # unused + #discuss = models.IntegerField() + # needs special treatment because of clash with attribute on base class + def get_discuss(self): + return self.pos_id == "discuss" + def set_discuss(self, x): + pass + discuss = property(get_discuss, set_discuss) + #recuse = models.IntegerField() + @property + def recuse(self): + return self.pos_id == "recuse" + def __str__(self): + return "Position for %s on %s" % ( self.ad, self.ballot ) + def abstain_ind(self): + if self.recuse: + return 'R' + if self.abstain: + return 'X' + else: + return ' ' + def name(self): + return self.pos.name if self.pos else "No Record" + + class Meta: + proxy = True + +class DraftLikeDocAlias(DocAlias): + # this class is mostly useful for the IPR part + + def __str__(self): + return str(unicode(self)) + + def __unicode__(self): + if self.name.startswith("rfc"): + return "RFC%04d" % int(self.name[3:]) + else: + return self.name + + @property + def id_document_tag(self): + return self.name + + @property + def title(self): + return self.document.title + + @property + def filename(self): + return self.name + + @property + def ipr(self): + from ipr.models import IprDraftProxy + return IprDraftProxy.objects.filter(doc_alias=self.pk) + + class Meta: + proxy = True diff --git a/redesign/group/__init__.py b/redesign/group/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/redesign/group/admin.py b/redesign/group/admin.py new file mode 100644 index 000000000..46f324cd7 --- /dev/null +++ b/redesign/group/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from models import * + +class GroupAdmin(admin.ModelAdmin): + list_display = ["acronym", "name", "type"] + search_fields = ["name"] + ordering = ["name"] + raw_id_fields = ["charter"] + +admin.site.register(Group, GroupAdmin) +admin.site.register(GroupHistory) + +admin.site.register(Role) diff --git a/redesign/group/models.py b/redesign/group/models.py new file mode 100644 index 000000000..ee813a713 --- /dev/null +++ b/redesign/group/models.py @@ -0,0 +1,100 @@ +# Copyright The IETF Trust 2007, All Rights Reserved + +from django.db import models +from redesign.name.models import * +from redesign.person.models import Email, Person + +import datetime + +class GroupInfo(models.Model): + time = models.DateTimeField(default=datetime.datetime.now) # should probably have auto_now=True + name = models.CharField(max_length=80) + acronym = models.CharField(max_length=16, db_index=True) + state = models.ForeignKey(GroupStateName, null=True) + type = models.ForeignKey(GroupTypeName, null=True) + parent = models.ForeignKey('Group', blank=True, null=True) + iesg_state = models.ForeignKey(IesgGroupStateName, verbose_name="IESG state", blank=True, null=True) + ad = models.ForeignKey(Person, blank=True, null=True) + list_email = models.CharField(max_length=64, blank=True) + list_subscribe = models.CharField(max_length=255, blank=True) + list_archive = models.CharField(max_length=255, blank=True) + comments = models.TextField(blank=True) + def __unicode__(self): + return self.name + + class Meta: + abstract = True + +class Group(GroupInfo): + # we keep charter separate + charter = models.OneToOneField('doc.Document', related_name='chartered_group', blank=True, null=True) + + def latest_event(self, *args, **filter_args): + """Get latest group event with filter arguments, e.g. + d.latest_event(type="xyz").""" + e = GroupEvent.objects.filter(group=self).filter(**filter_args).order_by('-time', '-id')[:1] + return e[0] if e else None + +# This will record the new state and the date it occurred for any changes +# to a group. The group acronym must be unique and is the invariant used +# to select group history from this table. +class GroupHistory(GroupInfo): + group = models.ForeignKey('Group', related_name='group_history') + charter = models.ForeignKey('doc.Document', related_name='chartered_group_history_set', blank=True, null=True) + + class Meta: + verbose_name_plural="group histories" + +class GroupURL(models.Model): + group = models.ForeignKey(Group) + name = models.CharField(max_length=255) + url = models.URLField(verify_exists=False) + +class GroupMilestone(models.Model): + group = models.ForeignKey(Group) + desc = models.TextField() + expected_due_date = models.DateField() + done = models.BooleanField() + done_date = models.DateField(null=True, blank=True) + time = models.DateTimeField(auto_now=True) + def __unicode__(self): + return self.desc[:20] + "..." + class Meta: + ordering = ['expected_due_date'] + +GROUP_EVENT_CHOICES = [("proposed", "Proposed group"), + ("started", "Started group"), + ("concluded", "Concluded group"), + ] + +class GroupEvent(models.Model): + """An occurrence for a group, used for tracking who, when and what.""" + group = models.ForeignKey(Group) + time = models.DateTimeField(default=datetime.datetime.now, help_text="When the event happened") + type = models.CharField(max_length=50, choices=GROUP_EVENT_CHOICES) + by = models.ForeignKey(Person) + desc = models.TextField() + + def __unicode__(self): + return u"%s %s at %s" % (self.by.name, self.get_type_display().lower(), self.time) + + class Meta: + ordering = ['-time', 'id'] + +class Role(models.Model): + name = models.ForeignKey(RoleName) + group = models.ForeignKey(Group) + email = models.ForeignKey(Email, help_text="Email address used by person for this role") + auth = models.CharField(max_length=255, blank=True) # unused? + def __unicode__(self): + return u"%s is %s in %s" % (self.email.get_name(), self.name.name, self.group.acronym) + + +class RoleHistory(models.Model): + name = models.ForeignKey(RoleName) + group = models.ForeignKey(GroupHistory) + email = models.ForeignKey(Email, help_text="Email address used by person for this role") + auth = models.CharField(max_length=255, blank=True) # unused? + def __unicode__(self): + return u"%s is %s in %s" % (self.email.get_name(), self.name.name, self.group.acronym) + diff --git a/redesign/group/proxy.py b/redesign/group/proxy.py new file mode 100644 index 000000000..7294f482f --- /dev/null +++ b/redesign/group/proxy.py @@ -0,0 +1,199 @@ +from redesign.proxy_utils import TranslatingManager + +from models import * + +class Acronym(Group): + class LazyIndividualSubmitter(object): + def __get__(self, obj, type=None): + return Group.objects.get(acronym="none").id + + INDIVIDUAL_SUBMITTER = LazyIndividualSubmitter() + + def from_object(self, base): + for f in base._meta.fields: + setattr(self, f.name, getattr(base, f.name)) + return self + + #acronym_id = models.AutoField(primary_key=True) + @property + def acronym_id(self): + raise NotImplemented + #acronym = models.CharField(max_length=12) # same name + #name = models.CharField(max_length=100) # same name + #name_key = models.CharField(max_length=50, editable=False) + @property + def name_key(self): + return self.name.upper() + + def __str__(self): + return self.acronym + + def __unicode__(self): + return self.acronym + + class Meta: + proxy = True + +class Area(Group): + def from_object(self, base): + for f in base._meta.fields: + setattr(self, f.name, getattr(base, f.name)) + return self + + ACTIVE=1 + #area_acronym = models.OneToOneField(Acronym, primary_key=True) + @property + def area_acronym(self): + return Acronym().from_object(self) + + #start_date = models.DateField(auto_now_add=True) + #concluded_date = models.DateField(null=True, blank=True) + #status = models.ForeignKey(AreaStatus) + @property + def status_id(self): + return { "active": 1, "dormant": 2, "conclude": 3 }[self.state_id] + #comments = models.TextField(blank=True) + #last_modified_date = models.DateField(auto_now=True) + @property + def last_modified_date(self): + return self.time.date() + #extra_email_addresses = models.TextField(blank=True,null=True) + + #def additional_urls(self): + # return AreaWGURL.objects.filter(name=self.area_acronym.name) + def active_wgs(self): + return IETFWG.objects.filter(type="wg", state="active", parent=self).select_related('type', 'state', 'parent').order_by("acronym") + + @staticmethod + def active_areas(): + return Area.objects.filter(type="area", state="active").select_related('type', 'state', 'parent').order_by('acronym') + + def __str__(self): + return self.acronym + def __unicode__(self): + return self.acronym + + class Meta: + proxy = True + +def proxied_role_emails(emails): + for e in emails: + e.person.email = { 1: e } + return emails + +class IETFWG(Group): + objects = TranslatingManager(dict(group_acronym="id", + group_acronym__acronym="acronym", + email_archive__startswith="list_archive__startswith", + group_type=lambda v: ("type", { 1: "wg" }[int(v)]), + status=lambda v: ("state", { 1: "active" }[int(v)]), + areagroup__area__status=lambda v: ("parent__state", { 1: "active" }[v]), + start_date__isnull=lambda v: None if v else ("groupevent__type", "started") + ), + always_filter=dict(type__in=("wg", "individ"))) + + ACTIVE=1 + #group_acronym = models.OneToOneField(Acronym, primary_key=True, editable=False) + @property + def group_acronym(self): + return Acronym().from_object(self) + + #group_type = models.ForeignKey(WGType) + #proposed_date = models.DateField(null=True, blank=True) + #start_date = models.DateField(null=True, blank=True) + @property + def start_date(self): + e = self.latest_event(type="started") + return e.time.date() if e else None + + #dormant_date = models.DateField(null=True, blank=True) + #concluded_date = models.DateField(null=True, blank=True) + #status = models.ForeignKey(WGStatus) + @property + def status_id(self): + return { "active": 1, "dormant": 2, "conclude": 3 }[self.state_id] + #area_director = models.ForeignKey(AreaDirector, null=True) + #meeting_scheduled = models.CharField(blank=True, max_length=3) + #email_address = models.CharField(blank=True, max_length=60) + @property + def email_address(self): + return self.list_email + #email_subscribe = models.CharField(blank=True, max_length=120) + @property + def email_subscribe(self): + return self.list_subscribe + #email_keyword = models.CharField(blank=True, max_length=50) + #email_archive = models.CharField(blank=True, max_length=95) + @property + def email_archive(self): + return self.list_archive + #comments = models.TextField(blank=True) + #last_modified_date = models.DateField() + @property + def last_modified_date(self): + return self.time.date() + #meeting_scheduled_old = models.CharField(blank=True, max_length=3) + #area = FKAsOneToOne('areagroup', reverse=True) + @property + def area(self): + class AreaGroup: pass + if self.parent: + areagroup = AreaGroup() + areagroup.area = Area().from_object(self.parent) + return areagroup + else: + return None + + def __str__(self): + return self.group_acronym.acronym + + def __unicode__(self): + return self.group_acronym.acronym + + def active_drafts(self): + from redesign.doc.proxy import InternetDraft + return InternetDraft.objects.filter(group=self, state="active") + # def choices(): + # return [(wg.group_acronym_id, wg.group_acronym.acronym) for wg in IETFWG.objects.all().filter(group_type__type='WG').select_related().order_by('acronym.acronym')] + # choices = staticmethod(choices) + def area_acronym(self): + return Area().from_object(self.parent) if self.parent else None + def area_directors(self): + if not self.parent: + return None + return proxied_role_emails(sorted(Email.objects.filter(role__group=self.parent, role__name="ad"), key=lambda e: e.person.name_parts()[3])) + def chairs(self): # return a set of WGChair objects for this work group + return proxied_role_emails(sorted(Email.objects.filter(role__group=self, role__name="chair"), key=lambda e: e.person.name_parts()[3])) + # def secretaries(self): # return a set of WGSecretary objects for this group + # return WGSecretary.objects.filter(group_acronym__exact=self.group_acronym) + # def milestones(self): # return a set of GoalMilestone objects for this group + # return GoalMilestone.objects.filter(group_acronym__exact=self.group_acronym) + # def rfcs(self): # return a set of Rfc objects for this group + # return Rfc.objects.filter(group_acronym__exact=self.group_acronym) + # def drafts(self): # return a set of Rfc objects for this group + # return InternetDraft.objects.filter(group__exact=self.group_acronym) + def charter_text(self): # return string containing WG description read from file + import os + from django.conf import settings + # get file path from settings. Syntesize file name from path, acronym, and suffix + try: + filename = os.path.join(settings.IETFWG_DESCRIPTIONS_PATH, self.acronym) + ".desc.txt" + desc_file = open(filename) + desc = desc_file.read() + except BaseException: + desc = 'Error Loading Work Group Description' + return desc + + def additional_urls(self): + return self.groupurl_set.all().order_by("name") + def clean_email_archive(self): + return self.list_archive + def wgchair_set(self): + # gross hack ... + class Dummy: pass + d = Dummy() + d.all = self.chairs() + return d + + class Meta: + proxy = True diff --git a/redesign/importing/__init__.py b/redesign/importing/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/redesign/importing/import-announcements.py b/redesign/importing/import-announcements.py new file mode 100755 index 000000000..51ce26fe8 --- /dev/null +++ b/redesign/importing/import-announcements.py @@ -0,0 +1,162 @@ +#!/usr/bin/python + +import sys, os, re, datetime + +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +settings.USE_DB_REDESIGN_PROXY_CLASSES = False +settings.IMPORTING_ANNOUNCEMENTS = True + +from django.core import management +management.setup_environ(settings) + +from redesign.person.models import * +from redesign.group.models import * +from redesign.name.utils import name +from redesign.importing.utils import old_person_to_person +from ietf.announcements.models import Message, SendQueue +from ietf.announcements.models import Announcement, PersonOrOrgInfo, AnnouncedTo, AnnouncedFrom, ScheduledAnnouncement +from ietf.idtracker.models import IESGLogin + +# assumptions: +# - nomcom groups have been imported +# - persons have been imported (Announcement originators and IESGLogins) + +# imports Announcement, ScheduledAnnouncement + +system = Person.objects.get(name="(System)") + +# Announcement +for o in Announcement.objects.all().select_related('announced_to', 'announced_from').order_by('announcement_id').iterator(): + print "importing Announcement", o.pk + try: + message = Message.objects.get(id=o.announcement_id) + except Message.DoesNotExist: + message = Message(id=o.announcement_id) + + message.time = datetime.datetime.combine(o.announced_date, + datetime.time(*(int(x) for x in o.announced_time.split(":")))) + + try: + x = o.announced_by + except PersonOrOrgInfo.DoesNotExist: + message.by = system + else: + if not o.announced_by.first_name and o.announced_by.last_name == 'None': + message.by = system + else: + message.by = old_person_to_person(o.announced_by) + + message.subject = o.subject.strip() + if o.announced_from_id == 99: + message.frm = o.other_val or "" + elif o.announced_from_id == 18 and o.nomcom_chair_id != 0: + message.frm = u"%s <%s>" % o.nomcom_chair.person.email() + else: + if '<' in o.announced_from.announced_from: + message.frm = o.announced_from.announced_from + else: + message.frm = u"%s <%s>" % (o.announced_from.announced_from, o.announced_from.email) + if o.announced_to_id == 99: + message.to = o.other_val or "" + else: + try: + message.to = u"%s <%s>" % (o.announced_to.announced_to, o.announced_to.email) + except AnnouncedTo.DoesNotExist: + message.to = "" + + message.cc = o.cc or "" + for l in (o.extra or "").strip().replace("^", "\n").replace("\r", "").split("\n"): + l = l.strip() + if l.lower().startswith("bcc:"): + message.bcc = l[len("bcc:"):].strip() + elif l.lower().startswith("reply-to:"): + message.reply_to = l[len("reply-to:"):].strip() + message.body = o.text + message.save() + + message.related_groups.clear() + + if o.nomcom: + nomcom = Group.objects.filter(role__name="chair", + role__email__person__id=o.nomcom_chair.person.pk, + acronym__startswith="nomcom").exclude(acronym="nomcom").get() + + message.related_groups.add(nomcom) + + +# precompute scheduled_by's to speed up the loop a bit +scheduled_by_mapping = {} +for by in ScheduledAnnouncement.objects.all().values_list("scheduled_by", flat=True).distinct(): + logins = IESGLogin.objects.filter(login_name=by) + if logins: + l = logins[0] + person = l.person + if not person: + person = PersonOrOrgInfo.objects.get(first_name=l.first_name, last_name=l.last_name) + found = old_person_to_person(person) + else: + found = system + + print "mapping", by, "to", found + scheduled_by_mapping[by] = found + +# ScheduledAnnouncement +for o in ScheduledAnnouncement.objects.all().order_by('id').iterator(): + print "importing ScheduledAnnouncement", o.pk + try: + q = SendQueue.objects.get(id=o.id) + except SendQueue.DoesNotExist: + q = SendQueue(id=o.id) + # make sure there's no id overlap with ordinary already-imported announcements + q.message = Message(id=o.id + 4000) + + time = datetime.datetime.combine(o.scheduled_date, + datetime.time(*(int(x) for x in o.scheduled_time.split(":")))) + by = scheduled_by_mapping[o.scheduled_by] + + q.message.time = time + q.message.by = by + + q.message.subject = (o.subject or "").strip() + q.message.to = (o.to_val or "").strip() + q.message.frm = (o.from_val or "").strip() + q.message.cc = (o.cc_val or "").strip() + q.message.bcc = (o.bcc_val or "").strip() + q.message.reply_to = (o.replyto or "").strip() + q.message.body = o.body or "" + q.message.content_type = o.content_type or "" + q.message.save() + + q.time = time + q.by = by + + d = None + if o.to_be_sent_date: + try: + t = datetime.time(*(int(x) for x in o.to_be_sent_time.split(":"))) + except ValueError: + t = datetime.time(0, 0, 0) + d = datetime.datetime.combine(o.to_be_sent_date, t) + + q.send_at = d + + d = None + if o.actual_sent_date: + try: + t = datetime.time(*(int(x) for x in o.scheduled_time.split(":"))) + except ValueError: + t = datetime.time(0, 0, 0) + + d = datetime.datetime.combine(o.actual_sent_date, t) + + q.sent_at = d + + n = (o.note or "").strip() + if n.startswith("<br>"): + n = n[len("<br>"):] + q.note = n + + q.save() diff --git a/redesign/importing/import-document-state.py b/redesign/importing/import-document-state.py new file mode 100755 index 000000000..3961b69f0 --- /dev/null +++ b/redesign/importing/import-document-state.py @@ -0,0 +1,1011 @@ +#!/usr/bin/python + +import sys, os, re, datetime + +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +settings.USE_DB_REDESIGN_PROXY_CLASSES = False + +from django.core import management +management.setup_environ(settings) + +from redesign.doc.models import * +from redesign.group.models import * +from redesign.name.models import * +from redesign.importing.utils import old_person_to_person +from redesign.name.utils import name +from ietf.idtracker.models import InternetDraft, IDInternal, IESGLogin, DocumentComment, PersonOrOrgInfo, Rfc, IESGComment, IESGDiscuss, BallotInfo, Position +from ietf.idrfc.models import RfcIndex, DraftVersions +from ietf.idrfc.mirror_rfc_index import get_std_level_mapping, get_stream_mapping + +import sys + +document_name_to_import = None +if len(sys.argv) > 1: + document_name_to_import = sys.argv[1] + +# prevent memory from leaking when settings.DEBUG=True +from django.db import connection +class DummyQueries(object): + def append(self, x): + pass +connection.queries = DummyQueries() + + +# assumptions: +# - groups have been imported +# - IESG login emails/roles have been imported +# - IDAuthor emails/persons have been imported + +# Regarding history, we currently don't try to create DocumentHistory +# objects, we just import the comments as events. + +# imports InternetDraft, IDInternal, BallotInfo, Position, +# IESGComment, IESGDiscuss, DocumentComment, IDAuthor, idrfc.RfcIndex, +# idrfc.DraftVersions + +def alias_doc(name, doc): + DocAlias.objects.filter(name=name).exclude(document=doc).delete() + alias, _ = DocAlias.objects.get_or_create(name=name, document=doc) + return alias + +type_draft = name(DocTypeName, "draft", "Draft") + +stream_mapping = get_stream_mapping() + +relationship_replaces = name(DocRelationshipName, "replaces", "Replaces") +relationship_updates = name(DocRelationshipName, "updates", "Updates") +relationship_obsoletes = name(DocRelationshipName, "obs", "Obsoletes") + +intended_std_level_mapping = { + "BCP": name(IntendedStdLevelName, "bcp", "Best Current Practice"), + "Draft Standard": name(IntendedStdLevelName, "ds", name="Draft Standard"), + "Experimental": name(IntendedStdLevelName, "exp", name="Experimental"), + "Historic": name(IntendedStdLevelName, "hist", name="Historic"), + "Informational": name(IntendedStdLevelName, "inf", name="Informational"), + "Proposed Standard": name(IntendedStdLevelName, "ps", name="Proposed Standard"), + "Standard": name(IntendedStdLevelName, "std", name="Standard"), + "None": None, + "Request": None, + } + +# add aliases from rfc_intend_status +intended_std_level_mapping["Proposed"] = intended_std_level_mapping["Proposed Standard"] +intended_std_level_mapping["Draft"] = intended_std_level_mapping["Draft Standard"] + +std_level_mapping = get_std_level_mapping() + +state_mapping = { + 'Active': name(DocStateName, "active", "Active"), + 'Expired': name(DocStateName, "expired", "Expired"), + 'RFC': name(DocStateName, "rfc", "RFC"), + 'Withdrawn by Submitter': name(DocStateName, "auth-rm", "Withdrawn by Submitter"), + 'Replaced': name(DocStateName, "repl", "Replaced"), + 'Withdrawn by IETF': name(DocStateName, "ietf-rm", "Withdrawn by IETF"), + } + +iesg_state_mapping = { + 'RFC Published': name(IesgDocStateName, "pub", "RFC Published", 'The ID has been published as an RFC.', order=32), + 'Dead': name(IesgDocStateName, "dead", "Dead", 'Document is "dead" and is no longer being tracked. (E.g., it has been replaced by another document with a different name, it has been withdrawn, etc.)', order=99), + 'Approved-announcement to be sent': name(IesgDocStateName, "approved", "Approved-announcement to be sent", 'The IESG has approved the document for publication, but the Secretariat has not yet sent out on official approval message.', order=27), + 'Approved-announcement sent': name(IesgDocStateName, "ann", "Approved-announcement sent", 'The IESG has approved the document for publication, and the Secretariat has sent out the official approval message to the RFC editor.', order=30), + 'AD is watching': name(IesgDocStateName, "watching", "AD is watching", 'An AD is aware of the document and has chosen to place the document in a separate state in order to keep a closer eye on it (for whatever reason). Documents in this state are still not being actively tracked in the sense that no formal request has been made to publish or advance the document. The sole difference between this state and "I-D Exists" is that an AD has chosen to put it in a separate state, to make it easier to keep track of (for the AD\'s own reasons).', order=42), + 'IESG Evaluation': name(IesgDocStateName, "iesg-eva", "IESG Evaluation", 'The document is now (finally!) being formally reviewed by the entire IESG. Documents are discussed in email or during a bi-weekly IESG telechat. In this phase, each AD reviews the document and airs any issues they may have. Unresolvable issues are documented as "discuss" comments that can be forwarded to the authors/WG. See the description of substates for additional details about the current state of the IESG discussion.', order=20), + 'AD Evaluation': name(IesgDocStateName, "ad-eval", "AD Evaluation", 'A specific AD (e.g., the Area Advisor for the WG) has begun reviewing the document to verify that it is ready for advancement. The shepherding AD is responsible for doing any necessary review before starting an IETF Last Call or sending the document directly to the IESG as a whole.', order=11), + 'Last Call Requested': name(IesgDocStateName, "lc-req", "Last Call Requested", 'The AD has requested that the Secretariat start an IETF Last Call, but the the actual Last Call message has not been sent yet.', order=15), + 'In Last Call': name(IesgDocStateName, "lc", "In Last Call", 'The document is currently waiting for IETF Last Call to complete. Last Calls for WG documents typically last 2 weeks, those for individual submissions last 4 weeks.', order=16), + 'Publication Requested': name(IesgDocStateName, "pub-req", "Publication Requested", 'A formal request has been made to advance/publish the document, following the procedures in Section 7.5 of RFC 2418. The request could be from a WG chair, from an individual through the RFC Editor, etc. (The Secretariat (iesg-secretary@ietf.org) is copied on these requests to ensure that the request makes it into the ID tracker.) A document in this state has not (yet) been reviewed by an AD nor has any official action been taken on it yet (other than to note that its publication has been requested.', order=10), + 'RFC Ed Queue': name(IesgDocStateName, "rfcqueue", "RFC Ed Queue", 'The document is in the RFC editor Queue (as confirmed by http://www.rfc-editor.org/queue.html).', order=31), + 'IESG Evaluation - Defer': name(IesgDocStateName, "defer", "IESG Evaluation - Defer", 'During a telechat, one or more ADs requested an additional 2 weeks to review the document. A defer is designed to be an exception mechanism, and can only be invoked once, the first time the document comes up for discussion during a telechat.', order=21), + 'Waiting for Writeup': name(IesgDocStateName, "writeupw", "Waiting for Writeup", 'Before a standards-track or BCP document is formally considered by the entire IESG, the AD must write up a protocol action. The protocol action is included in the approval message that the Secretariat sends out when the document is approved for publication as an RFC.', order=18), + 'Waiting for AD Go-Ahead': name(IesgDocStateName, "goaheadw", "Waiting for AD Go-Ahead", 'As a result of the IETF Last Call, comments may need to be responded to and a revision of the ID may be needed as well. The AD is responsible for verifying that all Last Call comments have been adequately addressed and that the (possibly revised) document is in the ID directory and ready for consideration by the IESG as a whole.', order=19), + 'Expert Review': name(IesgDocStateName, "review-e", "Expert Review", 'An AD sometimes asks for an external review by an outside party as part of evaluating whether a document is ready for advancement. MIBs, for example, are reviewed by the "MIB doctors". Other types of reviews may also be requested (e.g., security, operations impact, etc.). Documents stay in this state until the review is complete and possibly until the issues raised in the review are addressed. See the "note" field for specific details on the nature of the review.', order=12), + 'DNP-waiting for AD note': name(IesgDocStateName, "nopubadw", "DNP-waiting for AD note", 'Do Not Publish: The IESG recommends against publishing the document, but the writeup explaining its reasoning has not yet been produced. DNPs apply primarily to individual submissions received through the RFC editor. See the "note" field for more details on who has the action item.', order=33), + 'DNP-announcement to be sent': name(IesgDocStateName, "nopubanw", "DNP-announcement to be sent", 'The IESG recommends against publishing the document, the writeup explaining its reasoning has been produced, but the Secretariat has not yet sent out the official "do not publish" recommendation message.', order=34), + None: None, # FIXME: consider introducing the ID-exists state + } + +ballot_position_mapping = { + 'No Objection': name(BallotPositionName, 'noobj', 'No Objection'), + 'Yes': name(BallotPositionName, 'yes', 'Yes'), + 'Abstain': name(BallotPositionName, 'abstain', 'Abstain'), + 'Discuss': name(BallotPositionName, 'discuss', 'Discuss'), + 'Recuse': name(BallotPositionName, 'recuse', 'Recuse'), + 'No Record': name(BallotPositionName, 'norecord', 'No record'), + } +ballot_position_mapping["no"] = ballot_position_mapping['No Objection'] +ballot_position_mapping["yes"] = ballot_position_mapping['Yes'] +ballot_position_mapping["discuss"] = ballot_position_mapping['Discuss'] +ballot_position_mapping["abstain"] = ballot_position_mapping['Abstain'] +ballot_position_mapping["recuse"] = ballot_position_mapping['Recuse'] +ballot_position_mapping[None] = ballot_position_mapping["No Record"] +ballot_position_mapping["Undefined"] = ballot_position_mapping["No Record"] + +substate_mapping = { + "External Party": name(DocInfoTagName, 'extpty', "External Party", 'The document is awaiting review or input from an external party (i.e, someone other than the shepherding AD, the authors, or the WG). See the "note" field for more details on who has the action.', 3), + "Revised ID Needed": name(DocInfoTagName, 'need-rev', "Revised ID Needed", 'An updated ID is needed to address the issues that have been raised.', 5), + "AD Followup": name(DocInfoTagName, 'ad-f-up', "AD Followup", """A generic substate indicating that the shepherding AD has the action item to determine appropriate next steps. In particular, the appropriate steps (and the corresponding next state or substate) depend entirely on the nature of the issues that were raised and can only be decided with active involvement of the shepherding AD. Examples include: + +- if another AD raises an issue, the shepherding AD may first iterate with the other AD to get a better understanding of the exact issue. Or, the shepherding AD may attempt to argue that the issue is not serious enough to bring to the attention of the authors/WG. + +- if a documented issue is forwarded to a WG, some further iteration may be needed before it can be determined whether a new revision is needed or whether the WG response to an issue clarifies the issue sufficiently. + +- when a new revision appears, the shepherding AD will first look at the changes to determine whether they believe all outstanding issues have been raised satisfactorily, prior to asking the ADs who raised the original issues to verify the changes.""", 2), + "Point Raised - writeup needed": name(DocInfoTagName, 'point', "Point Raised - writeup needed", 'IESG discussions on the document have raised some issues that need to be brought to the attention of the authors/WG, but those issues have not been written down yet. (It is common for discussions during a telechat to result in such situations. An AD may raise a possible issue during a telechat and only decide as a result of that discussion whether the issue is worth formally writing up and bringing to the attention of the authors/WG). A document stays in the "Point Raised - Writeup Needed" state until *ALL* IESG comments that have been raised have been documented.', 1) + } + +tag_review_by_rfc_editor = name(DocInfoTagName, 'rfc-rev', "Review by RFC Editor") +tag_via_rfc_editor = name(DocInfoTagName, 'via-rfc', "Via RFC Editor") +tag_expired_tombstone = name(DocInfoTagName, 'exp-tomb', "Expired tombstone") +tag_approved_in_minute = name(DocInfoTagName, 'app-min', "Approved in minute") +tag_has_errata = name(DocInfoTagName, 'errata', "Has errata") + +# helpers +def save_docevent(doc, event, comment): + event.time = comment.datetime() + event.by = iesg_login_to_person(comment.created_by) + event.doc = doc + if not event.desc: + event.desc = comment.comment_text # FIXME: consider unquoting here + event.save() + +def sync_tag(d, include, tag): + if include: + d.tags.add(tag) + else: + d.tags.remove(tag) + +buggy_iesg_logins_cache = {} + +system = Person.objects.get(name="(System)") + +def iesg_login_to_person(l): + if not l: + return system + else: + # there's a bunch of old weird comments made by "IESG + # Member", transform these into "System" instead + if l.id == 2: + return system + + # fix logins without the right person + if not l.person: + if l.id not in buggy_iesg_logins_cache: + logins = IESGLogin.objects.filter(first_name=l.first_name, last_name=l.last_name).exclude(id=l.id) + if logins: + buggy_iesg_logins_cache[l.id] = logins[0] + else: + persons = PersonOrOrgInfo.objects.filter(first_name=l.first_name, last_name=l.last_name) + if persons: + l.person = persons[0] + buggy_iesg_logins_cache[l.id] = l + else: + buggy_iesg_logins_cache[l.id] = None + l = buggy_iesg_logins_cache[l.id] + + if not l: + return system + + try: + return old_person_to_person(l.person) + except Email.DoesNotExist: + try: + return Person.objects.get(name="%s %s" % (l.person.first_name, l.person.last_name)) + except Person.DoesNotExist: + print "MISSING IESG LOGIN", l.person, l.person.email() + return None + +def iesg_login_is_secretary(l): + # Amy has two users, for some reason, we sometimes get the wrong one + return l.user_level == IESGLogin.SECRETARIAT_LEVEL or (l.first_name == "Amy" and l.last_name == "Vezza") + +# regexps for parsing document comments + +date_re_str = "(?P<year>[0-9][0-9][0-9][0-9])-(?P<month>[0-9][0-9]?)-(?P<day>[0-9][0-9]?)" +def date_in_match(match): + y = int(match.group('year')) + m = int(match.group('month')) + d = int(match.group('day')) + if d == 35: # borked status date + d = 25 + return datetime.date(y, m, d) + +re_telechat_agenda = re.compile(r"(Placed on|Removed from) agenda for telechat(| - %s) by" % date_re_str) +re_telechat_changed = re.compile(r"Telechat date (was|has been) changed to (<b>)?%s(</b>)? from" % date_re_str) +re_ballot_position = re.compile(r"\[Ballot Position Update\] (New position, (?P<position>.*), has been recorded (|for (?P<for>.*) )|Position (|for (?P<for2>.*) )has been changed to (?P<position2>.*) from .*)by (?P<by>.*)") +re_ballot_issued = re.compile(r"Ballot has been issued(| by)") +re_state_changed = re.compile(r"(State (has been changed|changed|Changes) to <b>(?P<to>.*)</b> from (<b>|)(?P<from>.*)(</b> by|)|Sub state has been changed to (?P<tosub>.*) from (?P<fromsub>.*))") +re_note_changed = re.compile(r"(\[Note\]: .*'.*'|Note field has been cleared)", re.DOTALL) +re_draft_added = re.compile(r"Draft [Aa]dded (by .*)?( in state (?P<state>.*))?") +re_last_call_requested = re.compile(r"Last Call was requested") +re_document_approved = re.compile(r"IESG has approved and state has been changed to") +re_document_disapproved = re.compile(r"(Do Not Publish|DNP) note has been sent to RFC Editor and state has been changed to") +re_resurrection_requested = re.compile(r"(I-D |)Resurrection was requested by") +re_completed_resurrect = re.compile(r"(This document has been resurrected|This document has been resurrected per RFC Editor's request|Resurrection was completed)") + +re_status_date_changed = re.compile(r"Status [dD]ate has been changed to (<b>)?" + date_re_str) +re_responsible_ad_changed = re.compile(r"(Responsible AD|Shepherding AD|responsible) has been changed to (<b>)?") +re_intended_status_changed = re.compile(r"Intended [sS]tatus has been changed to (<b>)?") +re_state_change_notice = re.compile(r"State Change Notice email list (have been change|has been changed) (<b>)?") +re_area_acronym_changed = re.compile(r"Area acronymn? has been changed to \w+ from \w+(<b>)?") + +re_comment_discuss_by_tag = re.compile(r" by [\w-]+ [\w-]+$") + +def import_from_idinternal(d, idinternal): + d.time = idinternal.event_date + d.iesg_state = iesg_state_mapping[idinternal.cur_state.state] + d.ad = iesg_login_to_person(idinternal.job_owner) + d.notify = idinternal.state_change_notice_to or "" + d.note = (idinternal.note or "").replace('<br>', '\n').strip().replace('\n', '<br>') + d.save() + + # extract events + last_note_change_text = "" + started_iesg_process = "" + + document_comments = DocumentComment.objects.filter(document=idinternal.draft_id).order_by('date', 'time', 'id') + for c in document_comments: + handled = False + + # telechat agenda schedulings + match = re_telechat_agenda.search(c.comment_text) or re_telechat_changed.search(c.comment_text) + if match: + e = TelechatDocEvent() + e.type = "scheduled_for_telechat" + e.telechat_date = date_in_match(match) if "Placed on" in c.comment_text else None + # can't extract this from history so we just take the latest value + e.returning_item = bool(idinternal.returning_item) + save_docevent(d, e, c) + handled = True + + # ballot issued + match = re_ballot_issued.search(c.comment_text) + if match: + e = DocEvent() + e.type = "sent_ballot_announcement" + save_docevent(d, e, c) + handled = True + + ad = iesg_login_to_person(c.created_by) + last_pos = d.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=ad) + if not last_pos and not iesg_login_is_secretary(c.created_by): + # when you issue a ballot, you also vote yes; add that vote + e = BallotPositionDocEvent() + e.type = "changed_ballot_position" + e.ad = ad + e.desc = "[Ballot Position Update] New position, Yes, has been recorded by %s" % e.ad.name + + e.pos = ballot_position_mapping["Yes"] + e.discuss = last_pos.discuss if last_pos else "" + e.discuss_time = last_pos.discuss_time if last_pos else None + e.comment = last_pos.comment if last_pos else "" + e.comment_time = last_pos.comment_time if last_pos else None + save_docevent(d, e, c) + + # ballot positions + match = re_ballot_position.search(c.comment_text) + if match: + position = ballot_position_mapping[match.group('position') or match.group('position2')] + ad_name = match.group('for') or match.group('for2') or match.group('by') # some of the old positions don't specify who it's for, in that case assume it's "by", the person who entered the position + ad_first, ad_last = ad_name.split(' ') + login = IESGLogin.objects.filter(first_name=ad_first, last_name=ad_last).order_by('user_level')[0] + if iesg_login_is_secretary(login): + # now we're in trouble, a secretariat person isn't an + # AD, instead try to find a position object that + # matches and that we haven't taken yet + positions = Position.objects.filter(ballot=idinternal.ballot) + if position.slug == "noobj": + positions = positions.filter(noobj=1) + elif position.slug == "yes": + positions = positions.filter(yes=1) + elif position.slug == "abstain": + positions = positions.filter(models.Q(abstain=1)|models.Q(abstain=2)) + elif position.slug == "recuse": + positions = positions.filter(recuse=1) + elif position.slug == "discuss": + positions = positions.filter(models.Q(discuss=1)|models.Q(discuss=2)) + assert position.slug != "norecord" + + found = False + for p in positions: + if not d.docevent_set.filter(type="changed_ballot_position", ballotposition__pos=position, ballotposition__ad=iesg_login_to_person(p.ad)): + login = p.ad + found = True + break + + if not found: + # in even more trouble, we can try and see if it + # belongs to a nearby discuss + if position.slug == "discuss": + index_c = list(document_comments).index(c) + start = c.datetime() + end = c.datetime() + datetime.timedelta(seconds=30 * 60) + for i, x in enumerate(document_comments): + if (x.ballot == DocumentComment.BALLOT_DISCUSS + and (c.datetime() <= x.datetime() <= end + or abs(index_c - i) <= 2) + and not iesg_login_is_secretary(x.created_by)): + login = x.created_by + found = True + + if not found: + print "BALLOT BY SECRETARIAT", login + + + e = BallotPositionDocEvent() + e.type = "changed_ballot_position" + e.ad = iesg_login_to_person(login) + last_pos = d.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=e.ad) + e.pos = position + e.discuss = last_pos.discuss if last_pos else "" + e.discuss_time = last_pos.discuss_time if last_pos else None + if e.pos_id == "discuss" and not e.discuss_time: + # in a few cases, we don't have the discuss + # text/time, fudge the time so it's not null + e.discuss_time = c.datetime() + e.comment = last_pos.comment if last_pos else "" + e.comment_time = last_pos.comment_time if last_pos else None + save_docevent(d, e, c) + handled = True + + # ballot discusses/comments + if c.ballot in (DocumentComment.BALLOT_DISCUSS, DocumentComment.BALLOT_COMMENT): + e = BallotPositionDocEvent() + e.type = "changed_ballot_position" + e.ad = iesg_login_to_person(c.created_by) + last_pos = d.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=e.ad) + e.pos = last_pos.pos if last_pos else ballot_position_mapping[None] + c.comment_text = re_comment_discuss_by_tag.sub("", c.comment_text) + if c.ballot == DocumentComment.BALLOT_DISCUSS: + e.discuss = c.comment_text + e.discuss_time = c.datetime() + e.comment = last_pos.comment if last_pos else "" + e.comment_time = last_pos.comment_time if last_pos else None + # put header into description + c.comment_text = "[Ballot discuss]\n" + c.comment_text + else: + e.discuss = last_pos.discuss if last_pos else "" + e.discuss_time = last_pos.discuss_time if last_pos else None + if e.pos_id == "discuss" and not e.discuss_time: + # in a few cases, we don't have the discuss + # text/time, fudge the time so it's not null + e.discuss_time = c.datetime() + e.comment = c.comment_text + e.comment_time = c.datetime() + # put header into description + c.comment_text = "[Ballot comment]\n" + c.comment_text + + # there are some bogus copies where a secretary has the + # same discuss comment as an AD, skip saving if this is + # one of those + if not (iesg_login_is_secretary(c.created_by) + and DocumentComment.objects.filter(ballot=c.ballot, document=c.document).exclude(created_by=c.created_by)): + save_docevent(d, e, c) + + handled = True + + # last call requested + match = re_last_call_requested.search(c.comment_text) + if match: + e = DocEvent(type="requested_last_call") + save_docevent(d, e, c) + handled = True + + # state changes + match = re_state_changed.search(c.comment_text) + if match: + e = DocEvent(type="changed_document") + save_docevent(d, e, c) + handled = True + + # note changed + match = re_note_changed.search(c.comment_text) + if match: + # watch out for duplicates of which the old data's got many + if c.comment_text != last_note_change_text: + last_note_change_text = c.comment_text + e = DocEvent(type="changed_document") + save_docevent(d, e, c) + handled = True + + # draft added + match = re_draft_added.search(c.comment_text) + if match: + # watch out for extraneous starts, the old data contains + # some phony ones + if not started_iesg_process: + started_iesg_process = c.comment_text + e = DocEvent(type="started_iesg_process") + save_docevent(d, e, c) + handled = True + + # new version + if c.comment_text == "New version available": + e = NewRevisionDocEvent(type="new_revision", rev=c.version) + save_docevent(d, e, c) + handled = True + + # resurrect requested + match = re_resurrection_requested.search(c.comment_text) + if match: + e = DocEvent(type="requested_resurrect") + save_docevent(d, e, c) + handled = True + + # completed resurrect + match = re_completed_resurrect.search(c.comment_text) + if match: + e = DocEvent(type="completed_resurrect") + save_docevent(d, e, c) + handled = True + + # document expiration + if c.comment_text == "Document is expired by system": + e = DocEvent(type="expired_document") + save_docevent(d, e, c) + handled = True + + # approved document + match = re_document_approved.search(c.comment_text) + if match: + e = DocEvent(type="iesg_approved") + save_docevent(d, e, c) + handled = True + + # disapproved document + match = re_document_disapproved.search(c.comment_text) + if match: + e = DocEvent(type="iesg_disapproved") + save_docevent(d, e, c) + handled = True + + + # some changes can be bundled - this is not entirely + # convenient, especially since it makes it hard to give + # each a type, so unbundle them + if not handled: + unhandled_lines = [] + for line in c.comment_text.split("<br>"): + line = line.replace(" ", " ") + # status date changed + match = re_status_date_changed.search(line) + if match: + e = StatusDateDocEvent(type="changed_status_date", date=date_in_match(match)) + e.desc = line + save_docevent(d, e, c) + handled = True + + # AD/job owner changed + match = re_responsible_ad_changed.search(line) + if match: + e = DocEvent(type="changed_document") + e.desc = line + save_docevent(d, e, c) + handled = True + + # intended standard level changed + match = re_intended_status_changed.search(line) + if match: + e = DocEvent(type="changed_document") + e.desc = line + save_docevent(d, e, c) + handled = True + + # state change notice + match = re_state_change_notice.search(line) + if match: + e = DocEvent(type="changed_document") + e.desc = line + save_docevent(d, e, c) + handled = True + + # area acronym + match = re_area_acronym_changed.search(line) + if match: + e = DocEvent(type="changed_document") + e.desc = line + save_docevent(d, e, c) + handled = True + + # multiline change bundles end with a single "by xyz" that we skip + if not handled and not line.startswith("by <b>"): + unhandled_lines.append(line) + + if handled: + c.comment_text = "<br>".join(unhandled_lines) + + if c.comment_text: + if "Due date has been changed" not in c.comment_text: + print "COULDN'T HANDLE multi-line comment %s '%s'" % (c.id, c.comment_text.replace("\n", " ").replace("\r", "")[0:80]) + + # all others are added as comments + if not handled: + e = DocEvent(type="added_comment") + save_docevent(d, e, c) + + # stop typical comments from being output + typical_comments = [ + "Document Shepherd Write-up for %s" % d.name, + "Who is the Document Shepherd for this document", + "We understand that this document doesn't require any IANA actions", + "IANA questions", + "IANA has questions", + "IANA comments", + "IANA Comments", + "IANA Evaluation Comment", + "IANA Last Call Comments", + "ublished as RFC", + "A new comment added", + "Due date has been changed", + "Due date has been changed", + "by <b>", + "AD-review comments", + "IANA Last Call", + "Subject:", + "Merged with", + ] + for t in typical_comments: + if t in c.comment_text: + handled = True + break + + if not handled: + print (u"COULDN'T HANDLE comment %s '%s' by %s" % (c.id, c.comment_text.replace("\n", " ").replace("\r", "")[0:80], c.created_by)).encode("utf-8") + + e = d.latest_event() + if e: + made_up_date = e.time + else: + made_up_date = d.time + made_up_date += datetime.timedelta(seconds=1) + + e = d.latest_event(StatusDateDocEvent, type="changed_status_date") + status_date = e.date if e else None + if idinternal.status_date != status_date: + e = StatusDateDocEvent(type="changed_status_date", date=idinternal.status_date) + e.time = made_up_date + e.by = system + e.doc = d + e.desc = "Status date has been changed to <b>%s</b> from <b>%s</b>" % (idinternal.status_date, status_date) + e.save() + + e = d.latest_event(TelechatDocEvent, type="scheduled_for_telechat") + telechat_date = e.telechat_date if e else None + if not idinternal.agenda: + idinternal.telechat_date = None # normalize + + if telechat_date != idinternal.telechat_date: + e = TelechatDocEvent(type="scheduled_for_telechat", + telechat_date=idinternal.telechat_date, + returning_item=bool(idinternal.returning_item)) + # a common case is that it has been removed from the + # agenda automatically by a script without a notice in the + # comments, in that case the time is simply the day after + # the telechat + e.time = telechat_date + datetime.timedelta(days=1) if telechat_date and not idinternal.telechat_date else made_up_date + e.by = system + args = ("Placed on", idinternal.telechat_date) if idinternal.telechat_date else ("Removed from", telechat_date) + e.doc = d + e.desc = "%s agenda for telechat - %s by system" % args + e.save() + + try: + # sad fact: some ballots haven't been generated yet + ballot = idinternal.ballot + except BallotInfo.DoesNotExist: + ballot = None + + if ballot: + e = d.docevent_set.filter(type__in=("changed_ballot_position", "sent_ballot_announcement", "requested_last_call")).order_by('-time')[:1] + if e: + position_date = e[0].time + datetime.timedelta(seconds=1) + else: + position_date = made_up_date + + # make sure we got all the positions + existing = BallotPositionDocEvent.objects.filter(doc=d, type="changed_ballot_position").order_by("-time", '-id') + + for p in Position.objects.filter(ballot=ballot): + # there are some bogus ones + if iesg_login_is_secretary(p.ad): + continue + + ad = iesg_login_to_person(p.ad) + if p.noobj > 0: + pos = ballot_position_mapping["No Objection"] + elif p.yes > 0: + pos = ballot_position_mapping["Yes"] + elif p.abstain > 0: + pos = ballot_position_mapping["Abstain"] + elif p.recuse > 0: + pos = ballot_position_mapping["Recuse"] + elif p.discuss > 0: + pos = ballot_position_mapping["Discuss"] + else: + pos = ballot_position_mapping[None] + + found = False + for x in existing: + if x.ad == ad and x.pos == pos: + found = True + break + + if not found: + e = BallotPositionDocEvent() + e.type = "changed_ballot_position" + e.doc = d + e.time = position_date + e.by = system + e.ad = ad + last_pos = d.latest_event(BallotPositionDocEvent, type="changed_ballot_position", ad=e.ad) + e.pos = pos + e.discuss = last_pos.discuss if last_pos else "" + e.discuss_time = last_pos.discuss_time if last_pos else None + if e.pos_id == "discuss" and not e.discuss_time: + # in a few cases, we don't have the discuss + # text/time, fudge the time so it's not null + e.discuss_time = e.time + e.comment = last_pos.comment if last_pos else "" + e.comment_time = last_pos.comment_time if last_pos else None + if last_pos: + e.desc = "[Ballot Position Update] Position for %s has been changed to %s from %s" % (ad.name, pos.name, last_pos.pos.name) + else: + e.desc = "[Ballot Position Update] New position, %s, has been recorded for %s" % (pos.name, ad.name) + e.save() + + # make sure we got the ballot issued event + if ballot.ballot_issued and not d.docevent_set.filter(type="sent_ballot_announcement"): + position = d.docevent_set.filter(type=("changed_ballot_position")).order_by('time', 'id')[:1] + if position: + sent_date = position[0].time + else: + sent_date = made_up_date + + e = DocEvent() + e.type = "sent_ballot_announcement" + e.doc = d + e.time = sent_date + e.by = system + e.desc = "Ballot has been issued" + e.save() + + # make sure the comments and discusses are updated + positions = list(BallotPositionDocEvent.objects.filter(doc=d).order_by("-time", '-id')) + for c in IESGComment.objects.filter(ballot=ballot): + ad = iesg_login_to_person(c.ad) + for p in positions: + if p.ad == ad: + if p.comment != c.text: + p.comment = c.text + p.comment_time = c.date if p.time.date() != c.date else p.time + p.save() + break + + for c in IESGDiscuss.objects.filter(ballot=ballot): + ad = iesg_login_to_person(c.ad) + for p in positions: + if p.ad == ad: + if p.discuss != c.text: + p.discuss = c.text + p.discuss_time = c.date if p.time.date() != c.date else p.time + p.save() + break + + # if any of these events have happened, they're closer to + # the real time + e = d.docevent_set.filter(type__in=("requested_last_call", "sent_last_call", "sent_ballot_announcement", "iesg_approved", "iesg_disapproved")).order_by('time')[:1] + if e: + text_date = e[0].time - datetime.timedelta(seconds=1) + else: + text_date = made_up_date + + if idinternal.ballot.approval_text: + e, _ = WriteupDocEvent.objects.get_or_create(type="changed_ballot_approval_text", doc=d, + defaults=dict(by=system)) + e.text = idinternal.ballot.approval_text + e.time = text_date + e.desc = "Ballot approval text was added" + e.save() + + if idinternal.ballot.last_call_text: + e, _ = WriteupDocEvent.objects.get_or_create(type="changed_last_call_text", doc=d, + defaults=dict(by=system)) + e.text = idinternal.ballot.last_call_text + e.time = text_date + e.desc = "Last call text was added" + e.save() + + if idinternal.ballot.ballot_writeup: + e, _ = WriteupDocEvent.objects.get_or_create(type="changed_ballot_writeup_text", doc=d, + defaults=dict(by=system)) + e.text = idinternal.ballot.ballot_writeup + e.time = text_date + e.desc = "Ballot writeup text was added" + e.save() + + ballot_set = idinternal.ballot_set() + if len(ballot_set) > 1: + others = sorted(b.draft.filename for b in ballot_set if b != idinternal) + desc = u"This was part of a ballot set with: %s" % ",".join(others) + DocEvent.objects.get_or_create(type="added_comment", doc=d, desc=desc, + defaults=dict(time=made_up_date, + by=system)) + + # fix tags + sync_tag(d, idinternal.via_rfc_editor, tag_via_rfc_editor) + + n = idinternal.cur_sub_state and idinternal.cur_sub_state.sub_state + for k, v in substate_mapping.iteritems(): + sync_tag(d, k == n, v) + # currently we ignore prev_sub_state + + sync_tag(d, idinternal.approved_in_minute, tag_approved_in_minute) + + + +all_drafts = InternetDraft.objects.all().select_related() +if document_name_to_import: + if document_name_to_import.startswith("rfc"): + all_drafts = all_drafts.filter(rfc_number=document_name_to_import[3:]) + else: + all_drafts = all_drafts.filter(filename=document_name_to_import) +#all_drafts = all_drafts[all_drafts.count() - 1000:] +#all_drafts = all_drafts.none() + +for index, o in enumerate(all_drafts.iterator()): + print "importing", o.id_document_tag, o.filename, index, "ballot %s" % o.idinternal.ballot_id if o.idinternal and o.idinternal.ballot_id else "" + + try: + d = Document.objects.get(name=o.filename) + except Document.DoesNotExist: + d = Document(name=o.filename) + + d.time = o.revision_date + d.type = type_draft + d.title = o.title + d.state = state_mapping[o.status.status] + d.group = Group.objects.get(acronym=o.group.acronym) + if o.filename.startswith("draft-iab-"): + d.stream = stream_mapping["IAB"] + elif o.filename.startswith("draft-irtf-"): + d.stream = stream_mapping["IRTF"] + elif o.idinternal and o.idinternal.via_rfc_editor: + d.stream = stream_mapping["INDEPENDENT"] + else: + d.stream = stream_mapping["IETF"] + d.wg_state = None + d.iesg_state = iesg_state_mapping[None] + d.iana_state = None + d.rfc_state = None + d.rev = o.revision + d.abstract = o.abstract + d.pages = o.txt_page_count + d.intended_std_level = intended_std_level_mapping[o.intended_status.intended_status] + d.ad = None + d.shepherd = None + d.notify = "" + d.external_url = "" + d.note = "" + d.internal_comments = o.comments or "" + d.save() + + # make sure our alias is updated + d_alias = alias_doc(d.name, d) + + # RFC alias + if o.rfc_number: + alias_doc("rfc%s" % o.rfc_number, d) + + # authors + d.authors.clear() + for i, a in enumerate(o.authors.all().select_related("person").order_by('author_order', 'person')): + try: + e = Email.objects.get(address__iexact=a.email() or a.person.email()[1] or u"unknown-email-%s-%s" % (a.person.first_name, a.person.last_name)) + # renumber since old numbers may be a bit borked + DocumentAuthor.objects.create(document=d, author=e, order=i) + except Email.DoesNotExist: + print "SKIPPED author", unicode(a.person).encode('utf-8') + + # clear any already imported events + d.docevent_set.all().delete() + + if o.idinternal: + # import attributes and events + import_from_idinternal(d, o.idinternal) + + # import missing revision changes from DraftVersions + known_revisions = set(e.rev for e in NewRevisionDocEvent.objects.filter(doc=d, type="new_revision")) + draft_versions = list(DraftVersions.objects.filter(filename=d.name).order_by("revision")) + # DraftVersions is not entirely accurate, make sure we got the current one + draft_versions.insert(0, DraftVersions(filename=d.name, revision=o.revision_display(), revision_date=o.revision_date)) + for v in draft_versions: + if v.revision not in known_revisions: + e = NewRevisionDocEvent(type="new_revision") + e.rev = v.revision + # we don't have time information in this source, so + # hack the seconds to include the revision to ensure + # they're ordered correctly + e.time = datetime.datetime.combine(v.revision_date, datetime.time(0, 0, 0)) + datetime.timedelta(seconds=int(v.revision)) + e.by = system + e.doc = d + e.desc = "New version available" + e.save() + known_revisions.add(v.revision) + + # import events that might be missing, we can't be sure who did + # them or when but if we don't generate them, we'll be missing the + # information completely + + # make sure last decision is recorded + e = d.latest_event(type__in=("iesg_approved", "iesg_disapproved")) + decision_date = e.time.date() if e else None + if o.b_approve_date != decision_date: + disapproved = o.idinternal and o.idinternal.dnp + e = DocEvent(type="iesg_disapproved" if disapproved else "iesg_approved") + e.time = o.b_approve_date + e.by = system + e.doc = d + e.desc = "Do Not Publish note has been sent to RFC Editor" if disapproved else "IESG has approved" + e.save() + + if o.lc_expiration_date: + e = LastCallDocEvent(type="sent_last_call", expires=o.lc_expiration_date) + # let's try to find the actual change + events = d.docevent_set.filter(type="changed_document", desc__contains=" to <b>In Last Call</b>").order_by('-time')[:1] + # event time is more accurate with actual time instead of just + # date, gives better sorting + e.time = events[0].time if events else o.lc_sent_date + e.by = events[0].by if events else system + e.doc = d + e.desc = "Last call sent" + e.save() + + # import other attributes + + # tags + sync_tag(d, o.review_by_rfc_editor, tag_review_by_rfc_editor) + sync_tag(d, o.expired_tombstone, tag_expired_tombstone) + + # replacements + if o.replaced_by: + replacement, _ = Document.objects.get_or_create(name=o.replaced_by.filename, defaults=dict(time=datetime.datetime(1970, 1, 1, 0, 0, 0))) + RelatedDocument.objects.get_or_create(source=replacement, target=d_alias, relationship=relationship_replaces) + + # the RFC-related attributes are imported when we handle the RFCs below + +# now process RFCs + +def get_or_create_rfc_document(rfc_number): + name = "rfc%s" % rfc_number + + # try to find a draft that can form the base of the document + draft = None + + ids = InternetDraft.objects.filter(rfc_number=rfc_number)[:1] + if ids: + draft = ids[0] + else: + r = RfcIndex.objects.get(rfc_number=rfc_number) + # rfcindex occasionally includes drafts that were not + # really submitted to IETF (e.g. April 1st) + if r.draft: + ids = InternetDraft.objects.filter(filename=r.draft)[:1] + if ids: + draft = ids[0] + + if rfc_number in (2604, 3025): + # prevent merge for some botched RFCs that are obsoleted by + # another RFC coming from the same draft, in practice this is + # just these two, so we hardcode rather than querying for it + draft = None + + if draft: + name = draft.filename + + d, _ = Document.objects.get_or_create(name=name) + if not name.startswith('rfc'): + # make sure draft also got an alias + alias_doc(name, d) + + alias = alias_doc("rfc%s" % rfc_number, d) + + return (d, alias) + + +all_rfcs = RfcIndex.objects.all() + +if all_drafts.count() != InternetDraft.objects.count(): + if document_name_to_import and document_name_to_import.startswith("rfc"): + # we wanted to import an RFC + all_rfcs = all_rfcs.filter(rfc_number=document_name_to_import[3:]) + else: + # if we didn't process all drafts, limit the RFCs to the ones we + # did process + all_rfcs = all_rfcs.filter(rfc_number__in=set(d.rfc_number for d in all_drafts if d.rfc_number)) + +for index, o in enumerate(all_rfcs.iterator()): + print "importing rfc%s" % o.rfc_number, index + + d, d_alias = get_or_create_rfc_document(o.rfc_number) + d.time = datetime.datetime.now() + d.title = o.title + d.std_level = std_level_mapping[o.current_status] + d.state = state_mapping['RFC'] + d.stream = stream_mapping[o.stream] + if not d.group and o.wg: + d.group = Group.objects.get(acronym=o.wg) + + # get some values from the rfc table + rfcs = Rfc.objects.filter(rfc_number=o.rfc_number).select_related() + if rfcs: + r = rfcs[0] + l = intended_std_level_mapping[r.intended_status.status] + if l: # skip some bogus None values + d.intended_std_level = l + d.save() + + # a few RFCs have an IDInternal so we may have to import the + # events and attributes + internals = IDInternal.objects.filter(rfc_flag=1, draft=o.rfc_number) + if internals: + if d.name.startswith("rfc"): + # clear any already imported events, we don't do it for + # drafts as they've already been cleared above + d.docevent_set.all().delete() + import_from_idinternal(d, internals[0]) + + # publication date + e, _ = DocEvent.objects.get_or_create(doc=d, type="published_rfc", + defaults=dict(by=system)) + e.time = o.rfc_published_date + e.desc = "RFC published" + e.save() + + # import obsoletes/updates + def make_relation(other_rfc, rel_type, reverse): + other_number = int(other_rfc.replace("RFC", "")) + other, other_alias = get_or_create_rfc_document(other_number) + if reverse: + RelatedDocument.objects.get_or_create(source=other, target=d_alias, relationship=rel_type) + else: + RelatedDocument.objects.get_or_create(source=d, target=other_alias, relationship=rel_type) + + def parse_relation_list(s): + if not s: + return [] + res = [] + for x in s.split(","): + if x[:3] in ("NIC", "IEN", "STD", "RTR"): + # try translating this to RFC numbers that we can + # handle sensibly; otherwise we'll have to ignore them + l = ["RFC%s" % y.rfc_number for y in RfcIndex.objects.filter(also=x).order_by('rfc_number')] + if l: + print "translated", x, "to", ", ".join(l) + for y in l: + if y not in res: + res.append(y) + else: + print "SKIPPED relation to", x + else: + res.append(x) + return res + + RelatedDocument.objects.filter(source=d).delete() + for x in parse_relation_list(o.obsoletes): + make_relation(x, relationship_obsoletes, False) + for x in parse_relation_list(o.obsoleted_by): + make_relation(x, relationship_obsoletes, True) + for x in parse_relation_list(o.updates): + make_relation(x, relationship_updates, False) + for x in parse_relation_list(o.updated_by): + make_relation(x, relationship_updates, True) + + if o.also: + for a in o.also.lower().split(","): + alias_doc(a, d) + + sync_tag(d, o.has_errata, tag_has_errata) + + # FIXME: import RFC authors? diff --git a/redesign/importing/import-groups.py b/redesign/importing/import-groups.py new file mode 100755 index 000000000..6e5692b86 --- /dev/null +++ b/redesign/importing/import-groups.py @@ -0,0 +1,274 @@ +#!/usr/bin/python + +import sys, os, datetime + +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +settings.USE_DB_REDESIGN_PROXY_CLASSES = False + +from django.core import management +management.setup_environ(settings) + + +from redesign.group.models import * +from redesign.name.models import * +from redesign.name.utils import name +from redesign.importing.utils import old_person_to_person +from ietf.idtracker.models import AreaGroup, IETFWG, Area, AreaGroup, Acronym, AreaWGURL, IRTF, ChairsHistory, Role, AreaDirector + +# imports IETFWG, Area, AreaGroup, Acronym, IRTF, AreaWGURL + +# also creates nomcom groups + +# assumptions: persons have been imported + +state_names = dict( + bof=name(GroupStateName, slug="bof", name="BOF"), + proposed=name(GroupStateName, slug="proposed", name="Proposed"), + active=name(GroupStateName, slug="active", name="Active"), + dormant=name(GroupStateName, slug="dormant", name="Dormant"), + conclude=name(GroupStateName, slug="conclude", name="Concluded"), + unknown=name(GroupStateName, slug="unknown", name="Unknown"), + ) + +type_names = dict( + ietf=name(GroupTypeName, slug="ietf", name="IETF"), + area=name(GroupTypeName, slug="area", name="Area"), + ag=name(GroupTypeName, slug="ag", name="AG"), + wg=name(GroupTypeName, slug="wg", name="WG"), + rg=name(GroupTypeName, slug="rg", name="RG"), + team=name(GroupTypeName, slug="team", name="Team"), + individ=name(GroupTypeName, slug="individ", name="Individual"), + ) + +# make sure we got the IESG so we can use it as parent for areas +iesg_group, _ = Group.objects.get_or_create(acronym="iesg") +iesg_group.name = "IESG" +iesg_group.state = state_names["active"] +iesg_group.type = type_names["ietf"] +iesg_group.save() + +# make sure we got the IRTF as parent for RGs +irtf_group, _ = Group.objects.get_or_create(acronym="irtf") +irtf_group.name = "IRTF" +irtf_group.state = state_names["active"] +irtf_group.type = type_names["ietf"] +irtf_group.save() + +# create Secretariat for use with roles +secretariat_group, _ = Group.objects.get_or_create(acronym="secretariat") +secretariat_group.name = "IETF Secretariat" +secretariat_group.state = state_names["active"] +secretariat_group.type = type_names["ietf"] +secretariat_group.save() + +system = Person.objects.get(name="(System)") + + +# NomCom +for o in ChairsHistory.objects.filter(chair_type=Role.NOMCOM_CHAIR).order_by("start_year"): + print "importing ChairsHistory/Nomcom", o.pk, "nomcom%s" % o.start_year + group, _ = Group.objects.get_or_create(acronym="nomcom%s" % o.start_year) + group.name = "IAB/IESG Nominating Committee %s/%s" % (o.start_year, o.end_year) + if o.chair_type.person == o.person: + s = state_names["active"] + else: + s = state_names["conclude"] + group.state = s + group.type = type_names["ietf"] + group.parent = None + group.save() + + # we need start/end year so fudge events + group.groupevent_set.all().delete() + + e = GroupEvent(group=group, type="started") + e.time = datetime.datetime(o.start_year, 5, 1, 12, 0, 0) + e.by = system + e.desc = e.get_type_display() + e.save() + + e = GroupEvent(group=group, type="concluded") + e.time = datetime.datetime(o.end_year, 5, 1, 12, 0, 0) + e.by = system + e.desc = e.get_type_display() + e.save() + +# Area +for o in Area.objects.all(): + print "importing Area", o.pk, o.area_acronym.acronym + + try: + group = Group.objects.get(acronym=o.area_acronym.acronym) + except Group.DoesNotExist: + group = Group(acronym=o.area_acronym.acronym) + group.id = o.area_acronym_id # transfer id + + if o.last_modified_date: + group.time = datetime.datetime.combine(o.last_modified_date, datetime.time(12, 0, 0)) + group.name = o.area_acronym.name + if o.status.status == "Active": + s = state_names["active"] + elif o.status.status == "Concluded": + s = state_names["conclude"] + elif o.status.status == "Unknown": + s = state_names["unknown"] + group.state = s + group.type = type_names["area"] + group.parent = iesg_group + group.comments = o.comments.strip() if o.comments else "" + + group.save() + + for u in o.additional_urls(): + url, _ = GroupURL.objects.get_or_create(group=group, url=u.url) + url.name = u.description.strip() + url.save() + + # import events + group.groupevent_set.all().delete() + + if o.concluded_date: + e = GroupEvent(group=group, type="concluded") + e.time = datetime.datetime.combine(o.concluded_date, datetime.time(12, 0, 0)) + e.by = system + e.desc = e.get_type_display() + e.save() + + # FIXME: missing fields from old: extra_email_addresses + + +# IRTF +for o in IRTF.objects.all(): + print "importing IRTF", o.pk, o.acronym + + try: + group = Group.objects.get(acronym=o.acronym.lower()) + except Group.DoesNotExist: + group = Group(acronym=o.acronym.lower()) + + group.name = o.name + group.state = state_names["active"] # we assume all to be active + group.type = type_names["rg"] + group.parent = irtf_group + + group.comments = o.charter_text or "" + + group.save() + + # FIXME: missing fields from old: meeting_scheduled + +# IETFWG, AreaGroup +for o in IETFWG.objects.all().order_by("pk"): + print "importing IETFWG", o.pk, o.group_acronym.acronym + + try: + group = Group.objects.get(acronym=o.group_acronym.acronym) + except Group.DoesNotExist: + group = Group(acronym=o.group_acronym.acronym) + group.id = o.group_acronym_id # transfer id + + if o.last_modified_date: + group.time = datetime.datetime.combine(o.last_modified_date, datetime.time(12, 0, 0)) + group.name = o.group_acronym.name + # state + if o.group_type.type == "BOF": + s = state_names["bof"] + elif o.group_type.type == "PWG": + s = state_names["proposed"] + elif o.status.status == "Active": + s = state_names["active"] + elif o.status.status == "Dormant": + s = state_names["dormant"] + elif o.status.status == "Concluded": + s = state_names["conclude"] + group.state = s + # type + if o.group_type.type == "TEAM": + group.type = type_names["team"] + elif o.group_type.type == "AG": + if o.group_acronym.acronym == "none": + # none means individual + group.type = type_names["individ"] + elif o.group_acronym.acronym == "iab": + group.type = type_names["ietf"] + group.parent = None + elif o.group_acronym.acronym in ("tsvdir", "secdir", "saag", "usac"): + group.type = type_names["team"] + elif o.group_acronym.acronym == "iesg": + pass # we already treated iesg + elif o.group_acronym.acronym in ("apparea", "opsarea", "rtgarea", "usvarea", "genarea", "tsvarea", "raiarea", "apptsv"): + group.type = type_names["ag"] + else: + # the remaining groups are + # apples, null, dirdir + # for now, we don't transfer them + if group.id: + group.delete() + print "not transferring", o.group_acronym.acronym, o.group_acronym.name + continue + else: # PWG/BOF/WG + # some BOFs aren't WG-forming but we currently classify all as WGs + group.type = type_names["wg"] + + if o.area: + group.parent = Group.objects.get(acronym=o.area.area.area_acronym.acronym) + elif not group.parent: + print "no area/parent for", group.acronym, group.name, group.type, group.state + + try: + area_director = o.area_director + except AreaDirector.DoesNotExist: + area_director = None + if area_director and not area_director.area_id: + area_director = None # fake TBD guy + + group.ad = old_person_to_person(area_director.person) if area_director else None + group.list_email = o.email_address if o.email_address else "" + group.list_subscribe = (o.email_subscribe or "").replace("//listinfo", "/listinfo").strip() + l = o.clean_email_archive().strip() if o.email_archive else "" + if l in ("none", "not available"): + l = "" + group.list_archive = l + group.comments = o.comments.strip() if o.comments else "" + + group.save() + + for u in o.additional_urls(): + url, _ = GroupURL.objects.get_or_create(group=group, url=u.url) + url.name = u.description.strip() + url.save() + + for m in o.milestones(): + desc = m.description.strip() + try: + milestone = GroupMilestone.objects.get(group=group, desc=desc) + except GroupMilestone.DoesNotExist: + milestone = GroupMilestone(group=group, desc=desc) + + milestone.expected_due_date = m.expected_due_date + milestone.done = m.done == "Done" + milestone.done_date = m.done_date + milestone.time = datetime.datetime.combine(m.last_modified_date, datetime.time(12, 0, 0)) + milestone.save() + + # import events + group.groupevent_set.all().delete() + + def import_date_event(name, type_name): + d = getattr(o, "%s_date" % name) + if d: + e = GroupEvent(group=group, type=type_name) + e.time = datetime.datetime.combine(d, datetime.time(12, 0, 0)) + e.by = system + e.desc = e.get_type_display() + e.save() + + import_date_event("proposed", "proposed") + import_date_event("start", "started") + import_date_event("concluded", "concluded") + # dormant_date is empty on all so don't bother with that + + # FIXME: missing fields from old: meeting_scheduled, email_keyword, meeting_scheduled_old diff --git a/redesign/importing/import-ipr.py b/redesign/importing/import-ipr.py new file mode 100755 index 000000000..07c267b72 --- /dev/null +++ b/redesign/importing/import-ipr.py @@ -0,0 +1,59 @@ +#!/usr/bin/python + +import sys, os, re, datetime + +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +settings.USE_DB_REDESIGN_PROXY_CLASSES = False +settings.IMPORTING_IPR = True + +from django.core import management +management.setup_environ(settings) + +from ietf.ipr.models import IprDraftOld, IprRfcOld, IprDocAlias, IprDetail +from redesign.doc.models import DocAlias + +# imports IprDraft and IprRfc, converting them to IprDocAlias links to Document + +# assumptions: documents have been imported + +# some links are borked, only import those that reference an existing IprDetail +ipr_ids = IprDetail.objects.all() + +for o in IprDraftOld.objects.filter(ipr__in=ipr_ids).select_related("document").order_by("id").iterator(): + try: + alias = DocAlias.objects.get(name=o.document.filename) + except DocAlias.DoesNotExist: + print "COULDN'T FIND DOCUMENT", o.document.filename + continue + + try: + IprDocAlias.objects.get(ipr=o.ipr_id, doc_alias=alias) + except IprDocAlias.DoesNotExist: + link = IprDocAlias() + link.ipr_id = o.ipr_id + link.doc_alias = alias + link.rev = o.revision or "" + link.save() + + print "importing IprDraft", o.pk, "linking", o.ipr_id, o.document.filename + +for o in IprRfcOld.objects.filter(ipr__in=ipr_ids).select_related("document").order_by("id").iterator(): + try: + alias = DocAlias.objects.get(name="rfc%s" % o.document.rfc_number) + except DocAlias.DoesNotExist: + print "COULDN'T FIND RFC%s", o.document.rfc_number + continue + + try: + IprDocAlias.objects.get(ipr=o.ipr_id, doc_alias=alias) + except IprDocAlias.DoesNotExist: + link = IprDocAlias() + link.ipr_id = o.ipr_id + link.doc_alias = alias + link.rev = "" + link.save() + + print "importing IprRfc", o.pk, "linking", o.ipr_id, o.document.rfc_number diff --git a/redesign/importing/import-meetings.py b/redesign/importing/import-meetings.py new file mode 100755 index 000000000..980a5ec4f --- /dev/null +++ b/redesign/importing/import-meetings.py @@ -0,0 +1,273 @@ +#!/usr/bin/python + +import sys, os, re, datetime, pytz + +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +settings.USE_DB_REDESIGN_PROXY_CLASSES = False + +from django.core import management +management.setup_environ(settings) + + +from ietf.idtracker.models import AreaDirector, IETFWG, Acronym, IRTF +from ietf.meeting.models import * +from ietf.proceedings.models import Meeting as MeetingOld, MeetingVenue, MeetingRoom, NonSession, WgMeetingSession +from redesign.person.models import * +from redesign.importing.utils import get_or_create_email, old_person_to_person +from redesign.name.utils import name + + +# imports Meeting, MeetingVenue, MeetingRoom, NonSession, WgMeetingSession + +# assumptions: +# - persons have been imported +# - groups have been imported + + +session_status_mapping = { + 1: name(SessionStatusName, "schedw", "Waiting for Scheduling"), + 2: name(SessionStatusName, "apprw", "Waiting for Approval"), + 3: name(SessionStatusName, "appr", "Approved"), + 4: name(SessionStatusName, "sched", "Scheduled"), + 5: name(SessionStatusName, "canceled", "Canceled"), + 6: name(SessionStatusName, "disappr", "Disapproved"), + } + +session_status_mapping[0] = session_status_mapping[1] # assume broken statuses of 0 are actually cancelled + +session_slot = name(TimeSlotTypeName, "session", "Session") +break_slot = name(TimeSlotTypeName, "break", "Break") +registration_slot = name(TimeSlotTypeName, "reg", "Registration") +other_slot = name(TimeSlotTypeName, "other", "Other") +conflict_constraint = name(ConstraintName, "conflict", "Conflicts with") + +system_person = Person.objects.get(name="(System)") +obviously_bogus_date = datetime.date(1970, 1, 1) + +for o in MeetingOld.objects.all(): + print "importing Meeting", o.pk + + try: + m = Meeting.objects.get(number=o.meeting_num) + except: + m = Meeting(number="%s" % o.meeting_num) + m.pk = o.pk + + m.date = o.start_date + m.city = o.city + + # convert country to code + country_code = None + for k, v in pytz.country_names.iteritems(): + if v == o.country: + country_code = k + break + + if not country_code: + country_fallbacks = { + 'USA': 'US' + } + + country_code = country_fallbacks.get(o.country) + + if country_code: + m.country = country_code + else: + print "unknown country", o.country + + + time_zone_lookup = { + ("IE", "Dublin"): "Europe/Dublin", + ("FR", "Paris"): "Europe/Paris", + ("CA", "Vancouver"): "America/Vancouver", + ("CZ", "Prague"): "Europe/Prague", + ("US", "Chicago"): "America/Chicago", + ("US", "Anaheim"): "America/Los_Angeles", + ("NL", "Maastricht"): "Europe/Amsterdam", + ("CN", "Beijing"): "Asia/Shanghai", + ("JP", "Hiroshima"): "Asia/Tokyo", + ("SE", "Stockholm"): "Europe/Stockholm", + ("US", "San Francisco"): "America/Los_Angeles", + ("US", "Minneapolis"): "America/Menominee", + } + + m.time_zone = time_zone_lookup.get((m.country, m.city), "") + if not m.time_zone: + print "unknown time zone for", m.get_country_display(), m.city + + m.venue_name = "" # no source for that in the old DB? + m.venue_addr = "" # no source for that in the old DB? + try: + venue = o.meetingvenue_set.get() + m.break_area = venue.break_area_name + m.reg_area = venue.reg_area_name + except MeetingVenue.DoesNotExist: + pass + + # missing following semi-used fields from old Meeting: end_date, + # ack, agenda_html/agenda_text, future_meeting + + m.save() + +for o in MeetingRoom.objects.all(): + print "importing MeetingRoom", o.pk + + try: + r = Room.objects.get(pk=o.pk) + except Room.DoesNotExist: + r = Room(pk=o.pk) + + r.meeting = Meeting.objects.get(number="%s" % o.meeting_id) + r.name = o.room_name + r.save() + +def parse_time_desc(o): + t = o.time_desc.replace(' ', '') + + start_time = datetime.time(int(t[0:2]), int(t[2:4])) + end_time = datetime.time(int(t[5:7]), int(t[7:9])) + + d = o.meeting.start_date + datetime.timedelta(days=o.day_id) + + return (datetime.datetime.combine(d, start_time), datetime.datetime.combine(d, end_time)) + +def get_or_create_session_timeslot(meeting_time, room): + meeting = Meeting.objects.get(number=s.meeting_id) + starts, ends = parse_time_desc(meeting_time) + + try: + slot = TimeSlot.objects.get(meeting=meeting, time=starts, location=room) + except TimeSlot.DoesNotExist: + slot = TimeSlot(meeting=meeting, time=starts, location=room) + + slot.type = session_slot + slot.name = meeting_time.session_name.session_name if meeting_time.session_name_id else "Unknown" + slot.duration = ends - starts + slot.save() + + return slot + +requested_length_mapping = { + None: 0, # assume NULL to mean nothing particular requested + "1": 60 * 60, + "2": 90 * 60, + "3": 120 * 60, + "4": 150 * 60, + } + +for o in WgMeetingSession.objects.all().order_by("pk"): + # num_session is unfortunately not quite reliable, seems to be + # right for 1 or 2 but not 3 and it's sometimes null + sessions = o.num_session or 1 + if o.sched_time_id3: + sessions = 3 + + print "importing WgMeetingSession", o.pk, "subsessions", sessions + + for i in range(1, 1 + sessions): + try: + s = Session.objects.get(pk=o.pk + (i - 1) * 10000) + except: + s = Session(pk=o.pk) + + s.meeting = Meeting.objects.get(number=o.meeting_id) + sched_time_id = getattr(o, "sched_time_id%s" % i) + if sched_time_id: + room = Room.objects.get(pk=getattr(o, "sched_room_id%s_id" % i)) + s.timeslot = get_or_create_session_timeslot(sched_time_id, room) + else: + s.timeslot = None + if o.irtf: + s.group = Group.objects.get(acronym=IRTF.objects.get(pk=o.group_acronym_id).acronym.lower()) + else: + acronym = Acronym.objects.get(pk=o.group_acronym_id) + if o.group_acronym_id < 0: + # this wasn't actually a WG session, but rather a tutorial + # or similar, don't create a session but instead modify + # the time slot appropriately + if not s.timeslot: + print "IGNORING unscheduled non-WG-session", acronym.name + continue + s.timeslot.name = acronym.name + s.timeslot.type = other_slot + s.timeslot.save() + continue + + s.group = Group.objects.get(acronym=acronym.acronym) + s.attendees = o.number_attendee + s.agenda_note = (o.special_agenda_note or "").strip() + s.requested = o.requested_date or obviously_bogus_date + s.requested_by = old_person_to_person(o.requested_by) if o.requested_by else system_person + s.requested_duration = requested_length_mapping[getattr(o, "length_session%s" % i)] + comments = [] + special_req = (o.special_req or "").strip() + if special_req: + comments.append(u"Special requests:\n" + special_req) + conflict_other = (o.conflict_other or "").strip() + if conflict_other: + comments.append(u"Other conflicts:\n" + conflict_other) + s.comments = u"\n\n".join(comments) + s.status = session_status_mapping[o.status_id or 5] + + s.scheduled = o.scheduled_date + s.modified = o.last_modified_date + + s.save() + + conflict = (getattr(o, "conflict%s" % i) or "").replace(",", " ").lower() + conflicting_groups = [g for g in conflict.split() if g] + for target in Group.objects.filter(acronym__in=conflicting_groups): + Constraint.objects.get_or_create( + meeting=s.meeting, + source=target, + target=s.group, + name=conflict_constraint) + + + # missing following fields from old: ts_status_id (= third session + # status id, third session required AD approval), + # combined_room_id1/2, combined_time_id1/2 + +for o in NonSession.objects.all().order_by('pk').select_related("meeting"): + print "importing NonSession", o.pk + + if o.time_desc in ("", "0"): + print "IGNORING non-scheduled NonSession", o.non_session_ref.name + continue + + meeting = Meeting.objects.get(number=o.meeting_id) + + # some non-sessions are scheduled every day, but only if there's a + # session nearby, figure out which days this corresponds to + days = set() + if o.day_id == None: + t = datetime.time(int(o.time_desc[-4:][0:2]), int(o.time_desc[-4:][2:4])) + + for s in TimeSlot.objects.filter(meeting=meeting): + if s.time.time() == t: + days.add((s.time.date() - meeting.date).days) + else: + days.add(o.day_id) + + for day in days: + o.day_id = day + starts, ends = parse_time_desc(o) + name = o.non_session_ref.name + + try: + slot = TimeSlot.objects.get(meeting=meeting, time=starts, name=name) + except TimeSlot.DoesNotExist: + slot = TimeSlot(meeting=meeting, time=starts, name=name) + + slot.location = None + if o.non_session_ref_id == 1: + slot.type = registration_slot + else: + slot.type = break_slot + + slot.duration = ends - starts + slot.show_location = o.show_break_location + slot.save() diff --git a/redesign/importing/import-persons.py b/redesign/importing/import-persons.py new file mode 100755 index 000000000..4cb6a935c --- /dev/null +++ b/redesign/importing/import-persons.py @@ -0,0 +1,90 @@ +#!/usr/bin/python + +import sys, os, re, datetime + +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +settings.USE_DB_REDESIGN_PROXY_CLASSES = False + +from django.core import management +management.setup_environ(settings) + +from ietf.idtracker.models import AreaDirector, IETFWG, PersonOrOrgInfo, IDAuthor +from redesign.person.models import * +from redesign.importing.utils import clean_email_address, get_or_create_email + +# creates system person and email + +# imports AreaDirector persons that are connected to an IETFWG, +# persons from IDAuthor, announcement originators from Announcements, +# requesters from WgMeetingSession + +# should probably import +# PersonOrOrgInfo/PostalAddress/EmailAddress/PhoneNumber fully + +# make sure special system user/email is created +print "creating (System) person and email" +try: + system_person = Person.objects.get(name="(System)") +except Person.DoesNotExist: + system_person = Person.objects.create( + id=0, # special value + name="(System)", + ascii="(System)", + address="", + ) + + system_person = Person.objects.get(name="(System)") + +if system_person.id != 0: # work around bug in Django + Person.objects.filter(id=system_person.id).update(id=0) + system_person = Person.objects.get(id=0) + +system_alias = Alias.objects.get_or_create( + person=system_person, + name=system_person.name + ) + +system_email = Email.objects.get_or_create( + address="", + defaults=dict(active=True, person=system_person) + ) + +# AreaDirector from IETFWG persons +for o in AreaDirector.objects.filter(ietfwg__in=IETFWG.objects.all()).exclude(area=None).distinct().order_by("pk").iterator(): + print "importing AreaDirector (from IETFWG) persons", o.pk + + get_or_create_email(o, create_fake=False) + +# WgMeetingSession persons +for o in PersonOrOrgInfo.objects.filter(wgmeetingsession__pk__gte=1).distinct().order_by("pk").iterator(): + print "importing WgMeetingSession persons", o.pk, o.first_name.encode('utf-8'), o.last_name.encode('utf-8') + + get_or_create_email(o, create_fake=False) + +# Announcement persons +for o in PersonOrOrgInfo.objects.filter(announcement__announcement_id__gte=1).order_by("pk").distinct(): + print "importing Announcement originator", o.pk, o.first_name.encode('utf-8'), o.last_name.encode('utf-8') + + email = get_or_create_email(o, create_fake=False) + +# IDAuthor persons +for o in IDAuthor.objects.all().order_by('id').select_related('person').iterator(): + print "importing IDAuthor", o.id, o.person_id, o.person.first_name.encode('utf-8'), o.person.last_name.encode('utf-8') + email = get_or_create_email(o, create_fake=True) + + # we may also need to import email address used specifically for + # the document + addr = clean_email_address(o.email() or "") + if addr and addr.lower() != email.address.lower(): + try: + e = Email.objects.get(address=addr) + if e.person != email.person or e.active != False: + e.person = email.person + e.active = False + e.save() + except Email.DoesNotExist: + Email.objects.create(address=addr, person=email.person, active=False) + diff --git a/redesign/importing/import-roles.py b/redesign/importing/import-roles.py new file mode 100755 index 000000000..a19850086 --- /dev/null +++ b/redesign/importing/import-roles.py @@ -0,0 +1,173 @@ +#!/usr/bin/python + +import sys, os, re, datetime + +basedir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")) +sys.path = [ basedir ] + sys.path + +from ietf import settings +settings.USE_DB_REDESIGN_PROXY_CLASSES = False + +from django.core import management +management.setup_environ(settings) + +from redesign.person.models import * +from redesign.group.models import * +from redesign.name.models import * +from redesign.name.utils import name +from redesign.importing.utils import get_or_create_email + +from ietf.idtracker.models import IESGLogin, AreaDirector, PersonOrOrgInfo, WGChair, WGEditor, WGSecretary, WGTechAdvisor, ChairsHistory, Role as OldRole, Acronym, IRTFChair + + +# assumptions: +# - persons have been imported +# - groups have been imported + +# imports IESGLogin, AreaDirector, WGEditor, WGChair, IRTFChair, +# WGSecretary, WGTechAdvisor, NomCom chairs from ChairsHistory, + +# FIXME: should probably import Role, LegacyWgPassword, LegacyLiaisonUser + +area_director_role = name(RoleName, "ad", "Area Director") +inactive_area_director_role = name(RoleName, "ex-ad", "Ex-Area Director", desc="Inactive Area Director") +chair_role = name(RoleName, "chair", "Chair") +editor_role = name(RoleName, "editor", "Editor") +secretary_role = name(RoleName, "secr", "Secretary") +techadvisor_role = name(RoleName, "techadv", "Tech Advisor") + + +# WGEditor +for o in WGEditor.objects.all(): + acronym = Acronym.objects.get(acronym_id=o.group_acronym_id).acronym + print "importing WGEditor", acronym, o.person + + email = get_or_create_email(o, create_fake=True) + group = Group.objects.get(acronym=acronym) + + Role.objects.get_or_create(name=editor_role, group=group, email=email) + +# WGSecretary +for o in WGSecretary.objects.all(): + acronym = Acronym.objects.get(acronym_id=o.group_acronym_id).acronym + print "importing WGSecretary", acronym, o.person + + email = get_or_create_email(o, create_fake=True) + group = Group.objects.get(acronym=acronym) + + Role.objects.get_or_create(name=secretary_role, group=group, email=email) + +# WGTechAdvisor +for o in WGTechAdvisor.objects.all(): + acronym = Acronym.objects.get(acronym_id=o.group_acronym_id).acronym + print "importing WGTechAdvisor", acronym, o.person + + email = get_or_create_email(o, create_fake=True) + group = Group.objects.get(acronym=acronym) + + Role.objects.get_or_create(name=techadvisor_role, group=group, email=email) + +# WGChair +for o in WGChair.objects.all(): + # there's some garbage in this table, so wear double safety belts + try: + acronym = Acronym.objects.get(acronym_id=o.group_acronym_id).acronym + except Acronym.DoesNotExist: + print "SKIPPING WGChair with unknown acronym id", o.group_acronym_id + continue + + try: + person = o.person + except PersonOrOrgInfo.DoesNotExist: + print "SKIPPING WGChair", acronym, "with invalid person id", o.person_id + continue + + if acronym in ("apples", "apptsv", "usac", "null", "dirdir"): + print "SKIPPING WGChair", acronym, o.person + continue + + print "importing WGChair", acronym, o.person + + email = get_or_create_email(o, create_fake=True) + group = Group.objects.get(acronym=acronym) + + Role.objects.get_or_create(name=chair_role, group=group, email=email) + +# IRTFChair +for o in IRTFChair.objects.all(): + acronym = o.irtf.acronym.lower() + print "importing IRTFChair", acronym, o.person + + email = get_or_create_email(o, create_fake=True) + group = Group.objects.get(acronym=acronym) + + Role.objects.get_or_create(name=chair_role, group=group, email=email) + +# NomCom chairs +nomcom_groups = list(Group.objects.filter(acronym__startswith="nomcom").exclude(acronym="nomcom")) +for o in ChairsHistory.objects.filter(chair_type=OldRole.NOMCOM_CHAIR): + print "importing NOMCOM chair", o + for g in nomcom_groups: + if ("%s/%s" % (o.start_year, o.end_year)) in g.name: + break + + email = get_or_create_email(o, create_fake=False) + + Role.objects.get_or_create(name=chair_role, group=g, email=email) + +# IESGLogin +for o in IESGLogin.objects.all(): + print "importing IESGLogin", o.id, o.first_name, o.last_name + + if not o.person: + persons = PersonOrOrgInfo.objects.filter(first_name=o.first_name, last_name=o.last_name) + if persons: + o.person = persons[0] + else: + print "NO PERSON", o.person_id + continue + + email = get_or_create_email(o, create_fake=False) + if not email: + continue + + user, _ = User.objects.get_or_create(username=o.login_name) + email.person.user = user + email.person.save() + + # current ADs are imported below + if o.user_level == IESGLogin.SECRETARIAT_LEVEL: + if not Role.objects.filter(name=secretary_role, email=email): + Role.objects.create(name=secretary_role, group=Group.objects.get(acronym="secretariat"), email=email) + elif o.user_level == IESGLogin.INACTIVE_AD_LEVEL: + if not Role.objects.filter(name=inactive_area_director_role, email=email): + # connect them directly to the IESG as we don't really know where they belong + Role.objects.create(name=inactive_area_director_role, group=Group.objects.get(acronym="iesg"), email=email) + +# AreaDirector +for o in AreaDirector.objects.all(): + if not o.area: + print "NO AREA", o.person, o.area_id + continue + + print "importing AreaDirector", o.area, o.person + email = get_or_create_email(o, create_fake=False) + + area = Group.objects.get(acronym=o.area.area_acronym.acronym) + + if area.state_id == "active": + role_type = area_director_role + else: + # can't be active area director in an inactive area + role_type = inactive_area_director_role + + r = Role.objects.filter(name__in=(area_director_role, inactive_area_director_role), + email=email) + if r and r[0].group == "iesg": + r[0].group = area + r[0].name = role_type + r[0].save() + else: + Role.objects.get_or_create(name=role_type, group=area, email=email) + + diff --git a/redesign/importing/utils.py b/redesign/importing/utils.py new file mode 100644 index 000000000..84a07c0dc --- /dev/null +++ b/redesign/importing/utils.py @@ -0,0 +1,55 @@ +from redesign import unaccent +from redesign.person.models import Person, Email, Alias + +def clean_email_address(addr): + addr = addr.replace("!", "@").replace("(at)", "@") # some obvious @ replacements + addr = addr[addr.rfind('<') + 1:addr.find('>')] # whack surrounding <...> + addr = addr.strip() + if not "@" in addr: + return "" + else: + return addr + +def old_person_to_person(person): + try: + return Person.objects.get(id=person.pk) + except Person.DoesNotExist: + return Person.objects.get(alias__name=u"%s %s" % (person.first_name, person.last_name)) + +def old_person_to_email(person): + hardcoded_emails = { 'Dinara Suleymanova': "dinaras@ietf.org" } + + return clean_email_address(person.email()[1] or hardcoded_emails.get("%s %s" % (person.first_name, person.last_name)) or "") + +def get_or_create_email(o, create_fake): + # take o.person (or o) and get or create new Email and Person objects + person = o.person if hasattr(o, "person") else o + + email = old_person_to_email(person) + if not email: + if create_fake: + email = u"unknown-email-%s-%s" % (person.first_name, person.last_name) + print ("USING FAKE EMAIL %s for %s %s %s" % (email, person.pk, person.first_name, person.last_name)).encode('utf-8') + else: + print ("NO EMAIL FOR %s %s %s %s %s" % (o.__class__, o.pk, person.pk, person.first_name, person.last_name)).encode('utf-8') + return None + + e, _ = Email.objects.select_related("person").get_or_create(address=email) + if not e.person: + n = u"%s %s" % (person.first_name, person.last_name) + asciified = unaccent.asciify(n) + aliases = Alias.objects.filter(name__in=(n, asciified)) + if aliases: + p = aliases[0].person + else: + p = Person.objects.create(id=person.pk, name=n, ascii=asciified) + # FIXME: fill in address? + + Alias.objects.create(name=n, person=p) + if asciified != n: + Alias.objects.create(name=asciified, person=p) + + e.person = p + e.save() + + return e diff --git a/redesign/name/__init__.py b/redesign/name/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/redesign/name/admin.py b/redesign/name/admin.py new file mode 100644 index 000000000..347be27d7 --- /dev/null +++ b/redesign/name/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin +from models import * + +class NameAdmin(admin.ModelAdmin): + list_display = ["slug", "name", "desc", "used"] + prepopulate_from = { "slug": ("name",) } + +admin.site.register(GroupTypeName, NameAdmin) +admin.site.register(GroupStateName, NameAdmin) +admin.site.register(IesgGroupStateName, NameAdmin) +admin.site.register(RoleName, NameAdmin) +admin.site.register(DocStreamName, NameAdmin) +admin.site.register(DocStateName, NameAdmin) +admin.site.register(DocRelationshipName, NameAdmin) +admin.site.register(WgDocStateName, NameAdmin) +admin.site.register(IesgDocStateName, NameAdmin) +admin.site.register(IanaDocStateName, NameAdmin) +admin.site.register(RfcDocStateName, NameAdmin) +admin.site.register(DocTypeName, NameAdmin) +admin.site.register(DocInfoTagName, NameAdmin) +admin.site.register(StdLevelName, NameAdmin) +admin.site.register(IntendedStdLevelName, NameAdmin) +admin.site.register(BallotPositionName, NameAdmin) +admin.site.register(SessionStatusName, NameAdmin) +admin.site.register(TimeSlotTypeName, NameAdmin) +admin.site.register(ConstraintName, NameAdmin) diff --git a/redesign/name/models.py b/redesign/name/models.py new file mode 100644 index 000000000..62997a9ff --- /dev/null +++ b/redesign/name/models.py @@ -0,0 +1,98 @@ +# Copyright The IETF Trust 2007, All Rights Reserved + +from django.db import models + +class NameModel(models.Model): + slug = models.CharField(max_length=8, primary_key=True) + name = models.CharField(max_length=255) + desc = models.TextField(blank=True) + used = models.BooleanField(default=True) + order = models.IntegerField(default=0) + + def __unicode__(self): + return self.name + + class Meta: + abstract = True + ordering = ['order'] + +class GroupStateName(NameModel): + """BOF, Proposed, Active, Dormant, Concluded""" +class GroupTypeName(NameModel): + """IETF, Area, WG, RG, Team, etc.""" +class IesgGroupStateName(NameModel): + """Informal IESG review, Internal review, External review, IESG review, + WG exists, Not currently under review, Informal IESG recharter review, + Internal recharter review, External recharter review, IESG recharter + review """ +class RoleName(NameModel): + """AD, Chair""" +class DocStreamName(NameModel): + """IETF, IAB, IRTF, Independent Submission, Legacy""" +class DocStateName(NameModel): + """Active, Expired, RFC, Replaced, Withdrawn""" +class DocRelationshipName(NameModel): + """Updates, Replaces, Obsoletes, Reviews, ... The relationship is + always recorded in one direction. + """ +class WgDocStateName(NameModel): + """Not, Candidate, Active, Parked, LastCall, WriteUp, Submitted, + Dead""" +class IesgDocStateName(NameModel): + """Pub Request, Ad Eval, Expert Review, Last Call Requested, In + Last Call, Waiting for Writeup, Waiting for AD Go-Ahead, IESG + Evaluation, Deferred, Approved, Announcement Sent, Do Not Publish, + Ad is watching, Dead """ +class IanaDocStateName(NameModel): + """ """ +class RfcDocStateName(NameModel): + """Missref, Edit, RFC-Editor, Auth48, Auth, Published; ISR, + ISR-Auth, ISR-Timeout;""" +class DocTypeName(NameModel): + """Draft, Agenda, Minutes, Charter, Discuss, Guideline, Email, + Review, Issue, Wiki""" +class DocInfoTagName(NameModel): + """Waiting for Reference, IANA Coordination, Revised ID Needed, + External Party, AD Followup, Point Raised - Writeup Needed""" +class StdLevelName(NameModel): + """Proposed Standard, Draft Standard, Standard, Experimental, + Informational, Best Current Practice, Historic, ...""" +class IntendedStdLevelName(NameModel): + """Standards Track, Experimental, Informational, Best Current + Practice, Historic, ...""" +class BallotPositionName(NameModel): + """ Yes, NoObjection, Abstain, Discuss, Recuse """ +class SessionStatusName(NameModel): + """ Waiting for Approval, Approved, Waiting for Scheduling, Scheduled, Cancelled, Disapproved""" +class TimeSlotTypeName(NameModel): + """Session, Break, Registration""" +class ConstraintName(NameModel): + """Conflict""" + + +def get_next_iesg_states(iesg_state): + if not iesg_state: + return () + + next = { + "pub-req": ("ad-eval", "watching", "dead"), + "ad-eval": ("watching", "lc-req", "review-e", "iesg-eva"), + "review-e": ("ad-eval", ), + "lc-req": ("lc", ), + "lc": ("writeupw", "goaheadw"), + "writeupw": ("goaheadw", ), + "goaheadw": ("iesg-eva", ), + "iesg-eva": ("nopubadw", "defer", "ann"), + "defer": ("iesg-eva", ), + "ann": ("approved", ), + "approved": ("rfcqueue", ), + "rfcqueue": ("pub", ), + "pub": ("dead", ), + "nopubadw": ("nopubanw", ), + "nopubanw": ("dead", ), + "watching": ("pub-req", ), + "dead": ("pub-req", ), + } + + return IesgDocStateName.objects.filter(slug__in=next.get(iesg_state.slug, ())) + diff --git a/redesign/name/proxy.py b/redesign/name/proxy.py new file mode 100644 index 000000000..657e341f1 --- /dev/null +++ b/redesign/name/proxy.py @@ -0,0 +1,118 @@ +from redesign.proxy_utils import TranslatingManager +from models import * + +class IDStatus(DocStateName): + def from_object(self, base): + for f in base._meta.fields: + setattr(self, f.name, getattr(base, f.name)) + return self + + #status_id = models.AutoField(primary_key=True) + + #status = models.CharField(max_length=25, db_column='status_value') + @property + def status(self): + return self.name + + def __unicode__(self): + return super(self.__class__, self).__unicode__() + + class Meta: + proxy = True + +class IDState(IesgDocStateName): + PUBLICATION_REQUESTED = 10 + LAST_CALL_REQUESTED = 15 + IN_LAST_CALL = 16 + WAITING_FOR_WRITEUP = 18 + WAITING_FOR_AD_GO_AHEAD = 19 + IESG_EVALUATION = 20 + IESG_EVALUATION_DEFER = 21 + APPROVED_ANNOUNCEMENT_SENT = 30 + AD_WATCHING = 42 + DEAD = 99 + DO_NOT_PUBLISH_STATES = (33, 34) + + objects = TranslatingManager(dict(pk="order", + document_state_id="order", + document_state_id__in="order__in")) + + def from_object(self, base): + for f in base._meta.fields: + setattr(self, f.name, getattr(base, f.name)) + return self + + #document_state_id = models.AutoField(primary_key=True) + @property + def document_state_id(self): + return self.order + + #state = models.CharField(max_length=50, db_column='document_state_val') + @property + def state(self): + return self.name + + #equiv_group_flag = models.IntegerField(null=True, blank=True) # unused + #description = models.TextField(blank=True, db_column='document_desc') + @property + def description(self): + return self.desc + + @property + def nextstate(self): + # simulate related queryset + from name.models import get_next_iesg_states + return IDState.objects.filter(pk__in=[x.pk for x in get_next_iesg_states(self)]) + + @property + def next_state(self): + # simulate IDNextState + return self + + def __str__(self): + return self.state + + @staticmethod + def choices(): + return [(state.slug, state.name) for state in IDState.objects.all()] + + class Meta: + proxy = True + + +class IDSubStateManager(TranslatingManager): + def __init__(self, *args): + super(IDSubStateManager, self).__init__(*args) + + def all(self): + return self.filter(slug__in=['extpty', 'need-rev', 'ad-f-up', 'point']) + +class IDSubState(DocInfoTagName): + objects = IDSubStateManager(dict(pk="order")) + + def from_object(self, base): + for f in base._meta.fields: + setattr(self, f.name, getattr(base, f.name)) + return self + + #sub_state_id = models.AutoField(primary_key=True) + @property + def sub_state_id(self): + return self.order + + #sub_state = models.CharField(max_length=55, db_column='sub_state_val') + @property + def sub_state(self): + return self.name + + #description = models.TextField(blank=True, db_column='sub_state_desc') + @property + def description(self): + return self.desc + + def __str__(self): + return self.sub_state + + class Meta: + proxy = True + diff --git a/redesign/name/utils.py b/redesign/name/utils.py new file mode 100644 index 000000000..c4c47db94 --- /dev/null +++ b/redesign/name/utils.py @@ -0,0 +1,8 @@ +def name(name_class, slug, name, desc="", order=0): + # create if it doesn't exist, set name and desc + obj, _ = name_class.objects.get_or_create(slug=slug) + obj.name = name + obj.desc = desc + obj.order = order + obj.save() + return obj diff --git a/redesign/person/__init__.py b/redesign/person/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/redesign/person/admin.py b/redesign/person/admin.py new file mode 100644 index 000000000..8bc2b52bd --- /dev/null +++ b/redesign/person/admin.py @@ -0,0 +1,28 @@ +from django.contrib import admin +from models import * + +class EmailAdmin(admin.ModelAdmin): + list_display = ["address", "person", "time", "active", ] + raw_id_fields = ["person", ] + search_fields = ["address", "person__name", ] +admin.site.register(Email, EmailAdmin) + +class EmailInline(admin.TabularInline): + model = Email + +class AliasAdmin(admin.ModelAdmin): + list_display = ["name", "person", ] + search_fields = ["name",] + raw_id_fields = ["person"] +admin.site.register(Alias, AliasAdmin) + +class AliasInline(admin.StackedInline): + model = Alias + +class PersonAdmin(admin.ModelAdmin): + list_display = ["name", "short", "time", "user", ] + search_fields = ["name", "ascii"] + inlines = [ EmailInline, AliasInline, ] +# actions = None +admin.site.register(Person, PersonAdmin) + diff --git a/redesign/person/models.py b/redesign/person/models.py new file mode 100644 index 000000000..e2e177d6b --- /dev/null +++ b/redesign/person/models.py @@ -0,0 +1,103 @@ +# Copyright The IETF Trust 2007, All Rights Reserved + +from django.db import models +from django.contrib.auth.models import User + +class Person(models.Model): + time = models.DateTimeField(auto_now_add=True) # When this Person record entered the system + name = models.CharField(max_length=255, db_index=True) # The normal unicode form of the name. This must be + # set to the same value as the ascii-form if equal. + ascii = models.CharField(max_length=255) # The normal ascii-form of the name. + ascii_short = models.CharField(max_length=32, null=True, blank=True) # The short ascii-form of the name. Also in alias table if non-null + address = models.TextField(max_length=255, blank=True) + + user = models.OneToOneField(User, blank=True, null=True) + + def __unicode__(self): + return self.name + def _parts(self, name): + prefix, first, middle, last, suffix = "", "", "", "", "" + parts = name.split() + if parts[0] in ["Mr", "Mr.", "Mrs", "Mrs.", "Ms", "Ms.", "Miss", "Dr.", "Doctor", "Prof", "Prof.", "Professor", "Sir", "Lady", "Dame", ]: + prefix = parts[0]; + parts = parts[1:] + if len(parts) > 2: + if parts[-1] in ["Jr", "Jr.", "II", "2nd", "III", "3rd", ]: + suffix = parts[-1] + parts = parts[:-1] + if len(parts) > 2: + first = parts[0] + last = parts[-1] + middle = " ".join(parts[1:-1]) + elif len(parts) == 2: + first, last = parts + else: + last = parts[0] + return prefix, first, middle, last, suffix + def name_parts(self): + return self._parts(self.name) + def ascii_parts(self): + return self._parts(self.ascii) + def short(self): + if self.ascii_short: + return self.ascii_short + else: + prefix, first, middle, last, suffix = self.ascii_parts() + return (first and first[0]+"." or "")+(middle or "")+" "+last+(suffix and " "+suffix or "") + def role_email(self, role_name, group): + e = Email.objects.filter(person=self, role__group=group, role__name=role_name) + if e: + return e[0] + e = self.email_set.order_by("-active") + if e: + return e[0] + return None + def email_address(self): + e = self.email_set.filter(active=True) + if e: + return e[0] + else: + return "" + def formatted_email(self): + e = self.email_set.order_by("-active") + if e: + return e[0].formatted_email() + else: + return "" + def person(self): # little temporary wrapper to help porting + return self + def full_name_as_key(self): + return self.name.lower().replace(" ", ".") + + +class Alias(models.Model): + """This is used for alternative forms of a name. This is the + primary lookup point for names, and should always contain the + unicode form (and ascii form, if different) of a name which is + recorded in the Person record. + """ + person = models.ForeignKey(Person) + name = models.CharField(max_length=255, db_index=True) + def __unicode__(self): + return self.name + class Meta: + verbose_name_plural = "Aliases" + +class Email(models.Model): + address = models.CharField(max_length=64, primary_key=True) + person = models.ForeignKey(Person, null=True) + time = models.DateTimeField(auto_now_add=True) + active = models.BooleanField(default=True) # Old email addresses are *not* purged, as history + # information points to persons through these + def __unicode__(self): + return self.address + + def get_name(self): + return self.person.name if self.person else self.address + + def formatted_email(self): + if self.person and self.person.name: + return u'"%s" <%s>' % (self.person.name, self.address) + else: + return self.address + diff --git a/redesign/person/proxy.py b/redesign/person/proxy.py new file mode 100644 index 000000000..7fbeb9cc3 --- /dev/null +++ b/redesign/person/proxy.py @@ -0,0 +1,57 @@ +from redesign.proxy_utils import TranslatingManager + +from models import * + +class IESGLogin(Person): + objects = TranslatingManager(dict(user_level__in=None, + first_name="name" + )) + + def from_object(self, base): + for f in base._meta.fields: + setattr(self, f.name, getattr(base, f.name)) + return self + + SECRETARIAT_LEVEL = 0 + AD_LEVEL = 1 + INACTIVE_AD_LEVEL = 2 + + #login_name = models.CharField(blank=True, max_length=255) + @property + def login_name(self): raise NotImplemented + #password = models.CharField(max_length=25) + @property + def password(self): raise NotImplemented + #user_level = models.IntegerField(choices=USER_LEVEL_CHOICES) + @property + def user_level(self): raise NotImplemented + + #first_name = models.CharField(blank=True, max_length=25) + @property + def first_name(self): + return self.name_parts()[1] + + #last_name = models.CharField(blank=True, max_length=25) + @property + def last_name(self): + return self.name_parts()[3] + + # FIXME: person isn't wrapped yet + #person = BrokenForeignKey(PersonOrOrgInfo, db_column='person_or_org_tag', unique=True, null_values=(0, 888888), null=True) + + # apparently unused + #pgp_id = models.CharField(blank=True, null=True, max_length=20) + #default_search = models.NullBooleanField() + + def __str__(self): + return self.name + def __unicode__(self): + return self.name + def is_current_ad(self): + return self in Person.objects.filter(email__role__name="ad", email__role__group__state="active").distinct() + @staticmethod + def active_iesg(): + return IESGLogin.objects.filter(email__role__name="ad", email__role__group__state="active").distinct().order_by('name') + + class Meta: + proxy = True diff --git a/redesign/proxy_utils.py b/redesign/proxy_utils.py new file mode 100644 index 000000000..780e4a981 --- /dev/null +++ b/redesign/proxy_utils.py @@ -0,0 +1,259 @@ +from django.db.models.manager import Manager +from django.db.models.query import QuerySet + +class TranslatingQuerySet(QuerySet): + def translated_args(self, args): + trans = self.translated_attrs + res = [] + for a in args: + if a.startswith("-"): + prefix = "-" + a = a[1:] + else: + prefix = "" + + if a in trans: + t = trans[a] + if callable(t): + t, _ = t(None) + + if t: + res.append(prefix + t) + else: + res.append(prefix + a) + return res + + def translated_kwargs(self, kwargs): + trans = self.translated_attrs + res = dict() + for k, v in kwargs.iteritems(): + if k in trans: + t = trans[k] + if callable(t): + t, v = t(v) + + if t: + res[t] = v + else: + res[k] = v + return res + + # overridden methods + def _clone(self, *args, **kwargs): + c = super(TranslatingQuerySet, self)._clone(*args, **kwargs) + c.translated_attrs = self.translated_attrs + return c + + def dates(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).dates(*args, **kwargs) + + def distinct(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).distinct(*args, **kwargs) + + def extra(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).extra(*args, **kwargs) + + def get(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).get(*args, **kwargs) + + def get_or_create(self, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).get_or_create(**kwargs) + + def create(self, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).create(**kwargs) + + def filter(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).filter(*args, **kwargs) + + def aggregate(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).aggregate(*args, **kwargs) + + def annotate(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).annotate(*args, **kwargs) + + def complex_filter(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).complex_filter(*args, **kwargs) + + def exclude(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).exclude(*args, **kwargs) + + def in_bulk(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).in_bulk(*args, **kwargs) + + def iterator(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).iterator(*args, **kwargs) + + def latest(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).latest(*args, **kwargs) + + def order_by(self, *args, **kwargs): + args = self.translated_args(args) + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).order_by(*args, **kwargs) + + def select_related(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).select_related(*args, **kwargs) + + def values(self, *args, **kwargs): + args = self.translated_args(args) + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).values(*args, **kwargs) + + def values_list(self, *args, **kwargs): + args = self.translated_args(args) + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).values_list(*args, **kwargs) + + def update(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).update(*args, **kwargs) + + def reverse(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).reverse(*args, **kwargs) + + def defer(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).defer(*args, **kwargs) + + def only(self, *args, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self).only(*args, **kwargs) + + def _insert(self, values, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return insert_query(self.model, values, **kwargs) + + def _update(self, values, **kwargs): + kwargs = self.translated_kwargs(kwargs) + return super(TranslatingQuerySet, self)._update(values, **kwargs) + +class TranslatingManager(Manager): + """Translates keyword arguments for the ORM, for use in proxy + wrapping, e.g. given trans={'foo': 'bar'} it will transform a + lookup of the field foo to a lookup on the field bar. The right + hand side can either be a string or a function which is called + with the right-hand side to transform it.""" + + def __init__(self, trans, always_filter=None): + super(TranslatingManager, self).__init__() + self.translated_attrs = trans + self.always_filter = always_filter + + def get_query_set(self): + qs = TranslatingQuerySet(self.model) + qs.translated_attrs = self.translated_attrs + if self.always_filter: + qs = qs.filter(**self.always_filter) + return qs + + # def dates(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().dates(*args, **kwargs) + + # def distinct(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().distinct(*args, **kwargs) + + # def extra(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().extra(*args, **kwargs) + + # def get(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().get(*args, **kwargs) + + # def get_or_create(self, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().get_or_create(**kwargs) + + # def create(self, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().create(**kwargs) + + # def filter(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().filter(*args, **kwargs) + + # def aggregate(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().aggregate(*args, **kwargs) + + # def annotate(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().annotate(*args, **kwargs) + + # def complex_filter(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().complex_filter(*args, **kwargs) + + # def exclude(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().exclude(*args, **kwargs) + + # def in_bulk(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().in_bulk(*args, **kwargs) + + # def iterator(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().iterator(*args, **kwargs) + + # def latest(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().latest(*args, **kwargs) + + # def order_by(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().order_by(*args, **kwargs) + + # def select_related(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().select_related(*args, **kwargs) + + # def values(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().values(*args, **kwargs) + + # def values_list(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().values_list(*args, **kwargs) + + # def update(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().update(*args, **kwargs) + + # def reverse(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().reverse(*args, **kwargs) + + # def defer(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().defer(*args, **kwargs) + + # def only(self, *args, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set().only(*args, **kwargs) + + # def _insert(self, values, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return insert_query(self.model, values, **kwargs) + + # def _update(self, values, **kwargs): + # kwargs = self.translated_kwargs(kwargs) + # return self.get_query_set()._update(values, **kwargs) diff --git a/redesign/unaccent.py b/redesign/unaccent.py new file mode 100644 index 000000000..265e764de --- /dev/null +++ b/redesign/unaccent.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +# use a dynamically populated translation dictionary to remove accents +# from a string +# (by Chris Mulligan, http://chmullig.com/2009/12/python-unicode-ascii-ifier/) + +import unicodedata, sys + +class unaccented_map(dict): +# Translation dictionary. Translation entries are added to this dictionary as needed. + CHAR_REPLACEMENT = { + 0xc6: u"AE", # Æ LATIN CAPITAL LETTER AE + 0xd0: u"D", # Ð LATIN CAPITAL LETTER ETH + 0xd8: u"OE", # Ø LATIN CAPITAL LETTER O WITH STROKE + 0xde: u"Th", # Þ LATIN CAPITAL LETTER THORN + 0xc4: u'Ae', # Ä LATIN CAPITAL LETTER A WITH DIAERESIS + 0xd6: u'Oe', # Ö LATIN CAPITAL LETTER O WITH DIAERESIS + 0xdc: u'Ue', # Ü LATIN CAPITAL LETTER U WITH DIAERESIS + + 0xc0: u"A", # À LATIN CAPITAL LETTER A WITH GRAVE + 0xc1: u"A", # Á LATIN CAPITAL LETTER A WITH ACUTE + 0xc3: u"A", # Ã LATIN CAPITAL LETTER A WITH TILDE + 0xc7: u"C", # Ç LATIN CAPITAL LETTER C WITH CEDILLA + 0xc8: u"E", # È LATIN CAPITAL LETTER E WITH GRAVE + 0xc9: u"E", # É LATIN CAPITAL LETTER E WITH ACUTE + 0xca: u"E", # Ê LATIN CAPITAL LETTER E WITH CIRCUMFLEX + 0xcc: u"I", # Ì LATIN CAPITAL LETTER I WITH GRAVE + 0xcd: u"I", # Í LATIN CAPITAL LETTER I WITH ACUTE + 0xd2: u"O", # Ò LATIN CAPITAL LETTER O WITH GRAVE + 0xd3: u"O", # Ó LATIN CAPITAL LETTER O WITH ACUTE + 0xd5: u"O", # Õ LATIN CAPITAL LETTER O WITH TILDE + 0xd9: u"U", # Ù LATIN CAPITAL LETTER U WITH GRAVE + 0xda: u"U", # Ú LATIN CAPITAL LETTER U WITH ACUTE + + 0xdf: u"ss", # ß LATIN SMALL LETTER SHARP S + 0xe6: u"ae", # æ LATIN SMALL LETTER AE + 0xf0: u"d", # ð LATIN SMALL LETTER ETH + 0xf8: u"oe", # ø LATIN SMALL LETTER O WITH STROKE + 0xfe: u"th", # þ LATIN SMALL LETTER THORN, + 0xe4: u'ae', # ä LATIN SMALL LETTER A WITH DIAERESIS + 0xf6: u'oe', # ö LATIN SMALL LETTER O WITH DIAERESIS + 0xfc: u'ue', # ü LATIN SMALL LETTER U WITH DIAERESIS + + 0xe0: u"a", # à LATIN SMALL LETTER A WITH GRAVE + 0xe1: u"a", # á LATIN SMALL LETTER A WITH ACUTE + 0xe3: u"a", # ã LATIN SMALL LETTER A WITH TILDE + 0xe7: u"c", # ç LATIN SMALL LETTER C WITH CEDILLA + 0xe8: u"e", # è LATIN SMALL LETTER E WITH GRAVE + 0xe9: u"e", # é LATIN SMALL LETTER E WITH ACUTE + 0xea: u"e", # ê LATIN SMALL LETTER E WITH CIRCUMFLEX + 0xec: u"i", # ì LATIN SMALL LETTER I WITH GRAVE + 0xed: u"i", # í LATIN SMALL LETTER I WITH ACUTE + 0xf2: u"o", # ò LATIN SMALL LETTER O WITH GRAVE + 0xf3: u"o", # ó LATIN SMALL LETTER O WITH ACUTE + 0xf5: u"o", # õ LATIN SMALL LETTER O WITH TILDE + 0xf9: u"u", # ù LATIN SMALL LETTER U WITH GRAVE + 0xfa: u"u", # ú LATIN SMALL LETTER U WITH ACUTE + + 0x2018: u"'", # ‘ LEFT SINGLE QUOTATION MARK + 0x2019: u"'", # ’ RIGHT SINGLE QUOTATION MARK + 0x201c: u'"', # “ LEFT DOUBLE QUOTATION MARK + 0x201d: u'"', # ” RIGHT DOUBLE QUOTATION MARK + + } + + # Maps a unicode character code (the key) to a replacement code + # (either a character code or a unicode string). + def mapchar(self, key): + ch = self.get(key) + if ch is not None: + return ch + try: + de = unicodedata.decomposition(unichr(key)) + p1, p2 = [int(x, 16) for x in de.split(None, 1)] + if p2 == 0x308: + ch = self.CHAR_REPLACEMENT.get(key) + else: + ch = int(p1) + + except (IndexError, ValueError): + ch = self.CHAR_REPLACEMENT.get(key, key) + self[key] = ch + return ch + + if sys.version <= "2.5": + # use __missing__ where available + __missing__ = mapchar + else: + # otherwise, use standard __getitem__ hook (this is slower, + # since it's called for each character) + __getitem__ = mapchar + +map = unaccented_map() + +def asciify(input): + try: + return input.encode('ascii') + except AttributeError: + return str(input).encode('ascii') + except UnicodeEncodeError: + return unicodedata.normalize('NFKD', input.translate(map)).encode('ascii', 'replace') + +text = u""" + +##Norwegian +"Jo, når'n da ha gått ett stôck te, så kommer'n te e å, +å i åa ä e ö." +"Vasa", sa'n. +"Å i åa ä e ö", sa ja. +"Men va i all ti ä dä ni säjer, a, o?", sa'n. +"D'ä e å, vett ja", skrek ja, för ja ble rasen, "å i åa +ä e ö, hörer han lite, d'ä e å, å i åa ä e ö." +"A, o, ö", sa'n å dämmä geck'en. +Jo, den va nôe te dum den. + +(taken from the short story "Dumt fôlk" in Gustaf Fröding's +"Räggler å paschaser på våra mål tå en bonne" (1895). + +##Danish + +Nu bliver Mølleren sikkert sur, og dog, han er stadig den største på verdensplan. + +Userneeds A/S er en dansk virksomhed, der udfører statistiske undersøgelser på internettet. Den blev etableret i 2001 som et anpartsselskab af David Jensen og Henrik Vincentz. +Frem til 2004 var det primære fokus på at forbedre hjemmesiderne for andre virksomheder. Herefter blev fokus omlagt, så man også beskæftigede sig med statistiske målinger. Ledelsen vurderede, at dette marked ville vokse betragteligt i de kommende år, hvilket man ønskede at udnytte. +Siden omlægningen er der blevet fokuseret på at etablere meget store forbrugerpaneler. Således udgjorde det danske panel i 2005 65.000 personer og omfatter per 2008 100.000 personer. +I 2007 blev Userneeds ApS konverteret til aktieselskabet Userneeds A/S +Efterhånden er aktiviteterne blevet udvidet til de nordiske lande (med undtagelse af Island) og besidder i 2009 et forbrugerpanel med i alt mere end 250.000 personer bosat i de fire store nordiske lande. +Selskabet tegnes udadtil af en direktion på tre personer, der foruden Henrik Vincentz tæller Palle Viby Morgen og Simon Andersen. +De primære konkurrenter er andre analysebureauer som AC Nielsen, Analysedanmark, Gallup, Norstat, Synnovate og Zapera. + +##Finnish +Titus Aurelius Fulvus Boionius Arrius Antoninus eli Antoninus Pius (19. syyskuuta 86 – 7. maaliskuuta 161) oli Rooman keisari vuosina 138–161. Antoninus sai lisänimensä Pius (suom. velvollisuudentuntoinen) noustuaan valtaan vuonna 138. Hän kuului Nerva–Antoninusten hallitsijasukuun ja oli suosittu ja kunnioitettu keisari, joka tunnettiin lempeydestään ja oikeamielisyydestään. Hänen valtakauttaan on usein sanottu Rooman valtakunnan kultakaudeksi, jolloin talous kukoisti, poliittinen tilanne oli vakaa ja armeija vahva. Hän hallitsi pitempään kuin yksikään Rooman keisari Augustuksen jälkeen, ja hänen kautensa tunnetaan erityisen rauhallisena, joskaan ei sodattomana. Antoninus adoptoi Marcus Aureliuksen ja Lucius Veruksen vallanperijöikseen. Hän kuoli vuonna 161. + +#German +So heißt ein altes Märchen: "Der Ehre Dornenpfad", und es handelt von einem Schützen mit Namen Bryde, der wohl zu großen Ehren und Würden kam, aber nicht ohne lange und vielfältige Widerwärtigkeiten und Fährnisse des Lebens durchzumachen. Manch einer von uns hat es gewiß als Kind gehört oder es vielleicht später gelesen und dabei an seinen eigenen stillen Dornenweg und die vielen Widerwärtigkeiten gedacht. Märchen und Wirklichkeit liegen einander so nahe, aber das Märchen hat seine harmonische Lösung hier auf Erden, während die Wirklichkeit sie meist aus dem Erdenleben hinaus in Zeit und Ewigkeit verlegt. + +12\xbd inch +""" + +if __name__ == "__main__": + for i, line in enumerate(text.splitlines()): + line = line.strip() + print line + if line and not line.startswith('#'): + print '\tTrans: ', asciify(line).strip() diff --git a/redesign/util.py b/redesign/util.py new file mode 100644 index 000000000..8e24e462a --- /dev/null +++ b/redesign/util.py @@ -0,0 +1,45 @@ + +def name(obj): + if hasattr(obj, 'abbrev'): + return obj.abbrev() + elif hasattr(obj, 'name'): + if callable(obj.name): + return obj.name() + else: + return unicode(obj.name) + else: + return unicode(obj) + +def admin_link(field, label=None, ordering="", display=name, suffix=""): + if not label: + label = field.capitalize().replace("_", " ").strip() + if ordering == "": + ordering = field + def _link(self): + obj = self + for attr in field.split("__"): + obj = getattr(obj, attr) + if callable(obj): + obj = obj() + if hasattr(obj, "all"): + objects = obj.all() + elif callable(obj): + objects = obj() + if not hasattr(objects, "__iter__"): + objects = [ objects ] + elif hasattr(obj, "__iter__"): + objects = obj + else: + objects = [ obj ] + chunks = [] + for obj in objects: + app = obj._meta.app_label + model = obj.__class__.__name__.lower() + id = obj.pk + chunks += [ u'<a href="/admin/%(app)s/%(model)s/%(id)s/%(suffix)s">%(display)s</a>' % + {'app':app, "model": model, "id":id, "display": display(obj), "suffix":suffix, } ] + return u", ".join(chunks) + _link.allow_tags = True + _link.short_description = label + _link.admin_order_field = ordering + return _link diff --git a/static/css/base2.css b/static/css/base2.css index a00ab69a4..8dc4541d8 100644 --- a/static/css/base2.css +++ b/static/css/base2.css @@ -111,6 +111,7 @@ table.ietf-table { border-collapse:collapse; border:1px solid #7f7f7f; } .ietf-doctable tr.header { border-top: 1px solid #7f7f7f; border-bottom: 1px solid #7f7f7f; border-left: 1px solid white; border-right:2px solid white;} .ietf-doctable tr.header td {padding: 6px 6px; font-weight: bold; } .ietf-doctable table { max-width: 1200px; } +.ietf-doctable th { cursor: pointer } .ietf-doctable th.doc, .ietf-doctable td.doc { min-width:20em; max-width: 35em; } .ietf-doctable th.title, .ietf-doctable td.title { min-width: 20em; max-width: 35em; } .ietf-doctable th.date, .ietf-doctable td.date { white-space:nowrap; min-width: 6em;}