diff --git a/ietf/group/factories.py b/ietf/group/factories.py index aba1819494..35f8021890 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 Meta: 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 Meta: 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 0000000000..1dc7c3e337 --- /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 0000000000..bf0be03c98 --- /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', 'due', 'id']}, + ), + migrations.AlterModelOptions( + name='groupmilestonehistory', + options={'ordering': ['order', 'due', '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 0000000000..43ab009772 --- /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 38502d0909..0190eba472 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 @@ def __init__(self, needs_review, reviewer, *args, **kwargs): 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 @@ def __init__(self, needs_review, reviewer, *args, **kwargs): 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 set_attributes_from_form(f, m): 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 milestone_changed(f, m): 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 save_milestone_form(f): 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 save_milestone_form(f): 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) + action = request.POST.get("action", "review") + + 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() + 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 + 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: - continue + 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 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) + if not change: + continue - changes.append(change) - states.append(f.milestone.state_id) + 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": - return redirect('ietf.doc.views_doc.document_main', name=group.charter.canonical_name()) - else: - return HttpResponseRedirect(group.about_url()) + 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()) 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 ceb60f23bf..659920952d 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 @@ def __str__(self): return self.desc[:20] + "..." class Meta: abstract = True - ordering = ['due', 'id'] + ordering = [ 'order', 'due', '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 c3eb602087..852b09d69e 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.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 @@ def test_reset_charter_milestones(self): 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 dd3b17ef60..4cbeb0353a 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 @@ -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) diff --git a/ietf/static/ietf/css/ietf.css b/ietf/static/ietf/css/ietf.css index f7aff23c3d..28dc5082c8 100644 --- a/ietf/static/ietf/css/ietf.css +++ b/ietf/static/ietf/css/ietf.css @@ -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; +} diff --git a/ietf/static/ietf/js/edit-milestones.js b/ietf/static/ietf/js/edit-milestones.js index 5b06beb892..ed52eed023 100644 --- a/ietf/static/ietf/js/edit-milestones.js +++ b/ietf/static/ietf/js/edit-milestones.js @@ -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); + } +}); \ No newline at end of file diff --git a/ietf/templates/group/edit_milestones.html b/ietf/templates/group/edit_milestones.html index b359878434..1cff88311d 100644 --- a/ietf/templates/group/edit_milestones.html +++ b/ietf/templates/group/edit_milestones.html @@ -25,64 +25,81 @@

{{ title }}

{% endif %}

- -

- {% 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 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 %} +
+
+
{% csrf_token %} + +
+
+ + +
+

+ {% 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 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 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 %} +
+
{% csrf_token %} - - - {% for form in forms %} - - - - - - - - - {% endfor %} - - - - - - - -
- {% if form.milestone.resolved %} - {{ form.milestone.resolved }} - {% else %} - {{ form.milestone.due|date:"M Y" }} - {% endif %} - -
{{ form.milestone.desc }} - {% if form.needs_review %}Awaiting accept{% endif %} - {% if form.changed %}Changed{% endif %} - {% if form.delete.data %}Deleted{% endif %} -
- - {% for d in form.docs_names %} -
{{ d }}
- {% endfor %} -
{% include "group/milestone_form.html" %}
{% include "group/milestone_form.html" with form=empty_form %}
+
+ + {% for form in forms %} +
+ + + {% if form.milestone.resolved %} + {{ form.milestone.resolved }} + {% else %} + {% if group.uses_milestone_dates %}{{ form.milestone.due|date:"M Y" }}{% endif %} + {% endif %} + + + {{ form.milestone.desc }} + {% if form.needs_review %}Awaiting accept{% endif %} + {% if form.changed %}Changed{% endif %} + {% if form.delete.data %}Deleted{% endif %} + + + {% for d in form.docs_names %} +
{{ d }}
+ {% endfor %} +
+
+ + + {% include "group/milestone_form.html" %} + +
+ {% endfor %} +
+
+
+
+
+
+
+
{% include "group/milestone_form.html" with form=empty_form %}
+
+
{% buttons %} Cancel @@ -92,11 +109,19 @@

{{ title }}

{% endbuttons %}
+ + {% if group.uses_milestone_dates %} +
+ {% endif %} + {% endblock %} {% block js %} + {% if not group.uses_milestone_dates %} + + {% endif %} {% endblock %} diff --git a/ietf/templates/group/group_about.html b/ietf/templates/group/group_about.html index d2c6559320..a710518928 100644 --- a/ietf/templates/group/group_about.html +++ b/ietf/templates/group/group_about.html @@ -249,13 +249,7 @@

About

{% endif %} {% if group.features.has_milestones %} -

- {% if group.state_id == "proposed" %} - Proposed milestones - {% else %} - Milestones - {% endif %} -

+ {% include "group/milestones.html" with milestones=group.milestones %} {% if milestones_in_review %} diff --git a/ietf/templates/group/milestones.html b/ietf/templates/group/milestones.html index 1836459ef3..72ce605f15 100644 --- a/ietf/templates/group/milestones.html +++ b/ietf/templates/group/milestones.html @@ -1,29 +1,45 @@ {# Copyright The IETF Trust 2015, All Rights Reserved #}{% load origin %}{% origin %} -{# assumes milestones is in context #} - - - - - - - - - {% for milestone in milestones reversed %} +{# assumes group and milestones is in context #} +{% regroup milestones by resolved as milestonegroups %} + +{% for milestoneset in milestonegroups %} +

+ {% if milestoneset.grouper %} + {{milestoneset.grouper}} milestones + {% else %} + {% if group.state_id == "proposed" %}Proposed milestones{% else %}Milestones{% endif %} + {% endif %} +

+
DateMilestone
+ - - + + - {% endfor %} - -
- {% 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
+ + + {% for milestone in milestoneset.list reversed %} + + + {% if milestone.resolved %} + {{ milestone.resolved }} + {% else %} + {% if group.uses_milestone_dates %} + {{ milestone.due|date:"M Y" }} + {% else %} + {% if forloop.first %}Last{% endif %} + {% if forloop.last %}Next{% endif %} + {% endif %} + {% endif %} + + + {{ milestone.desc }} + {% for d in milestone.docs.all %} +
{{ d.name }} + {% endfor %} + + + {% endfor %} + + +{% endfor %} diff --git a/ietf/templates/group/reset_charter_milestones.html b/ietf/templates/group/reset_charter_milestones.html index 40ea5993dc..a243c23a4d 100644 --- a/ietf/templates/group/reset_charter_milestones.html +++ b/ietf/templates/group/reset_charter_milestones.html @@ -24,7 +24,7 @@

Reset Charter Milestones for {{ group.acronym }} {{ group.type.name }}