diff --git a/ietf/group/factories.py b/ietf/group/factories.py
index aba181949..35f802189 100644
--- a/ietf/group/factories.py
+++ b/ietf/group/factories.py
@@ -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)
+
diff --git a/ietf/group/migrations/0020_add_uses_milestone_dates.py b/ietf/group/migrations/0020_add_uses_milestone_dates.py
new file mode 100644
index 000000000..1dc7c3e33
--- /dev/null
+++ b/ietf/group/migrations/0020_add_uses_milestone_dates.py
@@ -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),
+ ),
+ ]
diff --git a/ietf/group/migrations/0021_add_order_to_milestones.py b/ietf/group/migrations/0021_add_order_to_milestones.py
new file mode 100644
index 000000000..65620e78a
--- /dev/null
+++ b/ietf/group/migrations/0021_add_order_to_milestones.py
@@ -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),
+ ),
+ ]
diff --git a/ietf/group/migrations/0022_populate_uses_milestone_dates.py b/ietf/group/migrations/0022_populate_uses_milestone_dates.py
new file mode 100644
index 000000000..43ab00977
--- /dev/null
+++ b/ietf/group/migrations/0022_populate_uses_milestone_dates.py
@@ -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)
+ ]
diff --git a/ietf/group/milestones.py b/ietf/group/milestones.py
index 38502d090..0190eba47 100644
--- a/ietf/group/milestones.py
+++ b/ietf/group/milestones.py
@@ -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,
)
diff --git a/ietf/group/models.py b/ietf/group/models.py
index ceb60f23b..3aa9aafb6 100644
--- a/ietf/group/models.py
+++ b/ietf/group/models.py
@@ -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)
diff --git a/ietf/group/tests_info.py b/ietf/group/tests_info.py
index c3eb60208..852b09d69 100644
--- a/ietf/group/tests_info.py
+++ b/ietf/group/tests_info.py
@@ -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):
diff --git a/ietf/group/views.py b/ietf/group/views.py
index dd3b17ef6..4cbeb0353 100644
--- a/ietf/group/views.py
+++ b/ietf/group/views.py
@@ -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
- {% 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 %} -
++ {% 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 %} -
- You can reset - this list to the milestones currently in use for the {{ group.acronym }} {{ group.type.name }}. -
- {% 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 %} + - {% if form_errors %} -There were errors, see below.
- {% endif %} + {% if can_reset %} ++ You can reset + this list to the milestones currently in use for the {{ group.acronym }} {{ group.type.name }}. +
+ {% endif %} + + {% if form_errors %} +There were errors, see below.
+ {% endif %} +Date | -Milestone | -
---|
- {% if milestone.resolved %} - {{ milestone.resolved }} - {% else %} - {{ milestone.due|date:"M Y" }} - {% endif %} - | -
- {{ milestone.desc }}
- {% for d in milestone.docs.all %}
- {{ d.name }} - {% endfor %} - |
+ {% if group.uses_milestone_dates %}Date{% else %}Order{% endif %} | +Milestone |
---|