Adds support for dateless milestones. Fixes #2799. Commit ready for merge.

- Legacy-Id: 17185
This commit is contained in:
Robert Sparks 2020-01-06 20:48:53 +00:00
commit 42e5163b09
14 changed files with 537 additions and 170 deletions

View file

@ -1,7 +1,9 @@
# Copyright The IETF Trust 2015-2020, All Rights Reserved
import datetime
import debug # pyflakes:ignore
import factory
from ietf.group.models import Group, Role, GroupEvent
from ietf.group.models import Group, Role, GroupEvent, GroupMilestone
from ietf.review.factories import ReviewTeamSettingsFactory
class GroupFactory(factory.DjangoModelFactory):
@ -14,6 +16,7 @@ class GroupFactory(factory.DjangoModelFactory):
state_id = 'active'
type_id = 'wg'
list_email = factory.LazyAttribute(lambda a: '%s@ietf.org'% a.acronym)
uses_milestone_dates = True
class ReviewTeamFactory(factory.DjangoModelFactory):
class Meta:
@ -44,3 +47,20 @@ class GroupEventFactory(factory.DjangoModelFactory):
by = factory.SubFactory('ietf.person.factories.PersonFactory')
type = 'comment'
desc = factory.Faker('paragraph')
class BaseGroupMilestoneFactory(factory.DjangoModelFactory):
class Meta:
model = GroupMilestone
group = factory.SubFactory(GroupFactory)
state_id = 'active'
desc = factory.Faker('sentence')
class DatedGroupMilestoneFactory(BaseGroupMilestoneFactory):
group = factory.SubFactory(GroupFactory, uses_milestone_dates=True)
due = datetime.datetime.today()+datetime.timedelta(days=180)
class DatelessGroupMilestoneFactory(BaseGroupMilestoneFactory):
group = factory.SubFactory(GroupFactory, uses_milestone_dates=False)
order = factory.Sequence(lambda n: n)

View file

@ -0,0 +1,26 @@
# Copyright The IETF Trust 2019-2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2019-10-30 11:41
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('group', '0019_rename_field_document2'),
]
operations = [
migrations.AddField(
model_name='group',
name='uses_milestone_dates',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='grouphistory',
name='uses_milestone_dates',
field=models.BooleanField(default=False),
),
]

View file

@ -0,0 +1,44 @@
# Copyright The IETF Trust 2019-2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2019-10-30 13:37
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('group', '0020_add_uses_milestone_dates'),
]
operations = [
migrations.AlterModelOptions(
name='groupmilestone',
options={'ordering': ['order', 'id']},
),
migrations.AlterModelOptions(
name='groupmilestonehistory',
options={'ordering': ['order', 'id']},
),
migrations.AddField(
model_name='groupmilestone',
name='order',
field=models.IntegerField(blank=True, null=True),
),
migrations.AddField(
model_name='groupmilestonehistory',
name='order',
field=models.IntegerField(blank=True, null=True),
),
migrations.AlterField(
model_name='groupmilestone',
name='due',
field=models.DateField(blank=True, null=True),
),
migrations.AlterField(
model_name='groupmilestonehistory',
name='due',
field=models.DateField(blank=True, null=True),
),
]

View file

@ -0,0 +1,30 @@
# Copyright The IETF Trust 2019-2020, All Rights Reserved
# -*- coding: utf-8 -*-
# Generated by Django 1.11.25 on 2019-10-30 11:42
from __future__ import unicode_literals
from django.db import migrations
def forward(apps, schema_editor):
Group = apps.get_model('group','Group')
GroupHistory = apps.get_model('group','GroupHistory')
Group.objects.filter(type__features__has_milestones=True).update(uses_milestone_dates=True)
GroupHistory.objects.filter(type__features__has_milestones=True).update(uses_milestone_dates=True)
def reverse(apps, schema_editor):
Group = apps.get_model('group','Group')
GroupHistory = apps.get_model('group','GroupHistory')
Group.objects.filter(type__features__has_milestones=True).update(uses_milestone_dates=False)
GroupHistory.objects.filter(type__features__has_milestones=True).update(uses_milestone_dates=False)
class Migration(migrations.Migration):
dependencies = [
('group', '0021_add_order_to_milestones'),
]
operations = [
migrations.RunPython(forward, reverse)
]

