Move WG edit and conclude views from wgcharter to wginfo since they

aren't dealing with charters directly, clean up and add some polish
and fix a bunch of refactoring bugs. Start on fixing tests.
 - Legacy-Id: 4169
This commit is contained in:
Ole Laursen 2012-03-22 18:36:04 +00:00
parent 32659ed957
commit 2fea753f51
22 changed files with 1124 additions and 728 deletions

View file

@ -1,25 +1,32 @@
{# Copyright The IETF Trust 2011, All Rights Reserved #}
{% load ietf_filters %}
{{ obj.info.text|safe }}<br/>
<br/>
<p>{{ obj.info.text|safe|linebreaksbr }}</p>
{% with obj.group as wg %}
WG name: {{ wg.name }}<br/>
WG acronym: {{ wg.acronym }}<br/>
IETF area: {{ wg.parent|default:"-" }}<br/>
<p>
WG name: {{ wg.name }}<br/>
WG acronym: {{ wg.acronym }}<br/>
IETF area: {{ wg.parent|default:"-" }}<br/>
</p>
WG chairs: {% for n in obj.chairs %}{{ n }}{% if not forloop.last %}, {% endif %}{% endfor %}<br/>
WG secretaries: {% for n in obj.secr %}{{ n }}{% if not forloop.last %}, {% endif %}{% endfor %}<br/>
WG technical advisors: {% for n in obj.techadv %}{{ n }}{% if not forloop.last %}, {% endif %}{% endfor %}<br/>
Assigned AD: {{ wg.ad }}<br/>
<p>
WG chairs: {% for n in obj.chairs %}{{ n }}{% if not forloop.last %}, {% endif %}{% endfor %}<br/>
WG secretaries: {% for n in obj.secr %}{{ n }}{% if not forloop.last %}, {% endif %}{% endfor %}<br/>
WG technical advisors: {% for n in obj.techadv %}{{ n }}{% if not forloop.last %}, {% endif %}{% endfor %}<br/>
Assigned AD: {{ wg.ad }}<br/>
</p>
Mailing list: {{ wg.list_email }}<br/>
Mailing list subscribe {{ wg.list_subscribe }}<br/>
Mailing list archive: {{ wg.list_archive }}<br/>
Other web sites: {% for a in wg.groupurl_set.all %}{{ a.url }} {% if a.name %}({{ a.name }}){% endif %}{% if not forloop.last %}, {% endif %}{% endfor %}<br/>
<p>
Mailing list: {{ wg.list_email }}<br/>
Mailing list subscribe {{ wg.list_subscribe }}<br/>
Mailing list archive: {{ wg.list_archive }}<br/>
Other web sites: {% for a in wg.groupurl_set.all %}{{ a.url }} {% if a.name %}({{ a.name }}){% endif %}{% if not forloop.last %}, {% endif %}{% endfor %}<br/>
</p>
WG State: {{ wg.state|safe }}<br/>
Charter State: {{ wg.charter.charter_state|safe }}<br/>
<br/>
<p>
WG State: {{ wg.state|safe }}<br/>
Charter State: {% if wg.charter %}{{ wg.charter.get_state|safe }}{% endif %}<br/>
</p>
{% if obj.rev %}
{{ obj.charter|safe }}
{% else %}

View file

@ -46,9 +46,7 @@
</td>
</tr>
{% if chartering %}
{% if doc.group.comments %}
{% if chartering and doc.group.comments %}
<tr>
{% if chartering == "initial" %}<td>Reason for chartering:</td>{% endif %}
{% if chartering == "rechartering" %}<td>Reason for rechartering:</td>{% endif %}
@ -72,28 +70,28 @@
<tr><td>Last updated:</td><td> {{ doc.time|date:"Y-m-d" }}</td></tr>
{% endif %}
<tr><td colspan='2'><hr size='1' noshade /></td></tr>
</table>
{% if not snapshot and user|has_role:"Area Director,Secretariat" %}
<div class="actions">
{% if not snapshot and user|has_role:"Area Director,Secretariat" %}
{% if chartering %}
{% if chartering %}
{% if doc.group.state_id != "conclude" %}
<a href="{% url wg_submit name=doc.group.acronym %}">Edit charter</a>
{% endif %}
{% if doc.group.state_id != "conclude" %}
<a href="{% url wg_submit name=doc.group.acronym %}">Edit charter text</a>
{% endif %}
<a href="{% url wg_startstop_process name=doc.group.acronym option='abandon' %}">Abandon effort</a>
<a href="{% url wg_startstop_process name=doc.group.acronym option='abandon' %}">Abandon effort</a>
{% else %}
<a href="{% url wg_startstop_process name=doc.group.acronym option='recharter' %}">Recharter</a>
{% endif %}
{% else %}
<a href="{% url wg_startstop_process name=doc.group.acronym option='recharter' %}">Recharter</a>
{% endif %}
{% endif %}
{% endif %}
<div><a href="/feed/wgcomments/{{ doc.group.acronym }}/">Atom feed</a></div>
<a href="/feed/wgcomments/{{ doc.group.acronym }}/">Atom feed</a>
</div>
</div>
<p>Other versions: <a href="{{ txt_url }}">plain text</a></p>

View file

@ -1,36 +0,0 @@
{% extends "base.html" %}
{% comment %}
Copyright The IETF Trust 2011, All Rights Reserved
{% endcomment %}
{% block title %}Add comment on {{ wg.acronym }}{% endblock %}
{% block morecss %}
form.add-comment #id_comment {
width: 600px;
height: 300px;
}
form.add-comment .actions {
padding-top: 20px;
}
{% endblock %}
{% block content %}
<h1>Add comment on {{ wg.acronym }}</h1>
<p>The comment will be added to the history trail.</p>
<form class="add-comment" action="" method="POST">
<table>
{{ form.as_table }}
<tr>
<td></td>
<td class="actions">
<a href="{% url wg_view name=wg.acronym %}">Back</a>
<input type="submit" value="Add comment"/>
</td>
</tr>
</table>
</form>
{% endblock %}

View file

@ -1,5 +1,5 @@
{% autoescape off %}
{{ text }}
WG Record URL: {{ url }}
WG URL: {{ url }}
{% endautoescape %}

View file

@ -2,7 +2,7 @@
{% block title %}
{% if wg %}
Edit info on {{ wg.acronym }}
Edit WG {{ wg.acronym }}
{% else %}
Create WG
{% endif %}
@ -42,7 +42,7 @@ form.edit-info #id_comments {
{% block content %}
{% load ietf_filters %}
<h1>{% if wg %}
Edit info on {{ wg.acronym }}
Edit WG {{ wg.acronym }}
{% else %}
Create WG
{% endif %}
@ -55,7 +55,7 @@ Create WG
<th>{{ field.label_tag }}:</th>
<td>{{ field }}
{% ifequal field.name "ad" %}
{% if user|in_group:"Area_Director" %}
{% if user|has_role:"Area Director" %}
<label><input type="checkbox" name="ad" value="{{ login.pk }}" /> Assign to me</label>
{% endif %}
{% endifequal %}
@ -68,7 +68,7 @@ Create WG
<td></td>
<td class="actions">
{% if wg %}
<a href="{% url wg_view name=wg.acronym %}">Back</a>
<a href="{% url wg_charter acronym=wg.acronym %}">Back</a>
<input type="submit" value="Save"/>
{% else %}
<input type="submit" value="Create"/>
@ -79,24 +79,8 @@ Create WG
</form>
{% endblock %}
{% block scripts %}
$(document).ready(function () {
var chairs = eval($("#id_chairs").val()),
secretaries = eval($("#id_secretaries").val()),
techadv = eval($("#id_techadv").val());
$("#id_chairs").tokenInput("/wgcharter/searchPerson/", { hintText: "",
preventDuplicates: true,
prePopulate: chairs });
$("#id_secretaries").tokenInput("/wgcharter/searchPerson/", { hintText: "",
preventDuplicates: true,
prePopulate: secretaries });
$("#id_techadv").tokenInput("/wgcharter/searchPerson/", { hintText: "",
preventDuplicates: true,
prePopulate: techadv });
$("#id_name").focus();
});
{% endblock %}
{% block content_end %}
<script type="text/javascript" src="/js/lib/jquery.tokeninput.js"></script>
<script type="text/javascript" src="/js/lib/json2.js"></script>
<script type="text/javascript" src="/js/emails-field.js"></script>
{% endblock %}

View file

@ -4,7 +4,7 @@ from django.conf import settings
from django.contrib.syndication.feeds import Feed, FeedDoesNotExist
from django.utils.feedgenerator import Atom1Feed
from django.core.exceptions import ObjectDoesNotExist
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.history import find_history_active_at
from ietf.group.models import Group
@ -19,16 +19,16 @@ class GroupComments(Feed):
description_template = "feeds/wg_charter_description.html"
def get_object(self, bits):
if len(bits) != 1:
raise ObjectDoesNotExist
raise Group.DoesNotExist
return Group.objects.get(acronym=bits[0])
def title(self, obj):
return "WG Record changes for %s" % obj.acronym
return "WG changes for %s" % obj.acronym
def link(self, obj):
if obj is None:
if not obj:
raise FeedDoesNotExist
return reverse('wg_view', kwargs={'name': obj.acronym})
return urlreverse('wg_charter', kwargs={'acronym': obj.acronym})
def description(self, obj):
return self.title(obj)
@ -59,7 +59,7 @@ class GroupComments(Feed):
return history
def item_link(self, obj):
return reverse('wg_view', kwargs={'name': obj['group'].acronym})
return urlreverse('wg_charter', kwargs={'acronym': obj['group'].acronym})
def item_pubdate(self, obj):
return obj['date']

