Fix edit pages and milestones functionality to work with RGs - currently the IRTF Chair takes the same role as the AD
- Legacy-Id: 7541
This commit is contained in:
parent
cb1e72ad21
commit
c125ca1c64
|
@ -5,6 +5,7 @@ from django.conf import settings
|
|||
from ietf.group.models import Group, RoleHistory
|
||||
from ietf.person.models import Email
|
||||
from ietf.utils.history import get_history_object_for, copy_many_to_many_for_history
|
||||
from ietf.ietfauth.utils import has_role
|
||||
|
||||
|
||||
def save_group_in_history(group):
|
||||
|
@ -98,3 +99,18 @@ def save_milestone_in_history(milestone):
|
|||
copy_many_to_many_for_history(h, milestone)
|
||||
|
||||
return h
|
||||
|
||||
def can_manage_group_type(user, group_type):
|
||||
if group_type == "rg":
|
||||
return has_role(user, ('IRTF Chair', 'Secretariat'))
|
||||
elif group_type == "wg":
|
||||
return has_role(user, ('Area Director', 'Secretariat'))
|
||||
|
||||
return has_role(user, 'Secretariat')
|
||||
|
||||
def milestone_reviewer_for_group_type(group_type):
|
||||
if group_type == "rg":
|
||||
return "IRTF Chair"
|
||||
else:
|
||||
return "Area Director"
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
</div></div></li>
|
||||
|
||||
<li style="padding-top:0;"><a href="/wg/">Active WGs</a></li>
|
||||
<li><a href="{% url "ietf.wginfo.views.chartering_wgs" group_type="wg" %}">Chartering WGs</a></li>
|
||||
<li><a href="{% url "ietf.wginfo.views.chartering_groups" group_type="wg" %}">Chartering WGs</a></li>
|
||||
<li><a href="{% url "ietf.wginfo.views.bofs" group_type="wg" %}">BoFs</a></li>
|
||||
<li><a href="http://tools.ietf.org/wg/concluded">Concluded WGs</a></li>
|
||||
<li><a href="http://www.ietf.org/list/nonwg.html">Non-WG Lists</a></li>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Chartering or Re-Chartering Working Groups{% endblock %}
|
||||
{% block title %}Chartering or Re-Chartering Groups{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% load ietf_filters %}
|
||||
{% load ballot_icon %}
|
||||
|
||||
<h1>Chartering or Re-Chartering Working Groups</h1>
|
||||
<h1>Chartering or Re-Chartering Groups</h1>
|
||||
|
||||
<p>Groups with a charter in state
|
||||
{% for s in charter_states %}{% if not forloop.first %}, {% if forloop.last %}or {% endif %}{% endif %}<i>{{ s.name }}</i>{% endfor %}.</p>
|
|
@ -37,9 +37,10 @@
|
|||
<a href="{% url "group_charter" group_type=group.type_id acronym=group.acronym %}">{{ group.acronym }} {{ group.type.name }}</a>. Note that some states are
|
||||
mandatory for group operation and cannot be deactivated.</p>
|
||||
|
||||
{% if group.type_id == "wg" %}
|
||||
<p>You can see the default Working Group I-D State Diagram
|
||||
in <a href="http://tools.ietf.org/html/rfc6174#section-4.1">Section 4.1 of RFC6174</a>.</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<h3>States</h3>
|
||||
|
||||
|
|
|
@ -43,13 +43,14 @@ chairs and delegates, need a Datatracker account to actually do
|
|||
so. New accounts can be <a href="{% url "create_account" %}">created here</a>.</p>
|
||||
|
||||
<form class="edit" action="" method="post">{% csrf_token %}
|
||||
{% for field in form.hidden_fields %}{{ field }}{% endfor %}
|
||||
<table>
|
||||
{% for field in form.visible_fields %}
|
||||
<tr>
|
||||
<th>{{ field.label_tag }} {% if field.field.required %}*{% endif %}</th>
|
||||
<td>{{ field }}
|
||||
{% if field.name == "ad" and user|has_role:"Area Director" %}
|
||||
<label><input type="checkbox" name="ad" value="{{ login.pk }}" /> Assign to me</label>
|
||||
{% if field.name == "ad" and request.user|has_role:"Area Director" %}
|
||||
<label><input type="checkbox" name="ad" value="{{ request.user.person.pk }}" /> Assign to me</label>
|
||||
{% endif %}
|
||||
{% if field.help_text %}<div class="help">{{ field.help_text }}</div>{% endif %}
|
||||
{{ field.errors }}
|
||||
|
|
|
@ -51,8 +51,7 @@ tr.milestone.add { font-style: italic; }
|
|||
|
||||
{% 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 Area
|
||||
Director.
|
||||
milestones and milestones you add are subject to review by the {{ reviewer }}.
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
|
@ -86,7 +85,7 @@ this list</a> to the milestones currently in use for the {{ group.acronym }} {{
|
|||
|
||||
<tr class="edit-milestone{% if form.changed %} changed{% endif %}"><td colspan="2">{% include "wginfo/milestone_form.html" %}</td></tr>
|
||||
{% endfor %}
|
||||
<tr class="milestone add"><td></td><td>Add {% if milestone_set == "chartering" %}charter{% endif%} milestone {% if needs_review %}for AD review{% endif %}</td></tr>
|
||||
<tr class="milestone add"><td></td><td>Add {% if milestone_set == "chartering" %}charter{% endif%} milestone {% if needs_review %}for {{ reviewer }} review{% endif %}</td></tr>
|
||||
<tr class="edit-milestone template"><td colspan="2">{% include "wginfo/milestone_form.html" with form=empty_form %}</td></tr>
|
||||
</table>
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|||
| <a href="{{ group.list_archive }}">List Archive »</a>
|
||||
{% endif %}
|
||||
{% if group.has_tools_page %}
|
||||
| <a href="http://tools.ietf.org/wg/{{ group.acronym }}/">Tools WG Page »</a>
|
||||
| <a href="http://tools.ietf.org/{{ group.type_id }}/{{ group.acronym }}/">Tools {{ group.type.name }} Page »</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ h2 a.button { margin-left: 0.5em; font-size: 13px; }
|
|||
<div class="ietf-box ietf-group-details">
|
||||
|
||||
{% if group.state_id == "conclude" %}
|
||||
<span class="ietf-concluded-warning">Note: The data for concluded WGs
|
||||
<span class="ietf-concluded-warning">Note: The data for concluded {{ group.type.name }}s
|
||||
is occasionally incorrect.</span>
|
||||
{% endif %}
|
||||
|
||||
|
@ -162,7 +162,7 @@ is occasionally incorrect.</span>
|
|||
|
||||
{% if milestones_in_review %}
|
||||
<p>+ {{ milestones_in_review|length }} new milestone{{ milestones_in_review|pluralize }}
|
||||
currently in Area Director review.</p>
|
||||
currently in {{ milestone_reviewer }} review.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{# assumes group, form, needs_review are in the context #}
|
||||
{# assumes group, form, needs_review, reviewer are in the context #}
|
||||
<input type="hidden" name="prefix" value="{{ form.prefix|default:"" }}"/>
|
||||
{{ form.id }}
|
||||
|
||||
|
@ -35,7 +35,7 @@
|
|||
<td>Review:</td>
|
||||
<td class="accept">
|
||||
This milestone is not active yet, awaiting
|
||||
AD acceptance{% if needs_review %}.{% else %}: {{ form.accept }}{% endif %}
|
||||
{{ reviewer }} acceptance{% if needs_review %}.{% else %}: {{ form.accept }}{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
{% autoescape off %}{% filter wordwrap:73 %}{{ milestones|length }} new milestone{{ milestones|pluralize }} in "{{ group.name }}" {% if milestones|length > 1 %}need{% else %}needs{%endif %} an AD review:
|
||||
{% autoescape off %}{% filter wordwrap:73 %}{{ milestones|length }} new milestone{{ milestones|pluralize }} in "{{ group.name }}" {% if milestones|length > 1 %}need{% else %}needs{%endif %} review by the {{ reviewer }}:
|
||||
|
||||
{% for m in milestones %}"{{ m.desc }}"{% if m.days_ready != None %}
|
||||
Waiting for {{ m.days_ready }} day{{ m.days_ready|pluralize }}.{% endif %}
|
||||
|
|
|
@ -10,6 +10,7 @@ from django.shortcuts import render, get_object_or_404, redirect
|
|||
from django.http import HttpResponseForbidden
|
||||
from django.utils.html import mark_safe
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
import debug # pyflakes:ignore
|
||||
|
||||
|
@ -17,7 +18,7 @@ from ietf.doc.models import DocAlias, DocTagName, Document, State, save_document
|
|||
from ietf.doc.utils import get_tags_for_stream_id
|
||||
from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName,
|
||||
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
|
||||
from ietf.group.utils import save_group_in_history
|
||||
from ietf.group.utils import save_group_in_history, can_manage_group_type
|
||||
from ietf.ietfauth.utils import role_required, has_role
|
||||
from ietf.person.forms import EmailsField
|
||||
from ietf.person.models import Person, Email
|
||||
|
@ -34,7 +35,7 @@ class GroupForm(forms.Form):
|
|||
techadv = EmailsField(label="Technical Advisors", required=False)
|
||||
delegates = EmailsField(label="Delegates", required=False, help_text=mark_safe("Type in name to search for person<br>Chairs can delegate the authority to update the state of group documents - max %s persons at a given time" % MAX_GROUP_DELEGATES))
|
||||
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active").order_by('name'), label="Shepherding AD", empty_label="(None)", required=False)
|
||||
parent = forms.ModelChoiceField(Group.objects.filter(type="area", state="active").order_by('name'), label="IETF Area", empty_label="(None)", required=False)
|
||||
parent = forms.ModelChoiceField(Group.objects.filter(state="active").order_by('name'), empty_label="(None)", required=False)
|
||||
list_email = forms.CharField(max_length=64, required=False)
|
||||
list_subscribe = forms.CharField(max_length=255, required=False)
|
||||
list_archive = forms.CharField(max_length=255, required=False)
|
||||
|
@ -43,6 +44,7 @@ class GroupForm(forms.Form):
|
|||
def __init__(self, *args, **kwargs):
|
||||
self.group = kwargs.pop('group', None)
|
||||
self.confirmed = kwargs.pop('confirmed', False)
|
||||
self.group_type = kwargs.pop('group_type', False)
|
||||
|
||||
super(self.__class__, self).__init__(*args, **kwargs)
|
||||
|
||||
|
@ -57,11 +59,19 @@ class GroupForm(forms.Form):
|
|||
if self.group:
|
||||
self.fields['acronym'].widget.attrs['readonly'] = True
|
||||
|
||||
if self.group_type == "rg":
|
||||
self.fields['ad'].widget = forms.HiddenInput()
|
||||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(acronym="irtf")
|
||||
self.fields['parent'].widget = forms.HiddenInput()
|
||||
else:
|
||||
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(type="area")
|
||||
self.fields['parent'].label = "IETF Area"
|
||||
|
||||
def clean_acronym(self):
|
||||
self.confirm_msg = ""
|
||||
self.autoenable_confirm = False
|
||||
|
||||
# Changing the acronym of an already existing GROUP will cause 404s all
|
||||
# Changing the acronym of an already existing group will cause 404s all
|
||||
# over the place, loose history, and generally muck up a lot of
|
||||
# things, so we don't permit it
|
||||
if self.group:
|
||||
|
@ -69,15 +79,15 @@ class GroupForm(forms.Form):
|
|||
|
||||
acronym = self.cleaned_data['acronym'].strip().lower()
|
||||
|
||||
# be careful with acronyms, requiring confirmation to take existing or override historic
|
||||
if not re.match(r'^[a-z][a-z0-9]+$', acronym):
|
||||
raise forms.ValidationError("Acronym is invalid, must be at least two characters and only contain lowercase letters and numbers starting with a letter.")
|
||||
|
||||
# be careful with acronyms, requiring confirmation to take existing or override historic
|
||||
existing = Group.objects.filter(acronym__iexact=acronym)
|
||||
if existing:
|
||||
existing = existing[0]
|
||||
|
||||
if existing and existing.type_id == "wg":
|
||||
if existing and existing.type_id == self.group_type:
|
||||
if self.confirmed:
|
||||
return acronym # take over confirmed
|
||||
|
||||
|
@ -93,7 +103,7 @@ class GroupForm(forms.Form):
|
|||
if existing:
|
||||
raise forms.ValidationError("Acronym used for an existing group (%s)." % existing.name)
|
||||
|
||||
old = GroupHistory.objects.filter(acronym__iexact=acronym, type="wg")
|
||||
old = GroupHistory.objects.filter(acronym__iexact=acronym, type__in=("wg", "rg"))
|
||||
if old and not self.confirmed:
|
||||
self.confirm_msg = "Confirm reusing acronym %s" % old[0].acronym
|
||||
self.autoenable_confirm = False
|
||||
|
@ -120,12 +130,19 @@ def format_urls(urls, fs="\n"):
|
|||
res.append(u.url)
|
||||
return fs.join(res)
|
||||
|
||||
def get_or_create_initial_charter(group):
|
||||
def get_or_create_initial_charter(group, group_type):
|
||||
if group_type == "rg":
|
||||
top_org = "irtf"
|
||||
else:
|
||||
top_org = "ietf"
|
||||
|
||||
charter_name = "charter-%s-%s" % (top_org, group.acronym)
|
||||
|
||||
try:
|
||||
charter = Document.objects.get(docalias__name="charter-ietf-%s" % group.acronym)
|
||||
charter = Document.objects.get(docalias__name=charter_name)
|
||||
except Document.DoesNotExist:
|
||||
charter = Document(
|
||||
name="charter-ietf-" + group.acronym,
|
||||
name=charter_name,
|
||||
type_id="charter",
|
||||
title=group.name,
|
||||
group=group,
|
||||
|
@ -135,26 +152,29 @@ def get_or_create_initial_charter(group):
|
|||
charter.save()
|
||||
charter.set_state(State.objects.get(used=True, type="charter", slug="notrev"))
|
||||
|
||||
# Create an alias as well
|
||||
DocAlias.objects.create(
|
||||
name=charter.name,
|
||||
document=charter
|
||||
)
|
||||
# Create an alias as well
|
||||
DocAlias.objects.create(name=charter.name, document=charter)
|
||||
|
||||
return charter
|
||||
|
||||
@role_required('Area Director', 'Secretariat')
|
||||
@login_required
|
||||
def submit_initial_charter(request, group_type, acronym=None):
|
||||
if not can_manage_group_type(request.user, group_type):
|
||||
return HttpResponseForbidden("You don't have permission to access this view")
|
||||
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
if not group.charter:
|
||||
group.charter = get_or_create_initial_charter(group)
|
||||
group.charter = get_or_create_initial_charter(group, group_type)
|
||||
group.save()
|
||||
return redirect('charter_submit', name=group.charter.name, option="initcharter")
|
||||
|
||||
@role_required('Area Director', 'Secretariat')
|
||||
@login_required
|
||||
def edit(request, group_type, acronym=None, action="edit"):
|
||||
"""Edit or create a GROUP, notifying parties as
|
||||
"""Edit or create a group, notifying parties as
|
||||
necessary and logging changes as group events."""
|
||||
if not can_manage_group_type(request.user, group_type):
|
||||
return HttpResponseForbidden("You don't have permission to access this view")
|
||||
|
||||
if action == "edit":
|
||||
group = get_object_or_404(Group, acronym=acronym)
|
||||
new_group = False
|
||||
|
@ -164,10 +184,8 @@ def edit(request, group_type, acronym=None, action="edit"):
|
|||
else:
|
||||
raise Http404
|
||||
|
||||
login = request.user.person
|
||||
|
||||
if request.method == 'POST':
|
||||
form = GroupForm(request.POST, group=group, confirmed=request.POST.get("confirmed", False))
|
||||
form = GroupForm(request.POST, group=group, confirmed=request.POST.get("confirmed", False), group_type=group_type)
|
||||
if form.is_valid():
|
||||
clean = form.cleaned_data
|
||||
if new_group:
|
||||
|
@ -185,7 +203,7 @@ def edit(request, group_type, acronym=None, action="edit"):
|
|||
|
||||
e = ChangeStateGroupEvent(group=group, type="changed_state")
|
||||
e.time = group.time
|
||||
e.by = login
|
||||
e.by = request.user.person
|
||||
e.state_id = clean["state"].slug
|
||||
e.desc = "Group created in state %s" % clean["state"].name
|
||||
e.save()
|
||||
|
@ -193,8 +211,8 @@ def edit(request, group_type, acronym=None, action="edit"):
|
|||
save_group_in_history(group)
|
||||
|
||||
|
||||
if action=="charter" and not group.charter: # make sure we have a charter
|
||||
group.charter = get_or_create_initial_charter(group)
|
||||
if action == "charter" and not group.charter: # make sure we have a charter
|
||||
group.charter = get_or_create_initial_charter(group, group_type)
|
||||
|
||||
changes = []
|
||||
|
||||
|
@ -266,7 +284,7 @@ def edit(request, group_type, acronym=None, action="edit"):
|
|||
|
||||
if changes and not new_group:
|
||||
for c in changes:
|
||||
GroupEvent.objects.create(group=group, by=login, type="info_changed", desc=c)
|
||||
GroupEvent.objects.create(group=group, by=request.user.person, type="info_changed", desc=c)
|
||||
|
||||
group.save()
|
||||
|
||||
|
@ -291,28 +309,27 @@ def edit(request, group_type, acronym=None, action="edit"):
|
|||
urls=format_urls(group.groupurl_set.all()),
|
||||
)
|
||||
else:
|
||||
init = dict(ad=login.id if has_role(request.user, "Area Director") else None,
|
||||
init = dict(ad=request.user.person.id if group_type == "wg" and has_role(request.user, "Area Director") else None,
|
||||
)
|
||||
form = GroupForm(initial=init, group=group)
|
||||
form = GroupForm(initial=init, group=group, group_type=group_type)
|
||||
|
||||
return render(request, 'wginfo/edit.html',
|
||||
dict(group=group,
|
||||
form=form,
|
||||
action=action,
|
||||
user=request.user,
|
||||
login=login))
|
||||
action=action))
|
||||
|
||||
|
||||
|
||||
class ConcludeForm(forms.Form):
|
||||
instructions = forms.CharField(widget=forms.Textarea(attrs={'rows': 30}), required=True)
|
||||
|
||||
@role_required('Area Director','Secretariat')
|
||||
@login_required
|
||||
def conclude(request, group_type, acronym):
|
||||
"""Request the closing of group, prompting for instructions."""
|
||||
group = get_object_or_404(Group, type=group_type, acronym=acronym)
|
||||
|
||||
login = request.user.person
|
||||
if not can_manage_group_type(request.user, group_type):
|
||||
return HttpResponseForbidden("You don't have permission to access this view")
|
||||
|
||||
if request.method == 'POST':
|
||||
form = ConcludeForm(request.POST)
|
||||
|
@ -321,7 +338,7 @@ def conclude(request, group_type, acronym):
|
|||
|
||||
email_secretariat(request, group, "Request closing of group", instructions)
|
||||
|
||||
e = GroupEvent(group=group, by=login)
|
||||
e = GroupEvent(group=group, by=request.user.person)
|
||||
e.type = "requested_close"
|
||||
e.desc = "Requested closing group"
|
||||
e.save()
|
||||
|
@ -334,13 +351,20 @@ def conclude(request, group_type, acronym):
|
|||
dict(form=form, group=group))
|
||||
|
||||
|
||||
@login_required
|
||||
def customize_workflow(request, group_type, acronym):
|
||||
MANDATORY_STATES = ('c-adopt', 'wg-doc', 'sub-pub')
|
||||
|
||||
group = get_object_or_404(Group, type=group_type, acronym=acronym)
|
||||
if not request.user.is_authenticated() or not (has_role(request.user, "Secretariat") or group.role_set.filter(name="chair", person__user=request.user)):
|
||||
if (not has_role(request.user, "Secretariat") and
|
||||
not group.role_set.filter(name="chair", person__user=request.user)):
|
||||
return HttpResponseForbidden("You don't have permission to access this view")
|
||||
|
||||
if group_type == "rg":
|
||||
stream_id = "irtf"
|
||||
MANDATORY_STATES = ('candidat', 'active', 'rfc-edit', 'pub', 'dead')
|
||||
else:
|
||||
stream_id = "ietf"
|
||||
MANDATORY_STATES = ('c-adopt', 'wg-doc', 'sub-pub')
|
||||
|
||||
if request.method == 'POST':
|
||||
action = request.POST.get("action")
|
||||
if action == "setstateactive":
|
||||
|
@ -365,7 +389,7 @@ def customize_workflow(request, group_type, acronym):
|
|||
except State.DoesNotExist:
|
||||
return HttpResponse("Invalid state %s" % request.POST.get("state"))
|
||||
|
||||
next_states = State.objects.filter(used=True, type='draft-stream-ietf', pk__in=request.POST.getlist("next_states"))
|
||||
next_states = State.objects.filter(used=True, type='draft-stream-%s' % stream_id, pk__in=request.POST.getlist("next_states"))
|
||||
unused = group.unused_states.all()
|
||||
if set(next_states.exclude(pk__in=unused)) == set(state.next_states.exclude(pk__in=unused)):
|
||||
# just use the default
|
||||
|
@ -390,15 +414,14 @@ def customize_workflow(request, group_type, acronym):
|
|||
|
||||
return redirect("ietf.wginfo.edit.customize_workflow", group_type=group.type_id, acronym=group.acronym)
|
||||
|
||||
|
||||
# put some info for the template on tags and states
|
||||
unused_tags = group.unused_tags.all().values_list('slug', flat=True)
|
||||
tags = DocTagName.objects.filter(slug__in=get_tags_for_stream_id("ietf"))
|
||||
tags = DocTagName.objects.filter(slug__in=get_tags_for_stream_id(stream_id))
|
||||
for t in tags:
|
||||
t.used = t.slug not in unused_tags
|
||||
|
||||
unused_states = group.unused_states.all().values_list('slug', flat=True)
|
||||
states = State.objects.filter(used=True, type="draft-stream-ietf")
|
||||
states = State.objects.filter(used=True, type="draft-stream-%s" % stream_id)
|
||||
transitions = dict((o.state, o) for o in group.groupstatetransitions_set.all())
|
||||
for s in states:
|
||||
s.used = s.slug not in unused_states
|
||||
|
|
|
@ -10,8 +10,8 @@ from django.conf import settings
|
|||
from django.core.urlresolvers import reverse as urlreverse
|
||||
|
||||
from ietf.utils.mail import send_mail, send_mail_text
|
||||
|
||||
from ietf.group.models import Group
|
||||
from ietf.group.utils import milestone_reviewer_for_group_type
|
||||
|
||||
def email_secretariat(request, group, subject, text):
|
||||
to = ["iesg-secretary@ietf.org"]
|
||||
|
@ -37,10 +37,12 @@ def email_milestones_changed(request, group, changes):
|
|||
u"Milestones changed for %s %s" % (group.acronym, group.type.name),
|
||||
text)
|
||||
|
||||
# first send to AD and chairs
|
||||
# first send to management and chairs
|
||||
to = []
|
||||
if group.ad:
|
||||
to.append(group.ad.role_email("ad").formatted_email())
|
||||
elif group.type_id == "rg":
|
||||
to.append("IRTF Chair <irtf-chair@irtf.org>")
|
||||
|
||||
for r in group.role_set.filter(name="chair"):
|
||||
to.append(r.formatted_email())
|
||||
|
@ -48,7 +50,7 @@ def email_milestones_changed(request, group, changes):
|
|||
if to:
|
||||
wrap_up_email(to, u"\n\n".join(c + "." for c in changes))
|
||||
|
||||
# then send to WG
|
||||
# then send to group
|
||||
if group.list_email:
|
||||
review_re = re.compile("Added .* for review, due")
|
||||
to = [ group.list_email ]
|
||||
|
@ -56,11 +58,17 @@ def email_milestones_changed(request, group, changes):
|
|||
|
||||
|
||||
def email_milestone_review_reminder(group, grace_period=7):
|
||||
"""Email reminders about milestones needing review to AD."""
|
||||
if not group.ad:
|
||||
"""Email reminders about milestones needing review to management."""
|
||||
to = []
|
||||
|
||||
if group.ad:
|
||||
to.append(group.ad.role_email("ad").formatted_email())
|
||||
elif group.type_id == "rg":
|
||||
to.append("IRTF Chair <irtf-chair@irtf.org>")
|
||||
|
||||
if not to:
|
||||
return False
|
||||
|
||||
to = [group.ad.role_email("ad").formatted_email()]
|
||||
cc = [r.formatted_email() for r in group.role_set.filter(name="chair")]
|
||||
|
||||
now = datetime.datetime.now()
|
||||
|
@ -84,6 +92,7 @@ def email_milestone_review_reminder(group, grace_period=7):
|
|||
"wginfo/reminder_milestones_need_review.txt",
|
||||
dict(group=group,
|
||||
milestones=milestones,
|
||||
reviewer=milestone_reviewer_for_group_type(group.type_id),
|
||||
url=settings.IDTRACKER_BASE_URL + urlreverse("group_edit_milestones", kwargs=dict(group_type=group.type_id, acronym=group.acronym)),
|
||||
cc=cc,
|
||||
)
|
||||
|
|
|
@ -6,13 +6,13 @@ import json
|
|||
|
||||
from django import forms
|
||||
from django.http import HttpResponse, HttpResponseForbidden, HttpResponseBadRequest
|
||||
from django.shortcuts import render_to_response, get_object_or_404, redirect
|
||||
from django.template import RequestContext
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from ietf.doc.models import Document, DocEvent
|
||||
from ietf.doc.utils import get_chartering_type
|
||||
from ietf.group.models import Group, GroupMilestone, MilestoneGroupEvent
|
||||
from ietf.group.utils import save_milestone_in_history
|
||||
from ietf.group.utils import save_milestone_in_history, can_manage_group_type, milestone_reviewer_for_group_type
|
||||
from ietf.ietfauth.utils import role_required, has_role
|
||||
from ietf.name.models import GroupMilestoneStateName
|
||||
from ietf.wginfo.mails import email_milestones_changed
|
||||
|
@ -108,23 +108,19 @@ class MilestoneForm(forms.Form):
|
|||
|
||||
return r
|
||||
|
||||
|
||||
@role_required('WG Chair', 'Area Director', 'Secretariat')
|
||||
@login_required
|
||||
def edit_milestones(request, group_type, acronym, milestone_set="current"):
|
||||
# milestones_set + needs_review: we have several paths into this view
|
||||
# AD/Secr. -> all actions on current + add new
|
||||
# management (IRTF chair/AD/...)/Secr. -> all actions on current + add new
|
||||
# group chair -> limited actions on current + add new for review
|
||||
# (re)charter -> all actions on existing in state charter + add new in state charter
|
||||
#
|
||||
# For charters we store the history on the charter document to not confuse people.
|
||||
|
||||
login = request.user.person
|
||||
|
||||
group = get_object_or_404(Group, type=group_type, acronym=acronym)
|
||||
|
||||
needs_review = False
|
||||
if not has_role(request.user, ("Area Director", "Secretariat")):
|
||||
if group.role_set.filter(name="chair", person=login):
|
||||
if not can_manage_group_type(request.user, group_type):
|
||||
if group.role_set.filter(name="chair", person__user=request.user):
|
||||
if milestone_set == "current":
|
||||
needs_review = True
|
||||
else:
|
||||
|
@ -298,10 +294,10 @@ def edit_milestones(request, group_type, acronym, milestone_set="current"):
|
|||
|
||||
if milestone_set == "charter":
|
||||
DocEvent.objects.create(doc=group.charter, type="changed_charter_milestone",
|
||||
by=login, desc=change)
|
||||
by=request.user.person, desc=change)
|
||||
else:
|
||||
MilestoneGroupEvent.objects.create(group=group, type="changed_milestone",
|
||||
by=login, desc=change, milestone=f.milestone)
|
||||
by=request.user.person, desc=change, milestone=f.milestone)
|
||||
|
||||
changes.append(change)
|
||||
|
||||
|
@ -322,27 +318,25 @@ def edit_milestones(request, group_type, acronym, milestone_set="current"):
|
|||
|
||||
forms.sort(key=lambda f: f.milestone.due if f.milestone else datetime.date.max)
|
||||
|
||||
return render_to_response('wginfo/edit_milestones.html',
|
||||
dict(group=group,
|
||||
title=title,
|
||||
forms=forms,
|
||||
form_errors=form_errors,
|
||||
empty_form=empty_form,
|
||||
milestone_set=milestone_set,
|
||||
finished_milestone_text=finished_milestone_text,
|
||||
needs_review=needs_review,
|
||||
can_reset=can_reset),
|
||||
context_instance=RequestContext(request))
|
||||
return render(request, 'wginfo/edit_milestones.html',
|
||||
dict(group=group,
|
||||
title=title,
|
||||
forms=forms,
|
||||
form_errors=form_errors,
|
||||
empty_form=empty_form,
|
||||
milestone_set=milestone_set,
|
||||
finished_milestone_text=finished_milestone_text,
|
||||
needs_review=needs_review,
|
||||
reviewer=milestone_reviewer_for_group_type(group_type),
|
||||
can_reset=can_reset))
|
||||
|
||||
@role_required('WG Chair', 'Area Director', 'Secretariat')
|
||||
@login_required
|
||||
def reset_charter_milestones(request, group_type, acronym):
|
||||
"""Reset charter milestones to the currently in-use milestones."""
|
||||
login = request.user.person
|
||||
|
||||
group = get_object_or_404(Group, type=group_type, acronym=acronym)
|
||||
|
||||
if (not has_role(request.user, ("Area Director", "Secretariat")) and
|
||||
not group.role_set.filter(name="chair", person=login)):
|
||||
if (not can_manage_group_type(request.user, group_type) and
|
||||
not group.role_set.filter(name="chair", person__user=request.user)):
|
||||
return HttpResponseForbidden("You are not chair of this group.")
|
||||
|
||||
current_milestones = group.groupmilestone_set.filter(state="active")
|
||||
|
@ -364,7 +358,7 @@ def reset_charter_milestones(request, group_type, acronym):
|
|||
DocEvent.objects.create(type="changed_charter_milestone",
|
||||
doc=group.charter,
|
||||
desc='Deleted milestone "%s"' % m.desc,
|
||||
by=login,
|
||||
by=request.user.person,
|
||||
)
|
||||
|
||||
# add current
|
||||
|
@ -380,18 +374,17 @@ def reset_charter_milestones(request, group_type, acronym):
|
|||
DocEvent.objects.create(type="changed_charter_milestone",
|
||||
doc=group.charter,
|
||||
desc='Added milestone "%s", due %s, from current group milestones' % (new.desc, new.due.strftime("%B %Y")),
|
||||
by=login,
|
||||
by=request.user.person,
|
||||
)
|
||||
|
||||
|
||||
return redirect('group_edit_charter_milestones', group_type=group.type_id, acronym=group.acronym)
|
||||
|
||||
return render_to_response('wginfo/reset_charter_milestones.html',
|
||||
dict(group=group,
|
||||
charter_milestones=charter_milestones,
|
||||
current_milestones=current_milestones,
|
||||
),
|
||||
context_instance=RequestContext(request))
|
||||
return render('wginfo/reset_charter_milestones.html',
|
||||
dict(group=group,
|
||||
charter_milestones=charter_milestones,
|
||||
current_milestones=current_milestones,
|
||||
))
|
||||
|
||||
|
||||
def ajax_search_docs(request, group_type, acronym):
|
||||
|
|
|
@ -90,12 +90,12 @@ class GroupPagesTests(TestCase):
|
|||
self.assertTrue(chair.address in r.content)
|
||||
self.assertTrue("This is a charter." in r.content)
|
||||
|
||||
def test_chartering_wgs(self):
|
||||
def test_chartering_groups(self):
|
||||
draft = make_test_data()
|
||||
group = draft.group
|
||||
group.charter.set_state(State.objects.get(used=True, type="charter", slug="intrev"))
|
||||
|
||||
url = urlreverse('ietf.wginfo.views.chartering_wgs', kwargs=dict(group_type="wg"))
|
||||
url = urlreverse('ietf.wginfo.views.chartering_groups', kwargs=dict(group_type="wg"))
|
||||
r = self.client.get(url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
q = PyQuery(r.content)
|
||||
|
|
|
@ -14,7 +14,7 @@ urlpatterns = patterns('',
|
|||
(r'^1wg-summary-by-acronym.txt', views.wg_summary_acronym),
|
||||
(r'^1wg-charters.txt', views.wg_charters),
|
||||
(r'^1wg-charters-by-acronym.txt', views.wg_charters_by_acronym),
|
||||
(r'^chartering/$', views.chartering_wgs),
|
||||
(r'^chartering/$', views.chartering_groups),
|
||||
(r'^bofs/$', views.bofs),
|
||||
(r'^chartering/create/$', edit.edit, {'action': "charter"}, "group_create"),
|
||||
(r'^bofs/create/$', edit.edit, {'action': "create"}, "bof_create"),
|
||||
|
|
|
@ -45,11 +45,11 @@ from django.views.decorators.cache import cache_page
|
|||
from django.db.models import Q
|
||||
|
||||
from ietf.doc.views_search import SearchForm, retrieve_search_results
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.doc.models import State, DocAlias, RelatedDocument
|
||||
from ietf.doc.utils import get_chartering_type
|
||||
from ietf.group.utils import get_charter_text
|
||||
from ietf.doc.templatetags.ietf_filters import clean_whitespace
|
||||
from ietf.group.models import Group, Role
|
||||
from ietf.group.utils import get_charter_text, can_manage_group_type, milestone_reviewer_for_group_type
|
||||
from ietf.ietfauth.utils import has_role
|
||||
from ietf.utils.pipe import pipe
|
||||
|
||||
|
@ -148,7 +148,8 @@ def active_groups(request, group_type):
|
|||
return active_wgs(request)
|
||||
elif group_type == "rg":
|
||||
return active_rgs(request)
|
||||
raise Http404
|
||||
else:
|
||||
raise Http404
|
||||
|
||||
def active_wgs(request):
|
||||
areas = Group.objects.filter(type="area", state="active").order_by("name")
|
||||
|
@ -178,14 +179,14 @@ def bofs(request, group_type):
|
|||
groups = Group.objects.filter(type=group_type, state="bof")
|
||||
return render(request, 'wginfo/bofs.html',dict(groups=groups))
|
||||
|
||||
def chartering_wgs(request, group_type):
|
||||
def chartering_groups(request, group_type):
|
||||
charter_states = State.objects.filter(used=True, type="charter").exclude(slug__in=("approved", "notrev"))
|
||||
groups = Group.objects.filter(type=group_type, charter__states__in=charter_states).select_related("state", "charter")
|
||||
|
||||
for g in groups:
|
||||
g.chartering_type = get_chartering_type(g.charter)
|
||||
|
||||
return render(request, 'wginfo/chartering_wgs.html',
|
||||
return render(request, 'wginfo/chartering_groups.html',
|
||||
dict(charter_states=charter_states,
|
||||
groups=groups))
|
||||
|
||||
|
@ -195,18 +196,18 @@ def construct_group_menu_context(request, group, selected, others):
|
|||
actions = []
|
||||
|
||||
is_chair = group.has_role(request.user, "chair")
|
||||
is_ad_or_secretariat = has_role(request.user, ("Area Director", "Secretariat"))
|
||||
can_manage = can_manage_group_type(request.user, group.type_id)
|
||||
|
||||
if group.state_id != "proposed" and (is_chair or is_ad_or_secretariat):
|
||||
if group.state_id != "proposed" and (is_chair or can_manage):
|
||||
actions.append((u"Add or edit milestones", urlreverse("group_edit_milestones", kwargs=dict(group_type=group.type_id, acronym=group.acronym))))
|
||||
|
||||
if group.state_id != "conclude" and is_ad_or_secretariat:
|
||||
if group.state_id != "conclude" and can_manage:
|
||||
actions.append((u"Edit group", urlreverse("group_edit", kwargs=dict(group_type=group.type_id, acronym=group.acronym))))
|
||||
|
||||
if is_chair or is_ad_or_secretariat:
|
||||
if is_chair or can_manage:
|
||||
actions.append((u"Customize workflow", urlreverse("ietf.wginfo.edit.customize_workflow", kwargs=dict(group_type=group.type_id, acronym=group.acronym))))
|
||||
|
||||
if group.state_id in ("active", "dormant") and is_ad_or_secretariat:
|
||||
if group.state_id in ("active", "dormant") and can_manage:
|
||||
actions.append((u"Request closing group", urlreverse("ietf.wginfo.edit.conclude", kwargs=dict(group_type=group.type_id, acronym=group.acronym))))
|
||||
|
||||
d = {
|
||||
|
@ -307,6 +308,7 @@ def group_charter(request, group_type, acronym):
|
|||
return render(request, 'wginfo/group_charter.html',
|
||||
construct_group_menu_context(request, group, "charter", {
|
||||
"milestones_in_review": group.groupmilestone_set.filter(state="review"),
|
||||
"milestone_reviewer": milestone_reviewer_for_group_type(group_type),
|
||||
"requested_close": requested_close,
|
||||
"long_group_type":long_group_types.get(group_type, "Group")
|
||||
}))
|
||||
|
|
Loading…
Reference in a new issue