View file

@ -1,3 +1,4 @@
# Copyright The IETF Trust 2012-2020, All Rights Reserved
# group milestone editing views
import datetime
@ -26,6 +27,7 @@ class MilestoneForm(forms.Form):
desc = forms.CharField(max_length=500, label="Milestone", required=True)
due = DatepickerDateField(date_format="MM yyyy", picker_settings={"min-view-mode": "months", "autoclose": "1", "view-mode": "years" }, required=True)
order = forms.IntegerField(required=True, widget=forms.HiddenInput)
docs = SearchableDocumentsField(label="Drafts", required=False, help_text="Any drafts that the milestone concerns.")
resolved_checkbox = forms.BooleanField(required=False, label="Resolved")
resolved = forms.CharField(label="Resolved as", max_length=50, required=False)
@ -39,6 +41,8 @@ class MilestoneForm(forms.Form):
def __init__(self, needs_review, reviewer, *args, **kwargs):
m = self.milestone = kwargs.pop("instance", None)
uses_dates = kwargs.pop("uses_dates", True)
can_review = not needs_review
if m:
@ -49,6 +53,7 @@ class MilestoneForm(forms.Form):
kwargs["initial"].update(dict(id=m.pk,
desc=m.desc,
due=m.due,
order=m.order,
resolved_checkbox=bool(m.resolved),
resolved=m.resolved,
docs=m.docs.all(),
@ -60,6 +65,11 @@ class MilestoneForm(forms.Form):
super(MilestoneForm, self).__init__(*args, **kwargs)
if not uses_dates:
self.fields.pop('due')
else:
self.fields.pop('order')
self.fields["resolved"].widget.attrs["data-default"] = "Done"
if needs_review and self.milestone and self.milestone.state_id != "review":
@ -139,8 +149,11 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
m.state = GroupMilestoneStateName.objects.get(slug="charter")
m.desc = c["desc"]
m.due = due_month_year_to_date(c)
m.resolved = c["resolved"]
if 'due' in f.fields:
m.due = due_month_year_to_date(c)
else:
m.order = c["order"]
def milestone_changed(f, m):
# we assume that validation has run
@ -148,12 +161,18 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
return True
c = f.cleaned_data
return (c["desc"] != m.desc or
due_month_year_to_date(c) != due_month_year_to_date(m.due) or
c["resolved"] != m.resolved or
set(c["docs"]) != set(m.docs.all()) or
c.get("review") in ("accept", "reject")
changed = (
c["desc"] != m.desc or
c["resolved"] != m.resolved or
set(c["docs"]) != set(m.docs.all()) or
c.get("review") in ("accept", "reject")
)
if 'due' in f.fields:
changed = changed or due_month_year_to_date(c) != due_month_year_to_date(m.due)
else:
changed = changed or c["order"] != m.order
return changed
def save_milestone_form(f):
c = f.cleaned_data
@ -195,14 +214,21 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
m.desc = c["desc"]
changes.append('set description to "%s"' % m.desc)
c_due = due_month_year_to_date(c)
m_due = due_month_year_to_date(m.due)
if c_due != m_due:
if not history:
history = save_milestone_in_history(m)
changes.append('set due date to %s from %s' % (c_due.strftime("%B %Y"), m.due.strftime("%B %Y")))
m.due = c_due
if 'due' in f.fields:
c_due = due_month_year_to_date(c)
m_due = due_month_year_to_date(m.due)
if c_due != m_due:
if not history:
history = save_milestone_in_history(m)
changes.append('set due date to %s from %s' % (c_due.strftime("%B %Y"), m.due.strftime("%B %Y")))
m.due = c_due
else:
order = c["order"]
if order != m.order:
if not history:
history = save_milestone_in_history(m)
changes.append("Milestone order changed from %s to %s" % ( m.order, order ))
m.order = order
resolved = c["resolved"]
if resolved != m.resolved:
@ -259,74 +285,94 @@ def edit_milestones(request, acronym, group_type=None, milestone_set="current"):
if milestone_set == "charter":
named_milestone = "charter " + named_milestone
if m.state_id in ("active", "charter"):
return 'Added %s, due %s' % (named_milestone, m.due.strftime("%B %Y"))
elif m.state_id == "review":
return 'Added %s for review, due %s' % (named_milestone, m.due.strftime("%B %Y"))
desc = 'Added %s' % (named_milestone, )
if m.state_id == 'review':
desc += ' for review'
if 'due' in f.fields:
desc += ', due %s' % (m.due.strftime("%B %Y"), )
return desc
form_errors = False
if request.method == 'POST':
# parse out individual milestone forms
for prefix in request.POST.getlist("prefix"):
if not prefix: # empty form
continue
# new milestones have non-existing ids so instance end up as None
instance = milestones_dict.get(request.POST.get(prefix + "-id", ""), None)
f = MilestoneForm(needs_review, reviewer, request.POST, prefix=prefix, instance=instance)
forms.append(f)
form_errors = form_errors or not f.is_valid()
f.changed = milestone_changed(f, f.milestone)
if f.is_valid() and f.cleaned_data.get("review") in ("accept", "reject"):
f.needs_review = False
action = request.POST.get("action", "review")
if action == "review":
for f in forms:
if f.is_valid():
# let's fill in the form milestone so we can output it in the template
if not f.milestone:
f.milestone = GroupMilestone()
set_attributes_from_form(f, f.milestone)
elif action == "save" and not form_errors:
changes = []
states = []
for f in forms:
change = save_milestone_form(f)
if not change:
if action == "switch":
if group.uses_milestone_dates:
group.uses_milestone_dates=False
group.save()
for order, milestone in enumerate(group.groupmilestone_set.filter(state_id='active').order_by('due','id')):
milestone.order = order
milestone.save()
else:
group.uses_milestone_dates=True
group.save()
for m in milestones:
forms.append(MilestoneForm(needs_review, reviewer, instance=m, uses_dates=group.uses_milestone_dates))
else:
# parse out individual milestone forms
for prefix in request.POST.getlist("prefix"):
if not prefix: # empty form
continue
# new milestones have non-existing ids so instance end up as None
instance = milestones_dict.get(request.POST.get(prefix + "-id", ""), None)
f = MilestoneForm(needs_review, reviewer, request.POST, prefix=prefix, instance=instance, uses_dates=group.uses_milestone_dates)
forms.append(f)
form_errors = form_errors or not f.is_valid()
f.changed = milestone_changed(f, f.milestone)
if f.is_valid() and f.cleaned_data.get("review") in ("accept", "reject"):
f.needs_review = False
if action == "review":
for f in forms:
if f.is_valid():
# let's fill in the form milestone so we can output it in the template
if not f.milestone:
f.milestone = GroupMilestone()
set_attributes_from_form(f, f.milestone)
elif action == "save" and not form_errors:
changes = []
states = []
for f in forms:
change = save_milestone_form(f)
if not change:
continue
if milestone_set == "charter":
DocEvent.objects.create(doc=group.charter, rev=group.charter.rev, type="changed_charter_milestone",
by=request.user.person, desc=change)
else:
MilestoneGroupEvent.objects.create(group=group, type="changed_milestone",
by=request.user.person, desc=change, milestone=f.milestone)
changes.append(change)
states.append(f.milestone.state_id)
if milestone_set == "current":
email_milestones_changed(request, group, changes, states)
if milestone_set == "charter":
DocEvent.objects.create(doc=group.charter, rev=group.charter.rev, type="changed_charter_milestone",
by=request.user.person, desc=change)
return redirect('ietf.doc.views_doc.document_main', name=group.charter.canonical_name())
else:
MilestoneGroupEvent.objects.create(group=group, type="changed_milestone",
by=request.user.person, desc=change, milestone=f.milestone)
changes.append(change)
states.append(f.milestone.state_id)
if milestone_set == "current":
email_milestones_changed(request, group, changes, states)
if milestone_set == "charter":
return redirect('ietf.doc.views_doc.document_main', name=group.charter.canonical_name())
else:
return HttpResponseRedirect(group.about_url())
return HttpResponseRedirect(group.about_url())
else:
for m in milestones:
forms.append(MilestoneForm(needs_review, reviewer, instance=m))
forms.append(MilestoneForm(needs_review, reviewer, instance=m, uses_dates=group.uses_milestone_dates))
can_reset = milestone_set == "charter" and get_chartering_type(group.charter) == "rechartering"
empty_form = MilestoneForm(needs_review, reviewer)
empty_form = MilestoneForm(needs_review, reviewer, uses_dates=group.uses_milestone_dates)
forms.sort(key=lambda f: f.milestone.due if f.milestone else datetime.date.max)
if group.uses_milestone_dates:
forms.sort(key=lambda f: f.milestone.due if f.milestone else datetime.date.max)
else:
forms.sort(key=lambda f: (f.milestone is None, f.milestone.order if f.milestone else None) )
return render(request, 'group/edit_milestones.html',
dict(group=group,
@ -378,15 +424,20 @@ def reset_charter_milestones(request, group_type, acronym):
state_id="charter",
desc=m.desc,
due=m.due,
order=m.order,
resolved=m.resolved,
)
new.docs.clear()
new.docs.set(m.docs.all())
if group.uses_milestone_dates:
desc='Added milestone "%s", due %s, from current group milestones' % (new.desc, new.due.strftime("%B %Y"))
else:
desc='Added milestone "%s" from current group milestones' % ( new.desc, )
DocEvent.objects.create(type="changed_charter_milestone",
doc=group.charter,
rev=group.charter.rev,
desc='Added milestone "%s", due %s, from current group milestones' % (new.desc, new.due.strftime("%B %Y")),
desc=desc,
by=request.user.person,
)

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2010-2019, All Rights Reserved
# Copyright The IETF Trust 2010-2020, All Rights Reserved
# -*- coding: utf-8 -*-
from __future__ import absolute_import, print_function, unicode_literals
@ -46,6 +46,8 @@ class GroupInfo(models.Model):
unused_states = models.ManyToManyField('doc.State', help_text="Document states that have been disabled for the group.", blank=True)
unused_tags = models.ManyToManyField(DocTagName, help_text="Document tags that have been disabled for the group.", blank=True)
uses_milestone_dates = models.BooleanField(default=False)
def __str__(self):
return self.name
@ -269,7 +271,8 @@ class GroupMilestoneInfo(models.Model):
# are stored on the charter document
state = ForeignKey(GroupMilestoneStateName)
desc = models.CharField(verbose_name="Description", max_length=500)
due = models.DateField()
due = models.DateField(blank=True,null=True)
order = models.IntegerField(blank=True,null=True)
resolved = models.CharField(max_length=50, blank=True, help_text="Explanation of why milestone is resolved (usually \"Done\"), or empty if still due.")
docs = models.ManyToManyField('doc.Document', blank=True)
@ -278,7 +281,7 @@ class GroupMilestoneInfo(models.Model):
return self.desc[:20] + "..."
class Meta:
abstract = True
ordering = ['due', 'id']
ordering = [ 'order', 'id']
class GroupMilestone(GroupMilestoneInfo):
time = models.DateTimeField(auto_now=True)

View file

@ -1,4 +1,4 @@
# Copyright The IETF Trust 2009-2019, All Rights Reserved
# Copyright The IETF Trust 2009-2020, All Rights Reserved
# -*- coding: utf-8 -*-
@ -29,7 +29,8 @@ from ietf.community.utils import reset_name_contains_index_for_rule
from ietf.doc.factories import WgDraftFactory, CharterFactory
from ietf.doc.models import Document, DocAlias, DocEvent, State
from ietf.doc.utils_charter import charter_name_for_group
from ietf.group.factories import GroupFactory, RoleFactory, GroupEventFactory
from ietf.group.factories import (GroupFactory, RoleFactory, GroupEventFactory,
DatedGroupMilestoneFactory, DatelessGroupMilestoneFactory)
from ietf.group.models import Group, GroupEvent, GroupMilestone, GroupStateTransitions, Role
from ietf.group.utils import save_group_in_history, setup_default_community_list_for_group
from ietf.meeting.factories import SessionFactory
@ -1059,6 +1060,110 @@ class MilestoneTests(TestCase):
self.assertEqual(group.charter.docevent_set.count(), events_before + 2) # 1 delete, 1 add
class DatelessMilestoneTests(TestCase):
def test_switch_to_dateless(self):
ms = DatedGroupMilestoneFactory()
chair = RoleFactory(group=ms.group,name_id='chair').person
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(acronym=ms.group.acronym))
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#uses_milestone_dates')),1)
r = self.client.post(url, dict(action="switch"))
self.assertEqual(r.status_code, 200)
ms = GroupMilestone.objects.get(id=ms.id)
self.assertFalse(ms.group.uses_milestone_dates)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#uses_milestone_dates')),0)
def test_switch_to_dated(self):
ms = DatelessGroupMilestoneFactory()
chair = RoleFactory(group=ms.group,name_id='chair').person
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(acronym=ms.group.acronym))
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#uses_milestone_dates')),0)
r = self.client.post(url, dict(action="switch"))
self.assertEqual(r.status_code, 200)
ms = GroupMilestone.objects.get(id=ms.id)
self.assertTrue(ms.group.uses_milestone_dates)
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('#uses_milestone_dates')),1)
def test_add_first_milestone(self):
role = RoleFactory(name_id='chair',group__uses_milestone_dates=False)
group = role.group
chair = role.person
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(acronym=group.acronym))
login_testing_unauthorized(self, chair.user.username, url)
r = self.client.post(url, { 'prefix': "m-1",
'm-1-id': -1,
'm-1-desc': "Test 3",
'm-1-order': 1,
'm-1-resolved': "",
'm-1-docs': "",
'action': "save",
})
self.assertEqual(r.status_code, 302)
self.assertEqual(group.groupmilestone_set.count(),1)
def test_edit_and_reorder_milestone(self):
role = RoleFactory(name_id='chair',group__uses_milestone_dates=False)
group = role.group
DatelessGroupMilestoneFactory.create_batch(3,group=group)
url = urlreverse('ietf.group.milestones.edit_milestones;current', kwargs=dict(acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
post_data = dict()
prefixes = []
for ms in group.groupmilestone_set.order_by('order'):
prefix = 'm%d' % ms.id
prefixes.append(prefix)
post_data['%s-id' % prefix] = ms.id
post_data['%s-desc' % prefix] = ms.desc
post_data['%s-order' % prefix] = ms.order
post_data['%s-docs' % prefix] = ""
post_data['prefix'] = prefixes
post_data['action'] = 'review'
# Change the second milestone's description
post_data['%s-desc' % prefixes[1]] = '2s09dhfbn23tn'
# Switch the order of the first and second milestone
post_data['%s-order' % prefixes[0]] = 2
post_data['%s-order' % prefixes[1]] = 1
r = self.client.post(url, post_data)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q('.label:contains("Changed")')), 2)
post_data['action'] = 'save'
r = self.client.post(url, post_data)
self.assertEqual(r.status_code, 302)
milestones = group.groupmilestone_set.order_by('order')
self.assertEqual(milestones[0].desc,'2s09dhfbn23tn')
class CustomizeWorkflowTests(TestCase):
def test_customize_workflow(self):

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Copyright The IETF Trust 2009-2019, All Rights Reserved
# Copyright The IETF Trust 2009-2020, All Rights Reserved
#
# Portion Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved. Contact: Pasi Eronen <pasi.eronen@nokia.com>
@ -168,7 +168,11 @@ def fill_in_charter_info(group, include_drafts=False):
group.personnel.sort(key=lambda t: t[2][0].name.order)
milestone_state = "charter" if group.state_id == "proposed" else "active"
group.milestones = group.groupmilestone_set.filter(state=milestone_state).order_by('due')
group.milestones = group.groupmilestone_set.filter(state=milestone_state)
if group.uses_milestone_dates:
group.milestones = group.milestones.order_by('resolved', 'due')
else:
group.milestones = group.milestones.order_by('resolved', 'order')
if group.charter:
group.charter_text = get_charter_text(group)