View file

@ -14,26 +14,26 @@ from ietf.ipr.search import iprs_from_docs
from ietf.doc.models import WriteupDocEvent, DocAlias, GroupBallotPositionDocEvent
from ietf.person.models import Person
# These become part of the subject of the email
types = {}
types['state'] = "State changed"
types['state-notrev'] = "State changed to Not currently under review"
types['state-infrev'] = "State changed to Informal review"
types['state-intrev'] = "State changed to Internal review"
types['state-extrev'] = "State changed to External review"
types['state-iesgrev'] = "State changed to IESG review"
types['state-approved'] = "Charter approved"
types['conclude'] = "Request closing of WG"
def email_secretariat(request, wg, type, text):
to = ["iesg-secretary@ietf.org"]
types = {}
types['state'] = "State changed"
types['state-notrev'] = "State changed to Not currently under review"
types['state-infrev'] = "State changed to Informal review"
types['state-intrev'] = "State changed to Internal review"
types['state-extrev'] = "State changed to External review"
types['state-iesgrev'] = "State changed to IESG review"
types['state-approved'] = "Charter approved"
types['conclude'] = "Request closing of WG"
subject = u"Regarding WG %s: %s" % (wg.acronym, types[type])
text = strip_tags(text)
send_mail(request, to, None,
"Regarding WG %s: %s" % (wg.acronym, types[type]),
send_mail(request, to, None, subject,
"wgcharter/email_secretariat.txt",
dict(text=text,
url=settings.IDTRACKER_BASE_URL + urlreverse('wg_view', kwargs=dict(name=wg.acronym))))
url=settings.IDTRACKER_BASE_URL + urlreverse('wg_charter', kwargs=dict(acronym=wg.acronym))))
def generate_ballot_writeup(request, doc):
e = WriteupDocEvent()

View file

@ -5,6 +5,7 @@ from StringIO import StringIO
import django.test
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.mail import outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
@ -120,34 +121,6 @@ class WgStateTestCase(django.test.TestCase):
else:
self.assertTrue("State changed" in outbox[-1]['Subject'])
def test_conclude(self):
make_test_data()
# And make a charter for group
group = Group.objects.get(acronym="mars")
# -- Test conclude WG --
url = urlreverse('wg_conclude', kwargs=dict(name=group.acronym))
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=instructions]')), 1)
# faulty post
r = self.client.post(url, dict(instructions="")) # No instructions
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
# conclusion request
r = self.client.post(url, dict(instructions="Test instructions"))
self.assertEquals(r.status_code, 302)
# The WG remains active until the state is set to conclude via change_state
group = Group.objects.get(acronym=group.acronym)
self.assertEquals(group.state_id, "active")
class WgInfoTestCase(django.test.TestCase):
fixtures = ['names']
@ -160,106 +133,6 @@ class WgInfoTestCase(django.test.TestCase):
def tearDown(self):
shutil.rmtree(self.charter_dir)
def test_create(self):
make_test_data()
# -- Test WG creation --
url = urlreverse('wg_create')
login_testing_unauthorized(self, "secretary", url)
num_wgs = len(Group.objects.filter(type="wg"))
# normal get
r = self.client.get(url)
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertEquals(len(q('form input[name=acronym]')), 1)
# faulty post
r = self.client.post(url, dict(acronym="foobarbaz")) # No name
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertEquals(len(Group.objects.filter(type="wg")), num_wgs)
# creation
r = self.client.post(url, dict(acronym="testwg", name="Testing WG"))
self.assertEquals(r.status_code, 302)
self.assertEquals(len(Group.objects.filter(type="wg")), num_wgs + 1)
group = Group.objects.get(acronym="testwg")
self.assertEquals(group.name, "Testing WG")
# check that a charter was created with the correct name
self.assertEquals(group.charter.name, "charter-ietf-testwg")
# and that it has no revision
self.assertEquals(group.charter.rev, "")
def test_edit_info(self):
make_test_data()
# And make a charter for group
group = Group.objects.get(acronym="mars")
url = urlreverse('wg_edit_info', kwargs=dict(name=group.acronym))
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=parent]')), 1)
self.assertEquals(len(q('form input[name=acronym]')), 1)
# faulty post
Group.objects.create(name="Collision Test Group", acronym="collide")
r = self.client.post(url, dict(acronym="collide"))
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
# Create old acronym
group.acronym = "oldmars"
group.save()
save_group_in_history(group)
group.acronym = "mars"
group.save()
# post with warning
r = self.client.post(url, dict(acronym="oldmars"))
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
# edit info
area = group.parent
ad = Person.objects.get(name="Aread Irector")
r = self.client.post(url,
dict(name="Mars Not Special Interest Group",
acronym="mnsig",
parent=area.pk,
ad=ad.pk,
chairs="aread@ietf.org, ad1@ietf.org",
secretaries="aread@ietf.org, ad1@ietf.org, ad2@ietf.org",
techadv="aread@ietf.org",
list_email="mars@mail",
list_subscribe="subscribe.mars",
list_archive="archive.mars",
urls="http://mars.mars (MARS site)"
))
self.assertEquals(r.status_code, 302)
group = Group.objects.get(acronym="mnsig")
self.assertEquals(group.name, "Mars Not Special Interest Group")
self.assertEquals(group.parent, area)
self.assertEquals(group.ad, ad)
for k in ("chair", "secr", "techadv"):
self.assertTrue(group.role_set.filter(name=k, email__address="aread@ietf.org"))
self.assertEquals(group.list_email, "mars@mail")
self.assertEquals(group.list_subscribe, "subscribe.mars")
self.assertEquals(group.list_archive, "archive.mars")
self.assertEquals(group.groupurl_set.all()[0].url, "http://mars.mars")
self.assertEquals(group.groupurl_set.all()[0].name, "MARS site")
def test_edit_telechat_date(self):
make_test_data()
@ -324,31 +197,6 @@ class WgInfoTestCase(django.test.TestCase):
self.assertEquals(charter.rev, next_revision(prev_rev))
self.assertTrue("new_revision" in charter.latest_event().type)
class WgAddCommentTestCase(django.test.TestCase):
fixtures = ['names']
def test_add_comment(self):
make_test_data()
group = Group.objects.get(acronym="mars")
url = urlreverse('wg_add_comment', kwargs=dict(name=group.acronym))
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
comments_before = group.groupevent_set.filter(type="added_comment").count()
r = self.client.post(url, dict(comment="This is a test."))
self.assertEquals(r.status_code, 302)
self.assertEquals(group.groupevent_set.filter(type="added_comment").count(), comments_before + 1)
self.assertTrue("This is a test." in group.groupevent_set.filter(type="added_comment").order_by('-time')[0].desc)
class WgEditPositionTestCase(django.test.TestCase):
fixtures = ['names', 'ballot']

View file

