datatracker/ietf/group/views_edit.py
Henrik Levkowetz 5f053ad21a Cleaned up the remaining explicit url names, using dotted-paths to view
functions instead.  In all almost 700 changes.
 - Legacy-Id: 12923
2017-02-26 23:21:49 +00:00

541 lines
24 KiB
Python

# edit/create view for groups
import re
import datetime
from django import forms
from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseForbidden, Http404, HttpResponseRedirect
from django.utils.html import mark_safe
from django.contrib.auth.decorators import login_required
import debug # pyflakes:ignore
from ietf.doc.models import DocTagName, State
from ietf.doc.utils import get_tags_for_stream_id
from ietf.doc.utils_charter import charter_name_for_group
from ietf.group.models import ( Group, Role, GroupEvent, GroupHistory, GroupStateName,
GroupStateTransitions, GroupTypeName, GroupURL, ChangeStateGroupEvent )
from ietf.group.utils import save_group_in_history, can_manage_group, can_manage_group_type
from ietf.group.utils import get_group_or_404, setup_default_community_list_for_group
from ietf.ietfauth.utils import has_role
from ietf.person.fields import SearchableEmailsField
from ietf.person.models import Person, Email
from ietf.group.mails import ( email_admin_re_charter, email_personnel_change)
from ietf.utils.ordereddict import insert_after_in_ordered_dict
from ietf.utils.text import strip_suffix
MAX_GROUP_DELEGATES = 3
def roles_for_group_type(group_type):
roles = ["chair", "secr", "techadv", "delegate"]
if group_type == "dir":
roles.append("reviewer")
return roles
class GroupForm(forms.Form):
name = forms.CharField(max_length=80, label="Name", required=True)
acronym = forms.CharField(max_length=40, label="Acronym", required=True)
state = forms.ModelChoiceField(GroupStateName.objects.all(), label="State", required=True)
# roles
chair_roles = SearchableEmailsField(label="Chairs", required=False, only_users=True)
secr_roles = SearchableEmailsField(label="Secretaries", required=False, only_users=True)
techadv_roles = SearchableEmailsField(label="Technical Advisors", required=False, only_users=True)
delegate_roles = SearchableEmailsField(label="Delegates", required=False, only_users=True, max_entries=MAX_GROUP_DELEGATES,
help_text=mark_safe("Chairs can delegate the authority to update the state of group documents - at most %s persons at a given time." % MAX_GROUP_DELEGATES))
reviewer_roles = SearchableEmailsField(label="Reviewers", required=False, only_users=True)
ad = forms.ModelChoiceField(Person.objects.filter(role__name="ad", role__group__state="active", role__group__type='area').order_by('name'), label="Shepherding AD", 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)
urls = forms.CharField(widget=forms.Textarea, label="Additional URLs", help_text="Format: https://site/path (Optional description). Separate multiple entries with newline. Prefer HTTPS URLs where possible.", required=False)
def __init__(self, *args, **kwargs):
self.group = kwargs.pop('group', None)
self.group_type = kwargs.pop('group_type', False)
if "field" in kwargs:
field = kwargs["field"]
del kwargs["field"]
if field in roles_for_group_type(self.group_type):
field = field + "_roles"
else:
field = None
super(self.__class__, self).__init__(*args, **kwargs)
if self.group_type == "rg":
self.fields["state"].queryset = self.fields["state"].queryset.exclude(slug__in=("bof", "bof-conc"))
# if previous AD is now ex-AD, append that person to the list
ad_pk = self.initial.get('ad')
choices = self.fields['ad'].choices
if ad_pk and ad_pk not in [pk for pk, name in choices]:
self.fields['ad'].choices = list(choices) + [("", "-------"), (ad_pk, Person.objects.get(pk=ad_pk).plain_name())]
if self.group:
self.fields['acronym'].widget.attrs['readonly'] = ""
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'].initial = self.fields['parent'].queryset.first()
self.fields['parent'].widget = forms.HiddenInput()
else:
self.fields['parent'].queryset = self.fields['parent'].queryset.filter(type="area")
self.fields['parent'].label = "IETF Area"
role_fields_to_remove = (set(strip_suffix(attr, "_roles") for attr in self.fields if attr.endswith("_roles"))
- set(roles_for_group_type(self.group_type)))
for r in role_fields_to_remove:
del self.fields[r + "_roles"]
if field:
for f in self.fields:
if f != field:
del self.fields[f]
def clean_acronym(self):
# 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:
return self.group.acronym # no change permitted
acronym = self.cleaned_data['acronym'].strip().lower()
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]
confirmed = self.data.get("confirm_acronym", False)
def insert_confirm_field(label, initial):
# set required to false, we don't need it since we do the
# validation of the field in here, and otherwise the
# browser and Django may barf
insert_after_in_ordered_dict(self.fields, "confirm_acronym", forms.BooleanField(label=label, required=False), after="acronym")
# we can't set initial, it's ignored since the form is bound, instead mutate the data
self.data = self.data.copy()
self.data["confirm_acronym"] = initial
if existing and existing.type_id == self.group_type:
if existing.state_id == "bof":
insert_confirm_field(label="Turn BoF %s into proposed %s and start chartering it" % (existing.acronym, existing.type.name), initial=True)
if confirmed:
return acronym
else:
raise forms.ValidationError("Warning: Acronym used for an existing BoF (%s)." % existing.name)
else:
insert_confirm_field(label="Set state of %s %s to proposed and start chartering it" % (existing.acronym, existing.type.name), initial=False)
if confirmed:
return acronym
else:
raise forms.ValidationError("Warning: Acronym used for an existing %s (%s, %s)." % (existing.type.name, existing.name, existing.state.name if existing.state else "unknown state"))
if existing:
raise forms.ValidationError("Acronym used for an existing group (%s)." % existing.name)
old = GroupHistory.objects.filter(acronym__iexact=acronym, type__in=("wg", "rg"))
if old:
insert_confirm_field(label="Confirm reusing acronym %s" % old[0].acronym, initial=False)
if confirmed:
return acronym
else:
raise forms.ValidationError("Warning: Acronym used for a historic group.")
return acronym
def clean_urls(self):
return [x.strip() for x in self.cleaned_data["urls"].splitlines() if x.strip()]
def clean_delegates(self):
if len(self.cleaned_data["delegates"]) > MAX_GROUP_DELEGATES:
raise forms.ValidationError("At most %s delegates can be appointed at the same time, please remove %s delegates." % (
MAX_GROUP_DELEGATES, len(self.cleaned_data["delegates"]) - MAX_GROUP_DELEGATES))
return self.cleaned_data["delegates"]
def clean(self):
cleaned_data = super(GroupForm, self).clean()
state = cleaned_data.get('state', None)
parent = cleaned_data.get('parent', None)
if state and (state.slug in ['bof', ] and not parent):
raise forms.ValidationError("You requested the creation of a BoF, but specified no parent area. A parent is required when creating a bof.")
return cleaned_data
def format_urls(urls, fs="\n"):
res = []
for u in urls:
if u.name:
res.append(u"%s (%s)" % (u.url, u.name))
else:
res.append(u.url)
return fs.join(res)
## XXX Remove after testing
# def get_or_create_initial_charter(group, group_type):
# charter_name = charter_name_for_group(group)
#
# try:
# charter = Document.objects.get(docalias__name=charter_name)
# except Document.DoesNotExist:
# charter = Document(
# name=charter_name,
# type_id="charter",
# title=group.name,
# group=group,
# abstract=group.name,
# rev="00-00",
# )
# 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)
#
# return charter
#
# @login_required
# def submit_initial_charter(request, group_type=None, acronym=None):
#
# # This needs refactoring.
# # The signature assumed you could have groups with the same name, but with different types, which we do not allow.
# # Consequently, this can be called with an existing group acronym and a type
# # that doesn't match the existing group type. The code below essentially ignores the group_type argument.
# #
# # If possible, the use of get_or_create_initial_charter should be moved
# # directly into charter_submit, and this function should go away.
#
# if acronym==None:
# raise Http404
#
# group = get_object_or_404(Group, acronym=acronym)
# if not group.features.has_chartering_process:
# raise Http404
#
# # This is where we start ignoring the passed in group_type
# group_type = group.type_id
#
# if not can_manage_group(request.user, group):
# return HttpResponseForbidden("You don't have permission to access this view")
#
# if not group.charter:
# group.charter = get_or_create_initial_charter(group, group_type)
# group.save()
#
# return redirect('ietf.doc.views_charter.submit', name=group.charter.name, option="initcharter")
@login_required
def edit(request, group_type=None, acronym=None, action="edit", field=None):
"""Edit or create a group, notifying parties as
necessary and logging changes as group events."""
if action == "edit":
new_group = False
elif action in ("create","charter"):
group = None
new_group = True
else:
raise Http404
if not new_group:
group = get_group_or_404(acronym, group_type)
if not group_type and group:
group_type = group.type_id
if not (can_manage_group(request.user, group)
or group.has_role(request.user, group.features.admin_roles)):
return HttpResponseForbidden("You don't have permission to access this view")
if request.method == 'POST':
form = GroupForm(request.POST, group=group, group_type=group_type, field=field)
if form.is_valid():
clean = form.cleaned_data
if new_group:
try:
group = Group.objects.get(acronym=clean["acronym"])
save_group_in_history(group)
group.time = datetime.datetime.now()
group.save()
except Group.DoesNotExist:
group = Group.objects.create(name=clean["name"],
acronym=clean["acronym"],
type=GroupTypeName.objects.get(slug=group_type),
state=clean["state"]
)
if group.features.has_documents:
setup_default_community_list_for_group(group)
e = ChangeStateGroupEvent(group=group, type="changed_state")
e.time = group.time
e.by = request.user.person
e.state_id = clean["state"].slug
e.desc = "Group created in state %s" % clean["state"].name
e.save()
else:
save_group_in_history(group)
## XXX Remove after testing
# if action == "charter" and not group.charter: # make sure we have a charter
# group.charter = get_or_create_initial_charter(group, group_type)
changes = []
def desc(attr, new, old):
entry = "%(attr)s changed to <b>%(new)s</b> from %(old)s"
if new_group:
entry = "%(attr)s changed to <b>%(new)s</b>"
return entry % dict(attr=attr, new=new, old=old)
def diff(attr, name):
if field and attr != field:
return
v = getattr(group, attr)
if clean[attr] != v:
changes.append((attr, clean[attr], desc(name, clean[attr], v)))
setattr(group, attr, clean[attr])
# update the attributes, keeping track of what we're doing
diff('name', "Name")
diff('acronym', "Acronym")
diff('state', "State")
diff('parent', "IETF Area" if group.type=="wg" else "Group parent")
diff('list_email', "Mailing list email")
diff('list_subscribe', "Mailing list subscribe address")
diff('list_archive', "Mailing list archive")
personnel_change_text=""
changed_personnel = set()
# update roles
for attr, f in form.fields.iteritems():
if not (attr.endswith("_roles") or attr == "ad"):
continue
slug = attr
slug = strip_suffix(slug, "_roles")
title = f.label
new = clean[attr]
if attr == 'ad':
new = [ new.role_email('ad') ] if new else []
old = Email.objects.filter(role__group=group, role__name=slug).select_related("person")
if set(new) != set(old):
changes.append((attr, new, desc(title,
", ".join(x.get_name() for x in new),
", ".join(x.get_name() for x in old))))
group.role_set.filter(name=slug).delete()
for e in new:
Role.objects.get_or_create(name_id=slug, email=e, group=group, person=e.person)
added = set(new) - set(old)
deleted = set(old) - set(new)
if added:
change_text=title + ' added: ' + ", ".join(x.name_and_email() for x in added)
personnel_change_text+=change_text+"\n"
if deleted:
change_text=title + ' deleted: ' + ", ".join(x.name_and_email() for x in deleted)
personnel_change_text+=change_text+"\n"
changed_personnel.update(set(old)^set(new))
if personnel_change_text!="":
email_personnel_change(request, group, personnel_change_text, changed_personnel)
# update urls
if 'urls' in clean:
new_urls = clean['urls']
old_urls = format_urls(group.groupurl_set.order_by('url'), ", ")
if ", ".join(sorted(new_urls)) != old_urls:
changes.append(('urls', new_urls, desc('Urls', ", ".join(sorted(new_urls)), old_urls)))
group.groupurl_set.all().delete()
# Add new ones
for u in new_urls:
m = re.search('(?P<url>[\w\d:#@%/;$()~_?\+-=\\\.&]+)( \((?P<name>.+)\))?', u)
if m:
if m.group('name'):
url = GroupURL(url=m.group('url'), name=m.group('name'), group=group)
else:
url = GroupURL(url=m.group('url'), name='', group=group)
url.save()
group.time = datetime.datetime.now()
if changes and not new_group:
for attr, new, desc in changes:
if attr == 'state':
ChangeStateGroupEvent.objects.create(group=group, time=group.time, state=new, by=request.user.person, type="changed_state", desc=desc)
else:
GroupEvent.objects.create(group=group, time=group.time, by=request.user.person, type="info_changed", desc=desc)
group.save()
if action=="charter":
return redirect('ietf.doc.views_charter.submit', name=charter_name_for_group(group), option="initcharter")
return HttpResponseRedirect(group.about_url())
else: # form.is_valid()
if not new_group:
ad_role = group.ad_role()
init = dict(name=group.name,
acronym=group.acronym,
state=group.state,
ad=ad_role and ad_role.person and ad_role.person.id,
parent=group.parent.id if group.parent else None,
list_email=group.list_email if group.list_email else None,
list_subscribe=group.list_subscribe if group.list_subscribe else None,
list_archive=group.list_archive if group.list_archive else None,
urls=format_urls(group.groupurl_set.all()),
)
for slug in roles_for_group_type(group_type):
init[slug + "_roles"] = Email.objects.filter(role__group=group, role__name=slug).order_by('role__person__name')
else:
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, group_type=group_type, field=field)
return render(request, 'group/edit.html',
dict(group=group,
form=form,
action=action))
class ConcludeForm(forms.Form):
instructions = forms.CharField(widget=forms.Textarea(attrs={'rows': 30}), required=True, strip=False)
@login_required
def conclude(request, acronym, group_type=None):
"""Request the closing of group, prompting for instructions."""
group = get_group_or_404(acronym, group_type)
if not can_manage_group_type(request.user, group):
return HttpResponseForbidden("You don't have permission to access this view")
if request.method == 'POST':
form = ConcludeForm(request.POST)
if form.is_valid():
instructions = form.cleaned_data['instructions']
email_admin_re_charter(request, group, "Request closing of group", instructions, 'group_closure_requested')
e = GroupEvent(group=group, by=request.user.person)
e.type = "requested_close"
e.desc = "Requested closing group"
e.save()
kwargs = {'acronym':group.acronym}
if group_type:
kwargs['group_type'] = group_type
return redirect(group.features.about_page, **kwargs)
else:
form = ConcludeForm()
return render(request, 'group/conclude.html', {
'form': form,
'group': group,
'group_type': group_type,
})
@login_required
def customize_workflow(request, group_type=None, acronym=None):
group = get_group_or_404(acronym, group_type)
if not group_type:
group_type = group.type_id
if not group.features.customize_workflow:
raise Http404
if not (can_manage_group(request.user, group)
or group.has_role(request.user, group.features.admin_roles)):
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":
active = request.POST.get("active") == "1"
try:
state = State.objects.exclude(slug__in=MANDATORY_STATES).get(pk=request.POST.get("state"))
except State.DoesNotExist:
return HttpResponse("Invalid state %s" % request.POST.get("state"))
if active:
group.unused_states.remove(state)
else:
group.unused_states.add(state)
# redirect so the back button works correctly, otherwise
# repeated POSTs fills up the history
return redirect("ietf.group.views_edit.customize_workflow", group_type=group.type_id, acronym=group.acronym)
if action == "setnextstates":
try:
state = State.objects.get(pk=request.POST.get("state"))
except State.DoesNotExist:
return HttpResponse("Invalid state %s" % request.POST.get("state"))
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
group.groupstatetransitions_set.filter(state=state).delete()
else:
transitions, _ = GroupStateTransitions.objects.get_or_create(group=group, state=state)
transitions.next_states = next_states
return redirect("ietf.group.views_edit.customize_workflow", group_type=group.type_id, acronym=group.acronym)
if action == "settagactive":
active = request.POST.get("active") == "1"
try:
tag = DocTagName.objects.get(pk=request.POST.get("tag"))
except DocTagName.DoesNotExist:
return HttpResponse("Invalid tag %s" % request.POST.get("tag"))
if active:
group.unused_tags.remove(tag)
else:
group.unused_tags.add(tag)
return redirect("ietf.group.views_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(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-%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
s.mandatory = s.slug in MANDATORY_STATES
default_n = s.next_states.all()
if s in transitions:
n = transitions[s].next_states.all()
else:
n = default_n
s.next_states_checkboxes = [(x in n, x in default_n, x) for x in states]
s.used_next_states = [x for x in n if x.slug not in unused_states]
return render(request, 'group/customize_workflow.html', {
'group': group,
'states': states,
'tags': tags,
})