View file

@ -976,3 +976,10 @@ a.fc-event, .fc-event, .fc-content, .fc-title, .fc-event-container {
/* same as button-primary */
background-image: linear-gradient(rgb(107, 91, 173) 0px, rgb(80, 68, 135) 100%)
}
/* === Edit Milestones============================================= */
#milestones-form .milestonerow, #milestones-form .extrabuttoncontainer {
padding: 8px;
border-top: 1px solid #ddd;
}

View file

@ -1,6 +1,8 @@
$(document).ready(function () {
var idCounter = -1;
var milestonesForm = $('#milestones-form');
var group_uses_milestone_dates = ( $('#uses_milestone_dates').length > 0 );
var milestone_order_has_changed = false;
// make sure we got the lowest number for idCounter
milestonesForm.find('.edit-milestone input[name$="-id"]').each(function () {
@ -12,6 +14,7 @@ $(document).ready(function () {
function setChanged() {
$(this).closest(".edit-milestone").addClass("changed");
setSubmitButtonState();
$("#switch-date-use-form").hide();
}
milestonesForm.on("change", '.edit-milestone select,.edit-milestone input,.edit-milestone textarea', setChanged);
@ -23,7 +26,7 @@ $(document).ready(function () {
function setSubmitButtonState() {
var action, label;
if (milestonesForm.find("input[name$=delete]:visible").length > 0)
if ( milestonesForm.find("input[name$=delete]:visible").length > 0 || milestone_order_has_changed )
action = "review";
else
action = "save";
@ -58,18 +61,25 @@ $(document).ready(function () {
});
milestonesForm.find(".add-milestone").click(function() {
// move Add milestone row and duplicate hidden template
var row = $(this).closest("tr"), editRow = row.next(".edit-milestone");
row.closest("table").append(row).append(editRow.clone());
var template = $("#extratemplatecontainer .extratemplate");
var templateclone = template.clone();
$("#dragdropcontainer").append(templateclone);
var new_milestone = $("#dragdropcontainer > div:last")
var new_edit_milestone = new_milestone.find(".edit-milestone");
var new_edit_milestone_order = $("#dragdropcontainer > div").length
new_milestone.removeClass("extratemplate")
new_milestone.addClass("draggable")
new_milestone.addClass("milestonerow")
// fixup template
var newId = idCounter;
--idCounter;
var prefix = "m" + newId;
editRow.find('input[name="prefix"]').val(prefix);
new_edit_milestone.find('input[name="prefix"]').val(prefix);
new_edit_milestone.find('input[name="order"]').val(new_edit_milestone_order);
editRow.find("input,select,textarea").each(function () {
new_edit_milestone.find("input,select,textarea").each(function () {
if (this.name == "prefix")
return;
@ -79,17 +89,21 @@ $(document).ready(function () {
this.name = prefix + "-" + this.name;
this.id = prefix + "-" + this.id;
});
editRow.find("label").each(function () {
new_edit_milestone.find("label").each(function () {
if (this.htmlFor)
this.htmlFor = prefix + "-" + this.htmlFor;
});
editRow.removeClass("template");
editRow.show();
new_edit_milestone.removeClass("template");
new_edit_milestone.show();
editRow.find(".select2-field").each(function () {
new_edit_milestone.find(".select2-field").each(function () {
window.setupSelect2Field($(this)); // from ietf.js
});
if ( ! group_uses_milestone_dates ) {
setOrderControlValue();
}
});
function setResolvedState() {
@ -129,6 +143,13 @@ $(document).ready(function () {
}
}
function setOrderControlValue() {
$("#dragdropcontainer > div").each(function(index){
var prefix = $(this).find('input[name="prefix"]').val();
$(this).find('input[name="'+prefix+'-order"]').val(index)
})
}
milestonesForm.find(".edit-milestone [name$=delete]").each(setDeleteState);
milestonesForm.on("change", ".edit-milestone input[name$=delete]", setDeleteState);
@ -137,4 +158,25 @@ $(document).ready(function () {
});
setSubmitButtonState();
});
if ( ! group_uses_milestone_dates) {
setOrderControlValue();
function onEnd(event) {
milestone_order_has_changed = true;
setSubmitButtonState();
setOrderControlValue();
$("#switch-date-use-form").hide();
}
var options = {
animation: 150,
draggable: ".draggable",
onEnd: function(event) {onEnd(event)}
};
var el = document.getElementById('dragdropcontainer');
var sortable = new Sortable(el, options);
}
});

View file

@ -25,64 +25,81 @@
{% endif %}
</p>
<div class="container-fluid">
<div class="col-sm-12">
<form method="post" id="switch-date-use-form">{% csrf_token %}
<button class="btn btn-default" type="submit" name="action" value="switch"{% if milestone_set == 'charter' %} style="display:none;"{% endif %}>
{% if group.uses_milestone_dates %}Stop{% else %}Start{% endif %} using milestone dates
</button>
</form>
</div>
<p class="help-block">
{% if forms %}Click a milestone to edit it.{% endif %}
{% if needs_review %}
Note that as {{ group.type.name }} Chair you cannot edit descriptions of existing
milestones and milestones you add are subject to review by the {{ reviewer }}.
{% endif %}
</p>
<div class="col-sm-12">
<p class="help-block">
{% if forms %}Click a milestone to edit it.{% endif %}
{% if forms and not group.uses_milestone_dates %}Drag and drop milestones to reorder them.{% endif %}
{% if can_reset %}
<p>
You can <a href="{% url 'ietf.group.milestones.reset_charter_milestones' group_type=group.type_id acronym=group.acronym %}">reset
this list</a> to the milestones currently in use for the {{ group.acronym }} {{ group.type.name }}.
</p>
{% endif %}
{% if needs_review %}
Note that as {{ group.type.name }} Chair you cannot edit descriptions of existing
milestones and milestones you add are subject to review by the {{ reviewer }}.
{% endif %}
</p>
{% if form_errors %}
<p class="alert alert-danger">There were errors, see below.</p>
{% endif %}
{% if can_reset %}
<p>
You can <a href="{% url 'ietf.group.milestones.reset_charter_milestones' group_type=group.type_id acronym=group.acronym %}">reset
this list</a> to the milestones currently in use for the {{ group.acronym }} {{ group.type.name }}.
</p>
{% endif %}
{% if form_errors %}
<p class="alert alert-danger">There were errors, see below.</p>
{% endif %}
</div>
</div>
<form method="post" id="milestones-form">{% csrf_token %}
<table class="table">
<div id="dragdropcontainer" class="container-fluid">
{% for form in forms %}
<tr class="milestone{% if form.delete.data %} delete{% endif %}">
<td class="due">
{% if form.milestone.resolved %}
<span class="label label-success">{{ form.milestone.resolved }}</span>
{% else %}
{{ form.milestone.due|date:"M Y" }}
{% endif %}
</td>
<td>
<div>{{ form.milestone.desc }}
{% if form.needs_review %}<span title="This milestone is not active yet, awaiting {{ reviewer }} acceptance" class="label label-warning">Awaiting accept</span>{% endif %}
{% if form.changed %}<span class="label label-info">Changed</span>{% endif %}
{% if form.delete.data %}<span class="label label-danger">Deleted</span>{% endif %}
</div>
{% for d in form.docs_names %}
<div class="doc">{{ d }}</div>
{% endfor %}
</td>
</tr>
{% for form in forms %}
<div class="row milestonerow draggable">
<span class="milestone{% if form.delete.data %} delete{% endif %}">
<span class="due handle col-sm-1">
{% if form.milestone.resolved %}
<span class="label label-success">{{ form.milestone.resolved }}</span>
{% else %}
{% if group.uses_milestone_dates %}{{ form.milestone.due|date:"M Y" }}{% endif %}
{% endif %}
</span>
<span class="col-sm-11">
<span>{{ form.milestone.desc }}
{% if form.needs_review %}<span title="This milestone is not active yet, awaiting {{ reviewer }} acceptance" class="label label-warning">Awaiting accept</span>{% endif %}
{% if form.changed %}<span class="label label-info">Changed</span>{% endif %}
{% if form.delete.data %}<span class="label label-danger">Deleted</span>{% endif %}
</span>
{% for d in form.docs_names %}
<div class="doc">{{ d }}</div>
{% endfor %}
</span>
</span>
<tr class="edit-milestone{% if form.changed %} changed{% endif %}">
<td colspan="2">{% include "group/milestone_form.html" %}</td>
</tr>
{% endfor %}
<tr>
<td></td>
<td><button type="button" class="btn btn-default add-milestone">Add extra {% if milestone_set == "chartering" %}charter{% endif%} milestone {% if needs_review %}for {{ reviewer }} review{% endif %}</button></td>
</tr>
<tr class="edit-milestone template"><td colspan="2">{% include "group/milestone_form.html" with form=empty_form %}</td></tr>
</table>
<span class="edit-milestone{% if form.changed %} changed{% endif %}">
<span colspan="2">{% include "group/milestone_form.html" %}</span>
</span>
</div>
{% endfor %}
</div>
<div class="row extrabuttoncontainer">
<div class="col-sm-1"></div>
<div class="col-sm-11"><button type="button" class="btn btn-default add-milestone">Add extra {% if milestone_set == "chartering" %}charter{% endif%} milestone {% if needs_review %}for {{ reviewer }} review{% endif %}</button></div>
</div>
<div id="extratemplatecontainer">
<div class="row extratemplate">
<div class="edit-milestone template"><div colspan="2">{% include "group/milestone_form.html" with form=empty_form %}</div></div>
</div>
</div>
{% buttons %}
<a class="btn btn-default pull-right" href="{% if milestone_set == "charter" %}{% url "ietf.doc.views_doc.document_main" name=group.charter.canonical_name %}{% else %}{{ group.about_url }}{% endif %}">Cancel</a>
@ -92,11 +109,19 @@
{% endbuttons %}
</form>
{% if group.uses_milestone_dates %}
<div id="uses_milestone_dates"></div>
{% endif %}
{% endblock %}
{% block js %}
<script src="{% static 'select2/select2.min.js' %}"></script>
<script src="{% static 'ietf/js/select2-field.js' %}"></script>
<script src="{% static 'bootstrap-datepicker/js/bootstrap-datepicker.min.js' %}"></script>
{% if not group.uses_milestone_dates %}
<script src="{% static 'Sortable/Sortable.min.js' %}"></script>
{% endif %}
<script src="{% static 'ietf/js/edit-milestones.js' %}"></script>
{% endblock %}

View file

@ -249,13 +249,7 @@
{% endif %}
{% if group.features.has_milestones %}
<h2>
{% if group.state_id == "proposed" %}
Proposed milestones
{% else %}
Milestones
{% endif %}
</h2>
{% include "group/milestones.html" with milestones=group.milestones %}
{% if milestones_in_review %}

View file

@ -1,29 +1,45 @@
{# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %}
{# assumes milestones is in context #}
<table class="table table-condensed table-striped tablesorter">
<thead>
<tr>
<th>Date</th>
<th>Milestone</th>
</tr>
</thead>
<tbody>
{% for milestone in milestones reversed %}
{# assumes group and milestones is in context #}
{% regroup milestones by resolved as milestonegroups %}
{% for milestoneset in milestonegroups %}
<h2>
{% if milestoneset.grouper %}
{{milestoneset.grouper}} milestones
{% else %}
{% if group.state_id == "proposed" %}Proposed milestones{% else %}Milestones{% endif %}
{% endif %}
</h2>
<table class="table table-condensed table-striped{% if group.uses_milestone_dates %} tablesorter{% endif %}">
<thead>
<tr>
<td class="text-nowrap">
{% if milestone.resolved %}
<span class="label label-success">{{ milestone.resolved }}</span>
{% else %}
{{ milestone.due|date:"M Y" }}
{% endif %}
</td>
<td>
{{ milestone.desc }}
{% for d in milestone.docs.all %}
<br><a href="{% url "ietf.doc.views_doc.document_main" name=d.name %}">{{ d.name }}</a>
{% endfor %}
</td>
<th>{% if group.uses_milestone_dates %}Date{% else %}Order{% endif %}</th>
<th>Milestone</th>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for milestone in milestoneset.list reversed %}
<tr>
<td class="text-nowrap">
{% if milestone.resolved %}
<span class="label label-success">{{ milestone.resolved }}</span>
{% else %}
{% if group.uses_milestone_dates %}
{{ milestone.due|date:"M Y" }}
{% else %}
{% if forloop.first %}Last{% endif %}
{% if forloop.last %}Next{% endif %}
{% endif %}
{% endif %}
</td>
<td>
{{ milestone.desc }}
{% for d in milestone.docs.all %}
<br><a href="{% url "ietf.doc.views_doc.document_main" name=d.name %}">{{ d.name }}</a>
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}

View file

@ -24,7 +24,7 @@
<label>
<input type="checkbox" name="milestone" value="{{ milestone.id }}" {% if not milestone.resolved %}checked="checked"{% endif %} />
<span class="date">{% if milestone.resolved %}{{ milestone.resolved }}{% else %}{{ milestone.due|date:"M Y" }}{% endif %}</span>
<span class="date">{% if milestone.resolved %}{{ milestone.resolved }}{% else %}{% if group.uses_milestone_dates %}{{ milestone.due|date:"M Y" }}{% endif %}{% endif %}</span>
{{ milestone.desc }}
</label>