@ -1,22 +1,18 @@
# Copyright The IETF Trust 2011, All Rights Reserved
from django.conf.urls.defaults import patterns, url
from ietf.wgcharter import views, views_search, views_edit, views_ballot, views_submit
from ietf.wgcharter import views, views_edit, views_ballot, views_submit
from ietf.doc.models import State
urlpatterns = patterns('django.views.generic.simple',
url(r'^help/state/$', 'direct_to_template', { 'template': 'wgcharter/states.html', 'extra_context': { 'states': State.objects.filter(type="charter") } }, name='help_charter_states'),
)
urlpatterns += patterns('',
url(r'^create/$', views_edit.edit_info, name="wg_create"),
(r'^searchPerson/$', views.search_person), # FIXME: fix URL
url(r'^(?P<name>[A-Za-z0-9._+-]+)/((?P<rev>[0-9][0-9](-[0-9][0-9])?)/)?((?P<tab>ballot|writeup|history)/)?$', views.wg_main, name="wg_view"),
(r'^(?P<name>[A-Za-z0-9._+-]+)/_ballot.data$', views.wg_ballot),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/state/$', views_edit.change_state, name='wg_change_state'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/(?P<option>initcharter|recharter|abandon)/$', views_edit.change_state, name='wg_startstop_process'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/telechat/$', views_edit.telechat_date, name='charter_telechat_date'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/info/$', views_edit.edit_info, name='wg_edit_info'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/conclude/$', views_edit.conclude, name='wg_conclude'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/(?P<ann>action|review)/$', views_ballot.announcement_text, name='wg_announcement_text'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/position/$', views_ballot.edit_position, name='wg_edit_position'),
url(r'^(?P<name>[A-Za-z0-9._+-]+)/edit/sendballotcomment/$', views_ballot.send_ballot_comment, name='wg_send_ballot_comment'),

View file

@ -29,47 +29,6 @@ def set_or_create_charter(wg):
wg.save()
return charter
def save_charter_in_history(charter):
'''This is a modified save_document_in_history that save the name
as charter-ietf-wgacronym with wgacronym being the current Group
acronym. The charter Document may have an old name which is no longer
in use'''
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(charter)
fields["doc"] = charter
fields["name"] = 'charter-ietf-%s' % charter.chartered_group.acronym
chist = DocHistory(**fields)
chist.save()
# copy many to many
for field in charter._meta.many_to_many:
if field.rel.through and field.rel.through._meta.auto_created:
setattr(chist, field.name, getattr(charter, field.name).all())
# copy remaining tricky many to many
def transfer_fields(obj, HistModel):
mfields = get_model_fields_as_dict(item)
# map charter -> chist
for k, v in mfields.iteritems():
if v == charter:
mfields[k] = chist
HistModel.objects.create(**mfields)
for item in RelatedDocument.objects.filter(source=charter):
transfer_fields(item, RelatedDocHistory)
for item in DocumentAuthor.objects.filter(document=charter):
transfer_fields(item, DocHistoryAuthor)
return chist
def add_wg_comment(request, wg, text, ballot=None):
if request:
login = request.user.get_profile()
@ -103,13 +62,6 @@ def log_group_state_changed(request, wg, by, note=''):
e.save()
return e
def log_info_changed(request, wg, by, change):
e = GroupEvent(group=wg, by=by)
e.type = "info_changed"
e.desc = change
e.save()
return e
def get_charter_for_revision(charter, r):
if r == None:
return None

View file

@ -243,14 +243,3 @@ def wg_ballot(request, name):
info['pos_no_record'] = no_record
return render_to_response('wgcharter/wg_ballot.html', {'info':info, 'wg':wg, 'doc': doc}, context_instance=RequestContext(request))
def json_emails(l):
result = []
for p in l:
result.append({"id": p.address + "", "name": p.person.plain_name() + " &lt;" + p.address + "&gt;"})
return simplejson.dumps(result)
def search_person(request):
if request.method == 'GET':
emails = Email.objects.filter(person__name__istartswith=request.GET.get('q','')).order_by('person__name')
return HttpResponse(json_emails(emails), mimetype='application/json')

View file

@ -14,82 +14,78 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
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 mails import email_secretariat, generate_ballot_writeup, generate_issue_ballot_mail
from utils import *
from ietf.ietfauth.decorators import has_role, role_required
from ietf.wgcharter.mails import email_secretariat, generate_ballot_writeup, generate_issue_ballot_mail
from ietf.wgcharter.utils import *
from ietf.group.models import Group, GroupHistory, GroupEvent
from ietf.group.utils import save_group_in_history
from ietf.name.models import GroupBallotPositionName, GroupStateName
from ietf.doc.models import *
def default_action_text(wg, charter, user, action):
e = WriteupDocEvent(doc=charter, by=user)
e.by = user
e.type = "changed_action_announcement"
e.desc = "WG action text was changed"
e = WriteupDocEvent(doc=charter, by=user)
e.by = user
e.type = "changed_action_announcement"
e.desc = "WG action text was changed"
info = {}
info['chairs'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Chair")]
info['secr'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Secr")]
info['techadv'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Techadv")]
info['ad'] = {'name': wg.ad.plain_name(), 'email': wg.ad.role_email("ad").address } if wg.ad else None,
info['list'] = wg.list_email if wg.list_email else None,
info['list_subscribe'] = str(wg.list_subscribe) if wg.list_subscribe else None,
info['list_archive'] = str(wg.list_archive) if wg.list_archive else None,
info = {}
info['chairs'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Chair")]
info['secr'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Secr")]
info['techadv'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Techadv")]
info['ad'] = {'name': wg.ad.plain_name(), 'email': wg.ad.role_email("ad").address } if wg.ad else None,
info['list'] = wg.list_email if wg.list_email else None,
info['list_subscribe'] = str(wg.list_subscribe) if wg.list_subscribe else None,
info['list_archive'] = str(wg.list_archive) if wg.list_archive else None,
filename = os.path.join(settings.CHARTER_PATH, 'charter-ietf-%s-%s.txt' % (wg.acronym, wg.charter.rev))
try:
charter_text = open(filename, 'r')
info['charter_txt'] = charter_text.read()
except IOError:
info['charter_txt'] = "Error: couldn't read charter text"
filename = os.path.join(settings.CHARTER_PATH, 'charter-ietf-%s-%s.txt' % (wg.acronym, wg.charter.rev))
try:
charter_text = open(filename, 'r')
info['charter_txt'] = charter_text.read()
except IOError:
info['charter_txt'] = "Error: couldn't read charter text"
e.text = render_to_string("wgcharter/action_text.txt",
dict(wg=wg,
charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(),
action_type=action,
info=info,
)
)
e.text = render_to_string("wgcharter/action_text.txt",
dict(wg=wg,
charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(),
action_type=action,
info=info,
))
e.save()
return e
e.save()
return e
def default_review_text(wg, charter, user):
e = WriteupDocEvent(doc=charter, by=user)
e.by = user
e.type = "changed_review_announcement"
e.desc = "WG review text was changed"
info = {}
info['chairs'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Chair")]
info['secr'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Secr")]
info['techadv'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Techadv")]
info['ad'] = {'name': wg.ad.plain_name(), 'email': wg.ad.role_email("ad").address } if wg.ad else None,
info['list'] = wg.list_email if wg.list_email else None,
info['list_subscribe'] = wg.list_subscribe if wg.list_subscribe else None,
info['list_archive'] = wg.list_archive if wg.list_archive else None,
e = WriteupDocEvent(doc=charter, by=user)
e.by = user
e.type = "changed_review_announcement"
e.desc = "WG review text was changed"
info = {}
info['chairs'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Chair")]
info['secr'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Secr")]
info['techadv'] = [{ 'name': x.person.plain_name(), 'email': x.email.address} for x in wg.role_set.filter(name="Techadv")]
info['ad'] = {'name': wg.ad.plain_name(), 'email': wg.ad.role_email("ad").address } if wg.ad else None,
info['list'] = wg.list_email if wg.list_email else None,
info['list_subscribe'] = wg.list_subscribe if wg.list_subscribe else None,
info['list_archive'] = wg.list_archive if wg.list_archive else None,
info['bydate'] = (date.today() + timedelta(weeks=1)).isoformat()
info['bydate'] = (date.today() + timedelta(weeks=1)).isoformat()
filename = os.path.join(settings.CHARTER_PATH, 'charter-ietf-%s-%s.txt' % (wg.acronym, wg.charter.rev))
try:
charter_text = open(filename, 'r')
info['charter_txt'] = charter_text.read()
except IOError:
info['charter_txt'] = "Error: couldn't read charter text"
filename = os.path.join(settings.CHARTER_PATH, 'charter-ietf-%s-%s.txt' % (wg.acronym, wg.charter.rev))
try:
charter_text = open(filename, 'r')
info['charter_txt'] = charter_text.read()
except IOError:
info['charter_txt'] = "Error: couldn't read charter text"
e.text = render_to_string("wgcharter/review_text.txt",
dict(wg=wg,
charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(),
info=info,
review_type="new" if wg.state_id == "proposed" else "recharter",
)
)
e.save()
return e
e.text = render_to_string("wgcharter/review_text.txt",
dict(wg=wg,
charter_url=settings.IDTRACKER_BASE_URL + charter.get_absolute_url(),
info=info,
review_type="new" if wg.state_id == "proposed" else "recharter",
)
)
e.save()
return e
BALLOT_CHOICES = (("yes", "Yes"),
("no", "No"),
@ -114,13 +110,13 @@ class EditPositionForm(forms.Form):
return_to_url = forms.CharField(required=False, widget=forms.HiddenInput)
def clean_blocking(self):
entered_blocking = self.cleaned_data["block_comment"]
entered_pos = self.cleaned_data["position"]
if entered_pos.slug == "block" and not entered_blocking:
raise forms.ValidationError("You must enter a non-empty blocking comment")
return entered_blocking
entered_blocking = self.cleaned_data["block_comment"]
entered_pos = self.cleaned_data["position"]
if entered_pos.slug == "block" and not entered_blocking:
raise forms.ValidationError("You must enter a non-empty blocking comment")
return entered_blocking
@group_required('Area_Director','Secretariat')
@role_required('Area Director','Secretariat')
def edit_position(request, name):
"""Vote and edit comments on Charter as Area Director."""
try:
@ -135,7 +131,7 @@ def edit_position(request, name):
charter = set_or_create_charter(wg)
started_process = charter.latest_event(type="started_iesg_process")
if not started_process:
raise Http404
raise Http404
ad = login = request.user.get_profile()
@ -157,12 +153,12 @@ def edit_position(request, name):
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']
return_to_url = clean['return_to_url']
pos = GroupBallotPositionDocEvent(doc=charter, by=login)
pos.type = "changed_ballot_position"
@ -219,7 +215,7 @@ def edit_position(request, name):
for e in added_events:
e.save() # save them after the position is saved to get later id
charter.time = pos.time
charter.save()
@ -252,7 +248,7 @@ def edit_position(request, name):
),
context_instance=RequestContext(request))
@group_required('Area_Director','Secretariat')
@role_required('Area Director','Secretariat')
def send_ballot_comment(request, name):
"""Email Charter ballot comment for area director."""
try:
@ -311,13 +307,13 @@ def send_ballot_comment(request, name):
dict(block_comment=d, comment=c, ad=ad.plain_name(), charter=charter, 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()]
send_mail_text(request, to, frm, subject, body, cc=", ".join(cc))
return HttpResponseRedirect(return_to_url)
return render_to_response('wgcharter/send_ballot_comment.html',
dict(charter=charter,
subject=subject,
@ -327,16 +323,16 @@ def send_ballot_comment(request, name):
ad=ad,
can_send=d or c,
back_url=back_url,
),
),
context_instance=RequestContext(request))
class AnnouncementTextForm(forms.Form):
announcement_text = forms.CharField(widget=forms.Textarea, required=True)
def clean_announcement_text(self):
return self.cleaned_data["announcement_text"].replace("\r", "")
@group_required('Area_Director','Secretariat')
@role_required('Area Director','Secretariat')
def announcement_text(request, name, ann):
"""Editing of announcement text"""
try:
@ -358,10 +354,10 @@ def announcement_text(request, name, ann):
existing = charter.latest_event(WriteupDocEvent, type="changed_review_announcement")
if not existing:
if ann == "action":
if next_approved_revision(wg.charter.rev) == "01":
existing = default_action_text(wg, charter, login, "Formed")
else:
existing = default_action_text(wg, charter, login, "Rechartered")
if next_approved_revision(wg.charter.rev) == "01":
existing = default_action_text(wg, charter, login, "Formed")
else:
existing = default_action_text(wg, charter, login, "Rechartered")
elif ann == "review":
existing = default_review_text(wg, charter, login)
@ -381,28 +377,28 @@ def announcement_text(request, name, ann):
charter.time = e.time
charter.save()
return redirect('wg_view', name=wg.acronym)
return redirect('doc_writeup', name=charter.name)
if "regenerate_text" in request.POST:
if ann == "action":
if next_approved_revision(wg.charter.rev) == "01":
e = default_action_text(wg, charter, login, "Formed")
else:
e = default_action_text(wg, charter, login, "Rechartered")
elif ann == "review":
e = default_review_text(wg, charter, login)
# make sure form has the updated text
form = AnnouncementTextForm(initial=dict(announcement_text=e.text))
if ann == "action":
if next_approved_revision(wg.charter.rev) == "01":
e = default_action_text(wg, charter, login, "Formed")
else:
e = default_action_text(wg, charter, login, "Rechartered")
elif ann == "review":
e = default_review_text(wg, charter, login)
# make sure form has the updated text
form = AnnouncementTextForm(initial=dict(announcement_text=e.text))
if "send_text" in request.POST and form.is_valid():
msg = form.cleaned_data['announcement_text']
import email
parsed_msg = email.message_from_string(msg.encode("utf-8"))
msg = form.cleaned_data['announcement_text']
import email
parsed_msg = email.message_from_string(msg.encode("utf-8"))
send_mail_text(request, parsed_msg["To"],
parsed_msg["From"], parsed_msg["Subject"],
parsed_msg.get_payload())
return redirect('wg_view', name=wg.acronym)
send_mail_text(request, parsed_msg["To"],
parsed_msg["From"], parsed_msg["Subject"],
parsed_msg.get_payload())
return redirect('doc_writeup', name=charter.name)
return render_to_response('wgcharter/announcement_text.html',
dict(charter=charter,
@ -418,7 +414,7 @@ class BallotWriteupForm(forms.Form):
def clean_ballot_writeup(self):
return self.cleaned_data["ballot_writeup"].replace("\r", "")
@group_required('Area_Director','Secretariat')
@role_required('Area Director','Secretariat')
def ballot_writeupnotes(request, name):
"""Editing of ballot write-up and notes"""
try:
@ -495,7 +491,7 @@ def ballot_writeupnotes(request, name):
),
context_instance=RequestContext(request))
@group_required('Secretariat')
@role_required('Secretariat')
def approve_ballot(request, name):
"""Approve ballot, changing state, copying charter"""
try:
@ -513,10 +509,10 @@ def approve_ballot(request, name):
e = charter.latest_event(WriteupDocEvent, type="changed_action_announcement")
if not e:
if next_approved_revision(wg.charter.rev) == "01":
announcement= default_action_text(wg, charter, login, "Formed").text
else:
announcement = default_action_text(wg, charter, login, "Rechartered").text
if next_approved_revision(wg.charter.rev) == "01":
announcement= default_action_text(wg, charter, login, "Formed").text
else:
announcement = default_action_text(wg, charter, login, "Rechartered").text
else:
announcement = e.text
@ -524,7 +520,7 @@ def approve_ballot(request, name):
new_state = GroupStateName.objects.get(slug="active")
new_charter_state = State.objects.get(type="charter", slug="approved")
save_charter_in_history(charter)
save_document_in_history(charter)
save_group_in_history(wg)
prev_state = wg.state
@ -540,7 +536,7 @@ def approve_ballot(request, name):
change_description = e.desc + " and WG state has been changed to %s" % new_state.name
e = log_state_changed(request, charter, login, prev_state)
wg.time = e.time
wg.save()
@ -548,15 +544,15 @@ def approve_ballot(request, name):
filename = os.path.join(charter.get_file_path(), ch.name+"-"+ch.rev+".txt")
try:
source = open(filename, 'rb')
raw_content = source.read()
source = open(filename, 'rb')
raw_content = source.read()
new_filename = os.path.join(charter.get_file_path(), 'charter-ietf-%s-%s.txt' % (wg.acronym, next_approved_revision(ch.rev)))
destination = open(new_filename, 'wb+')
destination.write(raw_content)
destination.close()
new_filename = os.path.join(charter.get_file_path(), 'charter-ietf-%s-%s.txt' % (wg.acronym, next_approved_revision(ch.rev)))
destination = open(new_filename, 'wb+')
destination.write(raw_content)
destination.close()
except IOError:
raise Http404("Charter text %s" % filename)
raise Http404("Charter text %s" % filename)
charter.rev = next_approved_revision(charter.rev)
charter.save()
@ -567,7 +563,7 @@ def approve_ballot(request, name):
send_mail_preformatted(request, announcement)
return HttpResponseRedirect(charter.get_absolute_url())
return render_to_response('wgcharter/approve_ballot.html',
dict(charter=charter,
announcement=announcement,

View file

@ -20,14 +20,12 @@ from ietf.person.models import *
from ietf.group.models import *
from ietf.group.utils import save_group_in_history
from views_search import json_emails
class ChangeStateForm(forms.Form):
charter_state = forms.ModelChoiceField(State.objects.filter(type="charter"), label="Charter state", empty_label=None, required=False)
confirm_state = forms.BooleanField(widget=forms.HiddenInput, required=False, initial=True)
initial_time = forms.IntegerField(initial=0, label="Review time", help_text="(in weeks)", required=False)
message = forms.CharField(widget=forms.Textarea, help_text="Message to the secretariat", required=False)
comment = forms.CharField(widget=forms.Textarea, help_text="Comment for the WG history", required=False)
message = forms.CharField(widget=forms.Textarea, help_text="Message to the Secretariat", required=False)
comment = forms.CharField(widget=forms.Textarea, help_text="Optional comment for the charter history", required=False)
def __init__(self, *args, **kwargs):
qs = kwargs.pop('queryset', None)
self.hide = kwargs.pop('hide', None)
@ -89,7 +87,7 @@ def change_state(request, name, option=None):
if charter_state != charter.get_state():
# Charter state changed
change = True
save_charter_in_history(charter)
save_document_in_history(charter)
prev = charter.get_state()
charter.set_state(charter_state)
@ -99,14 +97,12 @@ def change_state(request, name, option=None):
e = log_state_changed(request, charter, login, prev)
else:
# Special log for abandoned efforts
e = DocEvent(doc=charter, by=login)
e.type = "changed_document"
e = DocEvent(type="changed_document", doc=charter, by=login)
e.desc = "IESG has abandoned the chartering effort"
e.save()
if comment:
c = DocEvent(type="added_comment", by=login)
c.doc = doc
c = DocEvent(type="added_comment", doc=charter, by=login)
c.desc = comment
c.save()
@ -230,264 +226,3 @@ def telechat_date(request, name):
user=request.user,
login=login),
context_instance=RequestContext(request))
def parse_emails_string(s):
return Email.objects.filter(address__in=[x.strip() for x in s.split(",") if x.strip()]).select_related("person")
class EditInfoForm(forms.Form):
name = forms.CharField(max_length=255, label="WG Name", required=True)
acronym = forms.CharField(max_length=8, label="WG Acronym", required=True)
chairs = forms.CharField(max_length=255, label="WG Chairs", help_text="Type in a name", required=False)
secretaries = forms.CharField(max_length=255, label="WG Secretaries", help_text="Type in a name", required=False)
techadv = forms.CharField(max_length=255, label="WG Technical Advisors", help_text="Type in a name", required=False)
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active").order_by('name'), label="Shepherding AD", empty_label="-", required=False)
parent = forms.ModelChoiceField(Group.objects.filter(type="area", state="active").order_by('name'), label="IETF Area", empty_label="-", required=False)
list_email = forms.CharField(max_length=64, required=False)
list_subscribe = forms.CharField(max_length=255, required=False)
list_archive = forms.CharField(max_length=255, required=False)
urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", help_text="Format: http://site/url (optional description). Separate multiple entries with newline.", required=False)
def __init__(self, *args, **kwargs):
self.cur_acronym = kwargs.pop('cur_acronym')
self.hide = kwargs.pop('hide', None)
super(self.__class__, self).__init__(*args, **kwargs)
# hide requested fields
if self.hide:
for f in self.hide:
self.fields[f].widget = forms.HiddenInput
# if previous AD is now ex-AD, append that person to the list
ad_pk = self.initial.get('ad')
choices = self.fields['ad'].choices
if ad_pk and ad_pk not in [pk for pk, name in choices]:
self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())]
def clean_acronym(self):
acronym = self.cleaned_data['acronym']
if self.cur_acronym and acronym != self.cur_acronym:
if Group.objects.filter(acronym__iexact=acronym):
raise forms.ValidationError("Acronym used in an existing WG. Please pick another.")
if GroupHistory.objects.filter(acronym__iexact=acronym):
raise forms.ValidationError("Acronym used in a previous WG. Please pick another.")
return acronym
def clean_chairs(self):
return parse_emails_string(self.cleaned_data["chairs"])
def clean_secretaries(self):
return parse_emails_string(self.cleaned_data["secretaries"])
def clean_techadv(self):
return parse_emails_string(self.cleaned_data["techadv"])
def clean_urls(self):
return [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
def format_urls(urls, fs="\n"):
res = []
for u in urls:
if u.name:
res.append(u"%s (%s)" % (u.url, u.name))
else:
res.append(u.url)
return fs.join(res)
@group_required('Area_Director','Secretariat')
def edit_info(request, name=None):
"""Edit or create a WG, notifying parties as
necessary and logging changes as group events."""
if request.path_info == reverse('wg_edit_info', kwargs={'name': name}):
# Editing. Get group
wg = get_object_or_404(Group, acronym=name)
charter = set_or_create_charter(wg)
new_wg = False
elif request.path_info == reverse('wg_create'):
wg = None
new_wg = True
login = request.user.get_profile()
if request.method == 'POST':
form = EditInfoForm(request.POST, cur_acronym=wg.acronym if wg else None)
if form.is_valid():
r = form.cleaned_data
if not new_wg:
gh = save_group_in_history(wg)
else:
# Create WG
wg = Group(name=r["name"],
acronym=r["acronym"],
type=GroupTypeName.objects.get(name="WG"),
state=GroupStateName.objects.get(name="Proposed"))
wg.save()
e = ChangeStateGroupEvent(group=wg, type="changed_state")
e.time = datetime.datetime.now()
e.by = login
e.state_id = "proposed"
e.desc = "Proposed group"
e.save()
if not wg.charter:
# Create adjoined charter
charter = set_or_create_charter(wg)
charter.set_state(State.objects.get(type="charter", slug="infrev"))
charter.save()
e = DocEvent(doc=charter, type="started_iesg_process")
e.time = datetime.datetime.now()
e.by = login
e.desc = "Started IESG process on charter"
e.save()
changes = []
def desc(attr, new, old):
entry = "%(attr)s changed to <b>%(new)s</b> from %(old)s"
if new_wg:
entry = "%(attr)s changed to <b>%(new)s</b>"
return entry % dict(attr=attr, new=new, old=old)
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)
def diff(attr, name):
v = getattr(wg, attr)
if r[attr] != v:
changes.append(desc(name, r[attr], v))
setattr(wg, attr, r[attr])
if attr == "acronym":
c = wg.charter
save_charter_in_history(c)
# and add a DocAlias
DocAlias.objects.create(
name = "charter-ietf-%s" % r['acronym'],
document = charter
)
# update the attributes, keeping track of what we're doing
diff('name', "Name")
diff('acronym', "Acronym")
diff('ad', "Shepherding AD")
diff('parent', "IETF Area")
diff('list_email', "Mailing list email")
diff('list_subscribe', "Mailing list subscribe address")
diff('list_archive', "Mailing list archive")
# update roles
for attr, slug in [('chairs', 'chair'), ('secretaries', 'secr'), ('techadv', 'techadv')]:
rname = RoleName.objects.get(slug=slug)
new = r[attr]
old = Email.objects.filter(role__group=wg, role__name=rname).select_related("person")
if set(new) != set(old):
changes.append(desc(rname.name,
",".join(x.get_name() for x in new),
",".join(x.get_name() for x in new)))
wg.role_set.filter(name=rname).delete()
for e in new:
Role.objects.get_or_create(name=rname, email=e, group=wg, person=e.person)
# update urls
new_urls = r['urls']
old_urls = format_urls(wg.groupurl_set.order_by('url'), ", ")
if ", ".join(sorted(new_urls)) != old_urls:
changes.append(desc('Urls', ", ".join(sorted(new_urls)), old_urls))
wg.groupurl_set.all().delete()
# Add new ones
for u in new_urls:
m = re.search('(?P<url>[\w\d:#@%/;$()~_?\+-=\\\.&]+)( \((?P<name>.+)\))?', u)
if m:
if m.group('name'):
url = GroupURL(url=m.group('url'), name=m.group('name'), group=wg)
else:
url = GroupURL(url=m.group('url'), name='', group=wg)
url.save()
wg.time = datetime.datetime.now()
if changes and not new_wg:
for c in changes:
log_info_changed(request, wg, login, c)
wg.save()
if new_wg:
return redirect('wg_startstop_process', name=wg.acronym, option="initcharter")
else:
return redirect('wg_charter', acronym=wg.acronym)
else: # form.is_valid()
if not new_wg:
init = dict(name=wg.name,
acronym=wg.acronym,
chairs=json_emails([x.email for x in wg.role_set.filter(name="Chair")]),
secretaries=json_emails([x.email for x in wg.role_set.filter(name="Secr")]),
techadv=json_emails([x.email for x in wg.role_set.filter(name="Techadv")]),
charter=wg.charter.name if wg.charter else None,
ad=wg.ad.id if wg.ad else None,
parent=wg.parent.id if wg.parent else None,
list_email=wg.list_email if wg.list_email else None,
list_subscribe=wg.list_subscribe if wg.list_subscribe else None,
list_archive=wg.list_archive if wg.list_archive else None,
urls=format_urls(wg.groupurl_set.all()),
)
hide = None
else:
init = dict(ad=login.id,
)
hide = ['chairs', 'techadv', 'list_email', 'list_subscribe', 'list_archive', 'urls']
form = EditInfoForm(initial=init, cur_acronym=wg.acronym if wg else None, hide=hide)
return render_to_response('wgcharter/edit_info.html',
dict(wg=wg,
form=form,
user=request.user,
login=login),
context_instance=RequestContext(request))
class ConcludeForm(forms.Form):
instructions = forms.CharField(widget=forms.Textarea, required=True)
@group_required('Area_Director','Secretariat')
def conclude(request, name):
"""Request the closing of a WG, prompting for instructions."""
try:
wg = Group.objects.get(acronym=name)
except Group.DoesNotExist:
wglist = GroupHistory.objects.filter(acronym=name)
if wglist:
return redirect('wg_conclude', name=wglist[0].group.acronym)
else:
raise Http404
login = request.user.get_profile()
# if state != wg.state:
# # WG state changed
# change = True
# save_group_in_history(wg)
# prev = wg.state
# wg.state = state
# ge = log_group_state_changed(request, wg, login, comment)
# wg.save()
if request.method == 'POST':
form = ConcludeForm(request.POST)
if form.is_valid():
instructions = form.cleaned_data['instructions']
email_secretariat(request, wg, "conclude", instructions)
return redirect('wg_view', name=wg.acronym)
else:
form = ConcludeForm()
return render_to_response('wgcharter/conclude.html',
dict(form=form,
wg=wg),
context_instance=RequestContext(request))

View file

@ -10,10 +10,10 @@ from django.conf import settings
from ietf.ietfauth.decorators import group_required
from ietf.group.models import Group
from ietf.doc.models import Document, DocHistory, DocEvent
from ietf.doc.models import Document, DocHistory, DocEvent, save_document_in_history
from ietf.group.utils import save_group_in_history
from utils import next_revision, set_or_create_charter, save_charter_in_history
from utils import next_revision, set_or_create_charter
class UploadForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label="Charter text", help_text="Edit the charter text", required=False)
@ -57,7 +57,7 @@ def submit(request, name):
if request.method == 'POST':
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
save_charter_in_history(charter)
save_document_in_history(charter)
# Also save group history so we can search for it
save_group_in_history(wg)
@ -87,7 +87,7 @@ def submit(request, name):
filename = os.path.join(settings.CHARTER_PATH, 'charter-ietf-%s-%s.txt' % (wg.acronym, wg.charter.rev))
try:
charter_text = open(filename, 'r')
init = dict(content = charter_text.read())
init = dict(content=charter_text.read())
except IOError:
init = {}
form = UploadForm(initial = init)

258
ietf/wginfo/edit.py Normal file
View file

@ -0,0 +1,258 @@
# edit/create view for WGs
import re, os, string, datetime
from django.shortcuts import render_to_response, get_object_or_404, redirect
from django.core.urlresolvers import reverse
from django.template import RequestContext
from django import forms
from django.utils import simplejson
from ietf.ietfauth.decorators import role_required, has_role
from ietf.doc.models import *
from ietf.name.models import *
from ietf.person.models import *
from ietf.group.models import *
from ietf.group.utils import save_group_in_history
from ietf.wgcharter.utils import set_or_create_charter
from ietf.wgcharter.mails import email_secretariat
from ietf.person.forms import EmailsField
class WGForm(forms.Form):
name = forms.CharField(max_length=255, label="WG Name", required=True)
acronym = forms.CharField(max_length=8, label="WG Acronym", required=True)
chairs = EmailsField(label="WG Chairs", required=False)
secretaries = EmailsField(label="WG Secretaries", required=False)
techadv = EmailsField(label="WG Technical Advisors", required=False)
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active").order_by('name'), label="Shepherding AD", empty_label="(None)", required=False)
parent = forms.ModelChoiceField(Group.objects.filter(type="area", state="active").order_by('name'), label="IETF Area", empty_label="(None)", required=False)
list_email = forms.CharField(max_length=64, required=False)
list_subscribe = forms.CharField(max_length=255, required=False)
list_archive = forms.CharField(max_length=255, required=False)
urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", help_text="Format: http://site/path (Optional description). Separate multiple entries with newline.", required=False)
def __init__(self, *args, **kwargs):
self.cur_acronym = kwargs.pop('cur_acronym')
self.hide = kwargs.pop('hide', None)
super(self.__class__, self).__init__(*args, **kwargs)
# hide requested fields
if self.hide:
for f in self.hide:
self.fields[f].widget = forms.HiddenInput
# if previous AD is now ex-AD, append that person to the list
ad_pk = self.initial.get('ad')
choices = self.fields['ad'].choices
if ad_pk and ad_pk not in [pk for pk, name in choices]:
self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())]
def clean_acronym(self):
acronym = self.cleaned_data['acronym']
if self.cur_acronym and acronym != self.cur_acronym:
if Group.objects.filter(acronym__iexact=acronym):
raise forms.ValidationError("Acronym used in an existing WG. Please pick another.")
if GroupHistory.objects.filter(acronym__iexact=acronym):
raise forms.ValidationError("Acronym used in a previous WG. Please pick another.")
return acronym
def clean_urls(self):
return [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
def format_urls(urls, fs="\n"):
res = []
for u in urls:
if u.name:
res.append(u"%s (%s)" % (u.url, u.name))
else:
res.append(u.url)
return fs.join(res)
@role_required('Area Director', 'Secretariat')
def edit(request, acronym=None, action="edit"):
"""Edit or create a WG, notifying parties as
necessary and logging changes as group events."""
if action == "edit":
# Editing. Get group
wg = get_object_or_404(Group, acronym=acronym)
charter = set_or_create_charter(wg)
new_wg = False
elif action == "create":
wg = None
new_wg = True
else:
raise Http404()
login = request.user.get_profile()
if request.method == 'POST':
form = WGForm(request.POST, cur_acronym=wg.acronym if wg else None)
if form.is_valid():
r = form.cleaned_data
if not new_wg:
gh = save_group_in_history(wg)
else:
# Create WG
wg = Group(name=r["name"],
acronym=r["acronym"],
type=GroupTypeName.objects.get(name="WG"),
state=GroupStateName.objects.get(name="Proposed"))
wg.save()
e = ChangeStateGroupEvent(group=wg, type="changed_state")
e.time = datetime.datetime.now()
e.by = login
e.state_id = "proposed"
e.desc = "Proposed group"
e.save()
if not wg.charter:
# Create adjoined charter
charter = set_or_create_charter(wg)
charter.set_state(State.objects.get(type="charter", slug="infrev"))
charter.save()
e = DocEvent(doc=charter, type="started_iesg_process")
e.time = datetime.datetime.now()
e.by = login
e.desc = "Started IESG process on charter"
e.save()
changes = []
def desc(attr, new, old):
entry = "%(attr)s changed to <b>%(new)s</b> from %(old)s"
if new_wg:
entry = "%(attr)s changed to <b>%(new)s</b>"
return entry % dict(attr=attr, new=new, old=old)
def diff(attr, name):
v = getattr(wg, attr)
if r[attr] != v:
changes.append(desc(name, r[attr], v))
setattr(wg, attr, r[attr])
if attr == "acronym":
c = wg.charter
save_document_in_history(c)
# and add a DocAlias
DocAlias.objects.create(
name = "charter-ietf-%s" % r['acronym'],
document = charter
)
# update the attributes, keeping track of what we're doing
diff('name', "Name")
diff('acronym', "Acronym")
diff('ad', "Shepherding AD")
diff('parent', "IETF Area")
diff('list_email', "Mailing list email")
diff('list_subscribe', "Mailing list subscribe address")
diff('list_archive', "Mailing list archive")
# update roles
for attr, slug, title in [('chairs', 'chair', "Chairs"), ('secretaries', 'secr', "Secretaries"), ('techadv', 'techadv', "Tech Advisors")]:
new = r[attr]
old = Email.objects.filter(role__group=wg, role__name=slug).select_related("person")
if set(new) != set(old):
changes.append(desc(title,
", ".join(x.get_name() for x in new),
", ".join(x.get_name() for x in old)))
wg.role_set.filter(name=slug).delete()
for e in new:
Role.objects.get_or_create(name_id=slug, email=e, group=wg, person=e.person)
# update urls
new_urls = r['urls']
old_urls = format_urls(wg.groupurl_set.order_by('url'), ", ")
if ", ".join(sorted(new_urls)) != old_urls:
changes.append(desc('Urls', ", ".join(sorted(new_urls)), old_urls))
wg.groupurl_set.all().delete()
# Add new ones
for u in new_urls:
m = re.search('(?P<url>[\w\d:#@%/;$()~_?\+-=\\\.&]+)( \((?P<name>.+)\))?', u)
if m:
if m.group('name'):
url = GroupURL(url=m.group('url'), name=m.group('name'), group=wg)
else:
url = GroupURL(url=m.group('url'), name='', group=wg)
url.save()
wg.time = datetime.datetime.now()
if changes and not new_wg:
for c in changes:
GroupEvent.objects.create(group=wg, by=login, type="info_changed", desc=c)
wg.save()
if new_wg:
return redirect('wg_startstop_process', name=wg.acronym, option="initcharter")
return redirect('wg_charter', acronym=wg.acronym)
else: # form.is_valid()
if not new_wg:
from ietf.person.forms import json_emails
init = dict(name=wg.name,
acronym=wg.acronym,
chairs=Email.objects.filter(role__group=wg, role__name="chair"),
secretaries=Email.objects.filter(role__group=wg, role__name="secr"),
techadv=Email.objects.filter(role__group=wg, role__name="techadv"),
charter=wg.charter.name if wg.charter else None,
ad=wg.ad_id if wg.ad else None,
parent=wg.parent.id if wg.parent else None,
list_email=wg.list_email if wg.list_email else None,
list_subscribe=wg.list_subscribe if wg.list_subscribe else None,
list_archive=wg.list_archive if wg.list_archive else None,
urls=format_urls(wg.groupurl_set.all()),
)
hide = None
else:
init = dict(ad=login.id if has_role(request.user, "Area Director") else None,
)
hide = ['chairs', 'techadv', 'list_email', 'list_subscribe', 'list_archive', 'urls']
form = WGForm(initial=init, cur_acronym=wg.acronym if wg else None, hide=hide)
return render_to_response('wginfo/edit.html',
dict(wg=wg,
form=form,
user=request.user,
login=login),
context_instance=RequestContext(request))
class ConcludeForm(forms.Form):
instructions = forms.CharField(widget=forms.Textarea, required=True)
@role_required('Area Director','Secretariat')
def conclude(request, acronym):
"""Request the closing of a WG, prompting for instructions."""
try:
wg = Group.objects.get(acronym=acronym)
except Group.DoesNotExist:
wglist = GroupHistory.objects.filter(acronym=acronym)
if wglist:
return redirect('wg_conclude', acronym=wglist[0].group.acronym)
else:
raise Http404
login = request.user.get_profile()
if request.method == 'POST':
form = ConcludeForm(request.POST)
if form.is_valid():
instructions = form.cleaned_data['instructions']
email_secretariat(request, wg, "conclude", instructions)
return redirect('wg_charter', acronym=wg.acronym)
else:
form = ConcludeForm()
return render_to_response('wginfo/conclude.html',
dict(form=form,
wg=wg),
context_instance=RequestContext(request))

View file

@ -30,10 +30,24 @@
# (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 os
import unittest
import os, unittest, shutil
import django.test
from django.conf import settings
from django.core.urlresolvers import reverse as urlreverse
from ietf.utils.mail import outbox
from ietf.utils.test_data import make_test_data
from ietf.utils.test_utils import login_testing_unauthorized
from pyquery import PyQuery
from ietf.utils.test_utils import SimpleUrlTestCase
from ietf.doc.models import *
from ietf.group.models import *
from ietf.group.utils import *
from ietf.name.models import *
from ietf.person.models import *
class WgInfoUrlTestCase(SimpleUrlTestCase):
def testUrls(self):
@ -41,7 +55,6 @@ class WgInfoUrlTestCase(SimpleUrlTestCase):
class WgFileTestCase(unittest.TestCase):
def testFileExistence(self):
print " Testing if WG charter files exist locally"
fpath = os.path.join(settings.IETFWG_DESCRIPTIONS_PATH, "tls.desc.txt")
if not os.path.exists(fpath):
print "\nERROR: charter files not found in "+settings.IETFWG_DESCRIPTIONS_PATH
@ -49,6 +62,159 @@ class WgFileTestCase(unittest.TestCase):
print "Download them to a local directory with:"
print "wget -nd -nc -np -r http://www.ietf.org/wg-descriptions/"
print "And set IETFWG_DESCRIPTIONS_PATH in settings_local.py\n"
else:
print "OK (they seem to exist)"
class WgOverviewTestCase(django.test.TestCase):
fixtures = ["names"]
def test_overview(self):
make_test_data()
wg = Group.objects.get(acronym="mars")
wg.charter.set_state(State.objects.get(type="charter", slug="intrev"))
url = urlreverse('ietf.wginfo.views.chartering_wgs')
r = self.client.get(url)
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
print
self.assertEquals(len(q('table.ietf-doctable td.acronym a:contains("mars")')), 1)
class WgEditTestCase(django.test.TestCase):
fixtures = ["names"]
def setUp(self):
self.charter_dir = os.path.abspath("tmp-charter-dir")
os.mkdir(self.charter_dir)
settings.CHARTER_PATH = self.charter_dir
def tearDown(self):
shutil.rmtree(self.charter_dir)
def test_create(self):
make_test_data()
url = urlreverse('wg_create')
login_testing_unauthorized(self, "secretary", url)
num_wgs = len(Group.objects.filter(type="wg"))
# normal get
r = self.client.get(url)
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertEquals(len(q('form input[name=acronym]')), 1)
# faulty post
r = self.client.post(url, dict(acronym="foobarbaz")) # No name
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
self.assertEquals(len(Group.objects.filter(type="wg")), num_wgs)
# creation
r = self.client.post(url, dict(acronym="testwg", name="Testing WG"))
self.assertEquals(r.status_code, 302)
self.assertEquals(len(Group.objects.filter(type="wg")), num_wgs + 1)
group = Group.objects.get(acronym="testwg")
self.assertEquals(group.name, "Testing WG")
# check that a charter was created with the correct name
self.assertEquals(group.charter.name, "charter-ietf-testwg")
# and that it has no revision
self.assertEquals(group.charter.rev, "")
def test_edit_info(self):
make_test_data()
# And make a charter for group
group = Group.objects.get(acronym="mars")
url = urlreverse('wg_edit', kwargs=dict(acronym=group.acronym))
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=parent]')), 1)
self.assertEquals(len(q('form input[name=acronym]')), 1)
# faulty post
Group.objects.create(name="Collision Test Group", acronym="collide")
r = self.client.post(url, dict(acronym="collide"))
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
# Create old acronym
group.acronym = "oldmars"
group.save()
save_group_in_history(group)
group.acronym = "mars"
group.save()
# post with warning
r = self.client.post(url, dict(acronym="oldmars"))
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
# edit info
area = group.parent
ad = Person.objects.get(name="Aread Irector")
r = self.client.post(url,
dict(name="Mars Not Special Interest Group",
acronym="mnsig",
parent=area.pk,
ad=ad.pk,
chairs="aread@ietf.org, ad1@ietf.org",
secretaries="aread@ietf.org, ad1@ietf.org, ad2@ietf.org",
techadv="aread@ietf.org",
list_email="mars@mail",
list_subscribe="subscribe.mars",
list_archive="archive.mars",
urls="http://mars.mars (MARS site)"
))
self.assertEquals(r.status_code, 302)
group = Group.objects.get(acronym="mnsig")
self.assertEquals(group.name, "Mars Not Special Interest Group")
self.assertEquals(group.parent, area)
self.assertEquals(group.ad, ad)
for k in ("chair", "secr", "techadv"):
self.assertTrue(group.role_set.filter(name=k, email__address="aread@ietf.org"))
self.assertEquals(group.list_email, "mars@mail")
self.assertEquals(group.list_subscribe, "subscribe.mars")
self.assertEquals(group.list_archive, "archive.mars")
self.assertEquals(group.groupurl_set.all()[0].url, "http://mars.mars")
self.assertEquals(group.groupurl_set.all()[0].name, "MARS site")
def test_conclude(self):
make_test_data()
# And make a charter for group
group = Group.objects.get(acronym="mars")
# -- Test conclude WG --
url = urlreverse('wg_conclude', kwargs=dict(acronym=group.acronym))
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=instructions]')), 1)
# faulty post
r = self.client.post(url, dict(instructions="")) # No instructions
self.assertEquals(r.status_code, 200)
q = PyQuery(r.content)
self.assertTrue(len(q('form ul.errorlist')) > 0)
# conclusion request
r = self.client.post(url, dict(instructions="Test instructions"))
self.assertEquals(r.status_code, 302)
# The WG remains active until the state is set to conclude via change_state
group = Group.objects.get(acronym=group.acronym)
self.assertEquals(group.state_id, "active")

View file

@ -1,9 +1,10 @@
# Copyright The IETF Trust 2008, All Rights Reserved
from django.conf.urls.defaults import patterns, include, url
from ietf.wginfo import views
from django.conf.urls.defaults import patterns, include
from ietf.wginfo import views, edit
from django.views.generic.simple import redirect_to
urlpatterns = patterns('',
(r'^$', views.wg_dir),
(r'^summary.txt', redirect_to, { 'url':'/wg/1wg-summary.txt' }),
@ -13,10 +14,13 @@ urlpatterns = patterns('',
(r'^1wg-summary-by-acronym.txt', views.wg_summary_acronym),
(r'^1wg-charters.txt', views.wg_charters),
(r'^1wg-charters-by-acronym.txt', views.wg_charters_by_acronym),
(r'^chartering/create/$', edit.edit, {'action': "create"}, "wg_create"),
(r'^chartering/$', views.chartering_wgs),
(r'^(?P<acronym>[a-zA-Z0-9-]+)/documents/txt/$', views.wg_documents_txt),
(r'^(?P<acronym>[a-zA-Z0-9-]+)/$', views.wg_documents_html),
url(r'^(?P<acronym>[a-zA-Z0-9-]+)/charter/$', views.wg_charter, name='wg_charter'),
(r'^(?P<acronym>[a-zA-Z0-9-]+)/charter/$', views.wg_charter, None, 'wg_charter'),
(r'^(?P<acronym>[a-zA-Z0-9-]+)/history/', views.history),
(r'^(?P<acronym>[a-zA-Z0-9-]+)/edit/', edit.edit, {'action': "edit"}, "wg_edit"),
(r'^(?P<acronym>[a-zA-Z0-9-]+)/conclude/', edit.conclude, None, "wg_conclude"),
(r'^(?P<acronym>[^/]+)/management/', include('ietf.wgchairs.urls')),
)

View file

@ -169,10 +169,10 @@ def wg_charter(request, acronym):
fill_in_charter_info(wg)
actions = []
if wg.state_id != "conclude":
actions.append(("Edit WG", urlreverse("wg_edit_info", kwargs=dict(name=wg.acronym))))
actions.append(("Edit WG", urlreverse("wg_edit", kwargs=dict(acronym=wg.acronym))))
if wg.state_id == "active" and (not wg.charter or wg.charter.get_state_slug() == "approved"):
actions.append(("Conclude WG", urlreverse("wg_conclude", kwargs=dict(name=wg.acronym))))
actions.append(("Conclude WG", urlreverse("wg_conclude", kwargs=dict(acronym=wg.acronym))))
context = get_wg_menu_context(wg, "charter")
context.update(dict(

View file

@ -11,6 +11,8 @@
.snapshots { margin: 0.5em 0; }
.snapshots .revisions a:first-child { font-weight: bold }
.metabox .actions a { display: inline-block; margin-right: 0.4em; }
.diffTool { padding: 8px 4px; margin: 8px 0;}
.diffTool h2 { margin-top:0;margin-bottom:4px; }
.diffTool label { display: inline-block; width: 3em; padding: 0 0.5em; }

10
static/js/emails-field.js Normal file
View file

@ -0,0 +1,10 @@
jQuery(function () {
jQuery(".emails-field").each(function () {
var e = jQuery(this);
e.tokenInput(e.data("ajax-url"), {
hintText: "",
preventDuplicates: true,
prePopulate: JSON.parse(e.val())
});
});
});

487
static/js/lib/json2.js Normal file
View file

@ -0,0 +1,487 @@
/*
http://www.JSON.org/json2.js
2011-10-19
Public Domain.
NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
See http://www.JSON.org/js.html
This code should be minified before deployment.
See http://javascript.crockford.com/jsmin.html
USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
NOT CONTROL.
This file creates a global JSON object containing two methods: stringify
and parse.
JSON.stringify(value, replacer, space)
value any JavaScript value, usually an object or array.
replacer an optional parameter that determines how object
values are stringified for objects. It can be a
function or an array of strings.
space an optional parameter that specifies the indentation
of nested structures. If it is omitted, the text will
be packed without extra whitespace. If it is a number,
it will specify the number of spaces to indent at each
level. If it is a string (such as '\t' or '&nbsp;'),
it contains the characters used to indent at each level.
This method produces a JSON text from a JavaScript value.
When an object value is found, if the object contains a toJSON
method, its toJSON method will be called and the result will be
stringified. A toJSON method does not serialize: it returns the
value represented by the name/value pair that should be serialized,
or undefined if nothing should be serialized. The toJSON method
will be passed the key associated with the value, and this will be
bound to the value
For example, this would serialize Dates as ISO strings.
Date.prototype.toJSON = function (key) {
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
return this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z';
};
You can provide an optional replacer method. It will be passed the
key and value of each member, with this bound to the containing
object. The value that is returned from your method will be
serialized. If your method returns undefined, then the member will
be excluded from the serialization.
If the replacer parameter is an array of strings, then it will be
used to select the members to be serialized. It filters the results
such that only members with keys listed in the replacer array are
stringified.
Values that do not have JSON representations, such as undefined or
functions, will not be serialized. Such values in objects will be
dropped; in arrays they will be replaced with null. You can use
a replacer function to replace those with JSON values.
JSON.stringify(undefined) returns undefined.
The optional space parameter produces a stringification of the
value that is filled with line breaks and indentation to make it
easier to read.
If the space parameter is a non-empty string, then that string will
be used for indentation. If the space parameter is a number, then
the indentation will be that many spaces.
Example:
text = JSON.stringify(['e', {pluribus: 'unum'}]);
// text is '["e",{"pluribus":"unum"}]'
text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
// text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
text = JSON.stringify([new Date()], function (key, value) {
return this[key] instanceof Date ?
'Date(' + this[key] + ')' : value;
});
// text is '["Date(---current time---)"]'
JSON.parse(text, reviver)
This method parses a JSON text to produce an object or array.
It can throw a SyntaxError exception.
The optional reviver parameter is a function that can filter and
transform the results. It receives each of the keys and values,
and its return value is used instead of the original value.
If it returns what it received, then the structure is not modified.
If it returns undefined then the member is deleted.
Example:
// Parse the text. Values that look like ISO date strings will
// be converted to Date objects.
myData = JSON.parse(text, function (key, value) {
var a;
if (typeof value === 'string') {
a =
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
if (a) {
return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+a[5], +a[6]));
}
}
return value;
});
myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
var d;
if (typeof value === 'string' &&
value.slice(0, 5) === 'Date(' &&
value.slice(-1) === ')') {
d = new Date(value.slice(5, -1));
if (d) {
return d;
}
}
return value;
});
This is a reference implementation. You are free to copy, modify, or
redistribute.
*/
/*jslint evil: true, regexp: true */
/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
lastIndex, length, parse, prototype, push, replace, slice, stringify,
test, toJSON, toString, valueOf
*/
// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.
var JSON;
if (!JSON) {
JSON = {};
}
(function () {
'use strict';
function f(n) {
// Format integers to have at least two digits.
return n < 10 ? '0' + n : n;
}
if (typeof Date.prototype.toJSON !== 'function') {
Date.prototype.toJSON = function (key) {
return isFinite(this.valueOf())
? this.getUTCFullYear() + '-' +
f(this.getUTCMonth() + 1) + '-' +
f(this.getUTCDate()) + 'T' +
f(this.getUTCHours()) + ':' +
f(this.getUTCMinutes()) + ':' +
f(this.getUTCSeconds()) + 'Z'
: null;
};
String.prototype.toJSON =
Number.prototype.toJSON =
Boolean.prototype.toJSON = function (key) {
return this.valueOf();
};
}
var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
gap,
indent,
meta = { // table of character substitutions
'\b': '\\b',
'\t': '\\t',
'\n': '\\n',
'\f': '\\f',
'\r': '\\r',
'"' : '\\"',
'\\': '\\\\'
},
rep;
function quote(string) {
// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.
escapable.lastIndex = 0;
return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
var c = meta[a];
return typeof c === 'string'
? c
: '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
}) + '"' : '"' + string + '"';
}
function str(key, holder) {
// Produce a string from holder[key].
var i, // The loop counter.
k, // The member key.
v, // The member value.
length,
mind = gap,
partial,
value = holder[key];
// If the value has a toJSON method, call it to obtain a replacement value.
if (value && typeof value === 'object' &&
typeof value.toJSON === 'function') {
value = value.toJSON(key);
}
// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.
if (typeof rep === 'function') {
value = rep.call(holder, key, value);
}
// What happens next depends on the value's type.
switch (typeof value) {
case 'string':
return quote(value);
case 'number':
// JSON numbers must be finite. Encode non-finite numbers as null.
return isFinite(value) ? String(value) : 'null';
case 'boolean':
case 'null':
// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce 'null'. The case is included here in
// the remote chance that this gets fixed someday.
return String(value);
// If the type is 'object', we might be dealing with an object or an array or
// null.
case 'object':
// Due to a specification blunder in ECMAScript, typeof null is 'object',
// so watch out for that case.
if (!value) {
return 'null';
}
// Make an array to hold the partial results of stringifying this object value.
gap += indent;
partial = [];
// Is the value an array?
if (Object.prototype.toString.apply(value) === '[object Array]') {
// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.
length = value.length;
for (i = 0; i < length; i += 1) {
partial[i] = str(i, value) || 'null';
}
// Join all of the elements together, separated with commas, and wrap them in
// brackets.
v = partial.length === 0
? '[]'
: gap
? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
: '[' + partial.join(',') + ']';
gap = mind;
return v;
}
// If the replacer is an array, use it to select the members to be stringified.
if (rep && typeof rep === 'object') {
length = rep.length;
for (i = 0; i < length; i += 1) {
if (typeof rep[i] === 'string') {
k = rep[i];
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
} else {
// Otherwise, iterate through all of the keys in the object.
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = str(k, value);
if (v) {
partial.push(quote(k) + (gap ? ': ' : ':') + v);
}
}
}
}
// Join all of the member texts together, separated with commas,
// and wrap them in braces.
v = partial.length === 0
? '{}'
: gap
? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
: '{' + partial.join(',') + '}';
gap = mind;
return v;
}
}
// If the JSON object does not yet have a stringify method, give it one.
if (typeof JSON.stringify !== 'function') {
JSON.stringify = function (value, replacer, space) {
// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.
var i;
gap = '';
indent = '';
// If the space parameter is a number, make an indent string containing that
// many spaces.
if (typeof space === 'number') {
for (i = 0; i < space; i += 1) {
indent += ' ';
}
// If the space parameter is a string, it will be used as the indent string.
} else if (typeof space === 'string') {
indent = space;
}
// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.
rep = replacer;
if (replacer && typeof replacer !== 'function' &&
(typeof replacer !== 'object' ||
typeof replacer.length !== 'number')) {
throw new Error('JSON.stringify');
}
// Make a fake root object containing our value under the key of ''.
// Return the result of stringifying the value.
return str('', {'': value});
};
}
// If the JSON object does not yet have a parse method, give it one.
if (typeof JSON.parse !== 'function') {
JSON.parse = function (text, reviver) {
// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.
var j;
function walk(holder, key) {
// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.
var k, v, value = holder[key];
if (value && typeof value === 'object') {
for (k in value) {
if (Object.prototype.hasOwnProperty.call(value, k)) {
v = walk(value, k);
if (v !== undefined) {
value[k] = v;
} else {
delete value[k];
}
}
}
}
return reviver.call(holder, key, value);
}
// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.
text = String(text);
cx.lastIndex = 0;
if (cx.test(text)) {
text = text.replace(cx, function (a) {
return '\\u' +
('0000' + a.charCodeAt(0).toString(16)).slice(-4);
});
}
// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with '()' and 'new'
// because they can cause invocation, and '=' because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.
// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
// replace all simple value tokens with ']' characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or ']' or
// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
if (/^[\],:{}\s]*$/
.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
.replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
.replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.
j = eval('(' + text + ')');
// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.
return typeof reviver === 'function'
? walk({'': j}, '')
: j;
}
// If the text is not JSON parseable, then a SyntaxError is thrown.
throw new SyntaxError('JSON.parse');
};
}
}());