Adds support for dateless milestones. Fixes #2799. Commit ready for merge.
- Legacy-Id: 17185
This commit is contained in:
commit
42e5163b09
|
@ -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)
|
||||
|
||||
|
|
26
ietf/group/migrations/0020_add_uses_milestone_dates.py
Normal file
26
ietf/group/migrations/0020_add_uses_milestone_dates.py
Normal 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),
|
||||
),
|
||||
]
|
44
ietf/group/migrations/0021_add_order_to_milestones.py
Normal file
44
ietf/group/migrations/0021_add_order_to_milestones.py
Normal 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),
|
||||
),
|
||||
]
|
30
ietf/group/migrations/0022_populate_uses_milestone_dates.py
Normal file
30
ietf/group/migrations/0022_populate_uses_milestone_dates.py
Normal 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)
|
||||
]
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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 %}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue