Code reorganization, doing away with multiple urls_* and views_ files in ietf.group. No intentional functionality changes.

- Legacy-Id: 13709
This commit is contained in:
Henrik Levkowetz 2017-06-22 15:18:08 +00:00
parent c28b919e26
commit ce1b655fa2
30 changed files with 1707 additions and 1704 deletions

132
ietf/group/dot.py Normal file
View file

@ -0,0 +1,132 @@
# Copyright The IETF Trust 2007, All Rights Reserved
# -*- check-flake8 -*-
from __future__ import unicode_literals, print_function
from django.db.models import Q
from django.template.loader import render_to_string
from ietf.doc.models import RelatedDocument
class Edge(object):
def __init__(self, relateddocument):
self.relateddocument = relateddocument
def __hash__(self):
return hash("|".join([str(hash(nodename(self.relateddocument.source.name))),
str(hash(nodename(self.relateddocument.target.document.name))),
self.relateddocument.relationship.slug]))
def __eq__(self, other):
return self.__hash__() == other.__hash__()
def sourcename(self):
return nodename(self.relateddocument.source.name)
def targetname(self):
return nodename(self.relateddocument.target.document.name)
def styles(self):
# Note that the old style=dotted, color=red styling is never used
if self.relateddocument.is_downref():
return { 'color': 'red', 'arrowhead': 'normalnormal' }
else:
styles = { 'refnorm' : { 'color': 'blue' },
'refinfo' : { 'color': 'green' },
'refold' : { 'color': 'orange' },
'refunk' : { 'style': 'dashed' },
'replaces': { 'color': 'pink', 'style': 'dashed', 'arrowhead': 'diamond' },
}
return styles[self.relateddocument.relationship.slug]
def nodename(name):
return name.replace('-', '_')
def get_node_styles(node, group):
styles = dict()
# Shape and style (note that old diamond shape is never used
styles['style'] = 'filled'
if node.get_state('draft').slug == 'rfc':
styles['shape'] = 'box'
elif node.get_state('draft-iesg') and not node.get_state('draft-iesg').slug in ['watching', 'dead']:
styles['shape'] = 'parallelogram'
elif node.get_state('draft').slug == 'expired':
styles['shape'] = 'house'
styles['style'] = 'solid'
styles['peripheries'] = 3
elif node.get_state('draft').slug == 'repl':
styles['shape'] = 'ellipse'
styles['style'] = 'solid'
styles['peripheries'] = 3
else:
pass # quieter form of styles['shape'] = 'ellipse'
# Color (note that the old 'Flat out red' is never used
if node.group.acronym == 'none':
styles['color'] = '"#FF800D"' # orangeish
elif node.group == group:
styles['color'] = '"#0AFE47"' # greenish
else:
styles['color'] = '"#9999FF"' # blueish
# Label
label = node.name
if label.startswith('draft-'):
if label.startswith('draft-ietf-'):
label = label[11:]
else:
label = label[6:]
try:
t = label.index('-')
label = r"%s\n%s" % (label[:t], label[t+1:])
except:
pass
if node.group.acronym != 'none' and node.group != group:
label = "(%s) %s" % (node.group.acronym, label)
if node.get_state('draft').slug == 'rfc':
label = "%s\\n(%s)" % (label, node.canonical_name())
styles['label'] = '"%s"' % label
return styles
def make_dot(group):
references = Q(source__group=group, source__type='draft', relationship__slug__startswith='ref')
both_rfcs = Q(source__states__slug='rfc', target__document__states__slug='rfc')
inactive = Q(source__states__slug__in=['expired', 'repl'])
attractor = Q(target__name__in=['rfc5000', 'rfc5741'])
removed = Q(source__states__slug__in=['auth-rm', 'ietf-rm'])
relations = ( RelatedDocument.objects.filter(references).exclude(both_rfcs)
.exclude(inactive).exclude(attractor).exclude(removed) )
edges = set()
for x in relations:
target_state = x.target.document.get_state_slug('draft')
if target_state != 'rfc' or x.is_downref():
edges.add(Edge(x))
replacements = RelatedDocument.objects.filter(relationship__slug='replaces',
target__document__in=[x.relateddocument.target.document for x in edges])
for x in replacements:
edges.add(Edge(x))
nodes = set([x.relateddocument.source for x in edges]).union([x.relateddocument.target.document for x in edges])
for node in nodes:
node.nodename = nodename(node.name)
node.styles = get_node_styles(node, group)
return render_to_string('group/dot.txt',
dict( nodes=nodes, edges=edges )
)

View file

@ -41,5 +41,5 @@ class GroupFeatures(object):
if group in active_review_teams():
self.has_reviews = True
import ietf.group.views
self.default_tab = ietf.group.views_review.review_requests
self.default_tab = ietf.group.views.review_requests

318
ietf/group/forms.py Normal file
View file

@ -0,0 +1,318 @@
# Copyright The IETF Trust 2007, All Rights Reserved
from __future__ import unicode_literals, print_function
# Stdlib imports
import re
# Django imports
from django import forms
from django.utils.html import mark_safe
# IETF imports
from ietf.group.models import Group, GroupHistory, GroupStateName
from ietf.person.fields import SearchableEmailsField, PersonEmailChoiceField
from ietf.person.models import Person
from ietf.review.models import ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
from ietf.review.utils import close_review_request_states, setup_reviewer_field
from ietf.utils.textupload import get_cleaned_text_file_content
from ietf.utils.text import strip_suffix
from ietf.utils.ordereddict import insert_after_in_ordered_dict
from ietf.utils.fields import DatepickerDateField, MultiEmailField
# --- Constants --------------------------------------------------------
MAX_GROUP_DELEGATES = 3
# --- Utility Functions ------------------------------------------------
def roles_for_group_type(group_type):
roles = ["chair", "secr", "techadv", "delegate", ]
if group_type == "dir":
roles.append("reviewer")
return roles
# --- Forms ------------------------------------------------------------
class StatusUpdateForm(forms.Form):
content = forms.CharField(widget=forms.Textarea, label='Status update', help_text = 'Edit the status update', required=False, strip=False)
txt = forms.FileField(label='.txt format', help_text='Or upload a .txt file', required=False)
def clean_content(self):
return self.cleaned_data['content'].replace('\r','')
def clean_txt(self):
return get_cleaned_text_file_content(self.cleaned_data["txt"])
class ConcludeGroupForm(forms.Form):
instructions = forms.CharField(widget=forms.Textarea(attrs={'rows': 30}), required=True, strip=False)
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_parent(self):
p = self.cleaned_data["parent"]
seen = set()
if self.group:
seen.add(self.group)
while p != None and p not in seen:
seen.add(p)
p = p.parent
if p is None:
return self.cleaned_data["parent"]
else:
raise forms.ValidationError("A group cannot be its own ancestor. "
"Found that the group '%s' would end up being the ancestor of (%s)" % (p.acronym, ', '.join([g.acronym for g in seen])))
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
class StreamEditForm(forms.Form):
delegates = SearchableEmailsField(required=False, only_users=True)
# ----------------------------------------------------------------------
class ManageReviewRequestForm(forms.Form):
ACTIONS = [
("assign", "Assign"),
("close", "Close"),
]
action = forms.ChoiceField(choices=ACTIONS, widget=forms.HiddenInput, required=False)
close = forms.ModelChoiceField(queryset=close_review_request_states(), required=False)
reviewer = PersonEmailChoiceField(empty_label="(None)", required=False, label_with="person")
add_skip = forms.BooleanField(required=False)
def __init__(self, review_req, *args, **kwargs):
if not "prefix" in kwargs:
if review_req.pk is None:
kwargs["prefix"] = "r{}-{}".format(review_req.type_id, review_req.doc_id)
else:
kwargs["prefix"] = "r{}".format(review_req.pk)
super(ManageReviewRequestForm, self).__init__(*args, **kwargs)
if review_req.pk is None:
self.fields["close"].queryset = self.fields["close"].queryset.filter(slug__in=["no-review-version", "no-review-document"])
close_initial = None
if review_req.pk is None:
close_initial = "no-review-version"
elif review_req.reviewer:
close_initial = "no-response"
else:
close_initial = "overtaken"
if close_initial:
self.fields["close"].initial = close_initial
self.fields["close"].widget.attrs["class"] = "form-control input-sm"
setup_reviewer_field(self.fields["reviewer"], review_req)
self.fields["reviewer"].widget.attrs["class"] = "form-control input-sm"
if self.is_bound:
if self.data.get("action") == "close":
self.fields["close"].required = True
class EmailOpenAssignmentsForm(forms.Form):
frm = forms.CharField(label="From", widget=forms.EmailInput(attrs={"readonly":True}))
to = MultiEmailField()
cc = MultiEmailField(required=False)
reply_to = MultiEmailField(required=False)
subject = forms.CharField()
body = forms.CharField(widget=forms.Textarea, strip=False)
class ReviewerSettingsForm(forms.ModelForm):
class Meta:
model = ReviewerSettings
fields = ['min_interval', 'filter_re', 'skip_next', 'remind_days_before_deadline','expertise']
def __init__(self, *args, **kwargs):
exclude_fields = kwargs.pop('exclude_fields', [])
super(ReviewerSettingsForm, self).__init__(*args, **kwargs)
for field_name in exclude_fields:
self.fields.pop(field_name)
def clean_skip_next(self):
skip_next = self.cleaned_data.get('skip_next')
if skip_next < 0:
raise forms.ValidationError("Skip next must not be negative")
return skip_next
class AddUnavailablePeriodForm(forms.ModelForm):
class Meta:
model = UnavailablePeriod
fields = ['start_date', 'end_date', 'availability']
def __init__(self, *args, **kwargs):
super(AddUnavailablePeriodForm, self).__init__(*args, **kwargs)
self.fields["start_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["start_date"].label, help_text=self.fields["start_date"].help_text, required=self.fields["start_date"].required)
self.fields["end_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["end_date"].label, help_text=self.fields["end_date"].help_text, required=self.fields["end_date"].required)
self.fields['availability'].widget = forms.RadioSelect(choices=UnavailablePeriod.LONG_AVAILABILITY_CHOICES)
def clean(self):
start = self.cleaned_data.get("start_date")
end = self.cleaned_data.get("end_date")
if start and end and start > end:
self.add_error("start_date", "Start date must be before or equal to end date.")
return self.cleaned_data
class EndUnavailablePeriodForm(forms.Form):
def __init__(self, start_date, *args, **kwargs):
super(EndUnavailablePeriodForm, self).__init__(*args, **kwargs)
self.fields["end_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1", "start-date": start_date.isoformat() if start_date else "" })
self.start_date = start_date
def clean_end_date(self):
end = self.cleaned_data["end_date"]
if self.start_date and end < self.start_date:
raise forms.ValidationError("End date must be equal to or come after start date.")
return end
class ReviewSecretarySettingsForm(forms.ModelForm):
class Meta:
model = ReviewSecretarySettings
fields = ['remind_days_before_deadline']

View file

@ -36,7 +36,7 @@ else:
class StreamTests(TestCase):
def test_streams(self):
make_test_data()
r = self.client.get(urlreverse("ietf.group.views_stream.streams"))
r = self.client.get(urlreverse("ietf.group.views.streams"))
self.assertEqual(r.status_code, 200)
self.assertTrue("Independent Submission Editor" in unicontent(r))
@ -45,7 +45,7 @@ class StreamTests(TestCase):
draft.stream_id = "iab"
draft.save_with_history([DocEvent.objects.create(doc=draft, rev=draft.rev, type="changed_stream", by=Person.objects.get(user__username="secretary"), desc="Test")])
r = self.client.get(urlreverse("ietf.group.views_stream.stream_documents", kwargs=dict(acronym="iab")))
r = self.client.get(urlreverse("ietf.group.views.stream_documents", kwargs=dict(acronym="iab")))
self.assertEqual(r.status_code, 200)
self.assertTrue(draft.name in unicontent(r))
@ -54,7 +54,7 @@ class StreamTests(TestCase):
stream_acronym = "ietf"
url = urlreverse("ietf.group.views_stream.stream_edit", kwargs=dict(acronym=stream_acronym))
url = urlreverse("ietf.group.views.stream_edit", kwargs=dict(acronym=stream_acronym))
login_testing_unauthorized(self, "secretary", url)
# get

View file

@ -8,6 +8,7 @@ import StringIO
from pyquery import PyQuery
from tempfile import NamedTemporaryFile
import debug # pyflakes:ignore
from django.conf import settings
@ -317,7 +318,7 @@ class GroupPagesTests(TestCase):
self.assertTrue(group.acronym in unicontent(r))
self.assertTrue(group.description in unicontent(r))
for url in group_urlreverse_list(group, 'ietf.group.views_edit.edit'):
for url in group_urlreverse_list(group, 'ietf.group.views.edit'):
for username in can_edit[group.type_id]:
verify_can_edit_group(url, group, username)
@ -432,7 +433,7 @@ class GroupEditTests(TestCase):
def test_create(self):
make_test_data()
url = urlreverse('ietf.group.views_edit.edit', kwargs=dict(group_type="wg", action="charter"))
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type="wg", action="charter"))
login_testing_unauthorized(self, "secretary", url)
num_wgs = len(Group.objects.filter(type="wg"))
@ -491,7 +492,7 @@ class GroupEditTests(TestCase):
make_test_data()
url = urlreverse('ietf.group.views_edit.edit', kwargs=dict(group_type="rg", action="charter"))
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type="rg", action="charter"))
login_testing_unauthorized(self, "secretary", url)
irtf = Group.objects.get(acronym='irtf')
@ -516,7 +517,7 @@ class GroupEditTests(TestCase):
def test_create_based_on_existing_bof(self):
make_test_data()
url = urlreverse('ietf.group.views_edit.edit', kwargs=dict(group_type="wg", action="charter"))
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type="wg", action="charter"))
login_testing_unauthorized(self, "secretary", url)
group = Group.objects.get(acronym="mars")
@ -551,7 +552,7 @@ class GroupEditTests(TestCase):
make_test_data()
group = Group.objects.get(acronym="mars")
url = urlreverse('ietf.group.views_edit.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit"))
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit"))
login_testing_unauthorized(self, "secretary", url)
# normal get
@ -629,7 +630,7 @@ class GroupEditTests(TestCase):
group = Group.objects.get(acronym="mars")
# Edit name
url = urlreverse('ietf.group.views_edit.edit', kwargs=dict(acronym=group.acronym, action="edit", field="name"))
url = urlreverse('ietf.group.views.edit', kwargs=dict(acronym=group.acronym, action="edit", field="name"))
login_testing_unauthorized(self, "secretary", url)
# normal get
r = self.client.get(url)
@ -646,7 +647,7 @@ class GroupEditTests(TestCase):
# Edit list email
url = urlreverse('ietf.group.views_edit.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit", field="list_email"))
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit", field="list_email"))
# normal get
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@ -666,7 +667,7 @@ class GroupEditTests(TestCase):
review_req = make_review_data(doc)
group = review_req.team
url = urlreverse('ietf.group.views_edit.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit"))
url = urlreverse('ietf.group.views.edit', kwargs=dict(group_type=group.type_id, acronym=group.acronym, action="edit"))
login_testing_unauthorized(self, "secretary", url)
# normal get
@ -700,7 +701,7 @@ class GroupEditTests(TestCase):
group = Group.objects.get(acronym="mars")
url = urlreverse('ietf.group.views_edit.conclude', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
url = urlreverse('ietf.group.views.conclude', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
# normal get
@ -1009,7 +1010,7 @@ class CustomizeWorkflowTests(TestCase):
group = Group.objects.get(acronym="mars")
url = urlreverse('ietf.group.views_edit.customize_workflow', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
url = urlreverse('ietf.group.views.customize_workflow', kwargs=dict(group_type=group.type_id, acronym=group.acronym))
login_testing_unauthorized(self, "secretary", url)
state = State.objects.get(used=True, type="draft-stream-ietf", slug="wg-lc")
@ -1138,7 +1139,7 @@ class AjaxTests(TestCase):
def test_group_menu_data(self):
make_test_data()
r = self.client.get(urlreverse('ietf.group.views_ajax.group_menu_data'))
r = self.client.get(urlreverse('ietf.group.views.group_menu_data'))
self.assertEqual(r.status_code, 200)
parents = json.loads(r.content)

View file

@ -17,7 +17,7 @@ from ietf.review.utils import (
review_requests_needing_secretary_reminder, email_secretary_reminder,
)
from ietf.name.models import ReviewTypeName, ReviewResultName, ReviewRequestStateName
import ietf.group.views_review
import ietf.group.views
from ietf.utils.mail import outbox, empty_outbox
from ietf.dbtemplate.factories import DBTemplateFactory
from ietf.person.factories import PersonFactory
@ -29,14 +29,14 @@ class ReviewTests(TestCase):
group = review_req.team
for url in [urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym }),
urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym , 'group_type': group.type_id})]:
for url in [urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym }),
urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym , 'group_type': group.type_id})]:
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(review_req.doc.name in unicontent(r))
self.assertTrue(unicode(review_req.reviewer.person) in unicontent(r))
url = urlreverse(ietf.group.views_review.review_requests, kwargs={ 'acronym': group.acronym })
url = urlreverse(ietf.group.views.review_requests, kwargs={ 'acronym': group.acronym })
# close request, listed under closed
review_req.state = ReviewRequestStateName.objects.get(slug="completed")
@ -138,8 +138,8 @@ class ReviewTests(TestCase):
group = review_req1.team
# get
for url in [urlreverse(ietf.group.views_review.reviewer_overview, kwargs={ 'acronym': group.acronym }),
urlreverse(ietf.group.views_review.reviewer_overview, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })]:
for url in [urlreverse(ietf.group.views.reviewer_overview, kwargs={ 'acronym': group.acronym }),
urlreverse(ietf.group.views.reviewer_overview, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })]:
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertTrue(unicode(reviewer) in unicontent(r))
@ -155,12 +155,12 @@ class ReviewTests(TestCase):
group = review_req1.team
url = urlreverse(ietf.group.views_review.manage_review_requests, kwargs={ 'acronym': group.acronym, "assignment_status": "assigned" })
url = urlreverse(ietf.group.views.manage_review_requests, kwargs={ 'acronym': group.acronym, "assignment_status": "assigned" })
login_testing_unauthorized(self, "secretary", url)
assigned_url = urlreverse(ietf.group.views_review.manage_review_requests, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id, "assignment_status": "assigned" })
unassigned_url = urlreverse(ietf.group.views_review.manage_review_requests, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id, "assignment_status": "unassigned" })
assigned_url = urlreverse(ietf.group.views.manage_review_requests, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id, "assignment_status": "assigned" })
unassigned_url = urlreverse(ietf.group.views.manage_review_requests, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id, "assignment_status": "unassigned" })
review_req2 = ReviewRequest.objects.create(
doc=review_req1.doc,
@ -310,11 +310,11 @@ class ReviewTests(TestCase):
group = review_req1.team
url = urlreverse(ietf.group.views_review.email_open_review_assignments, kwargs={ 'acronym': group.acronym })
url = urlreverse(ietf.group.views.email_open_review_assignments, kwargs={ 'acronym': group.acronym })
login_testing_unauthorized(self, "secretary", url)
url = urlreverse(ietf.group.views_review.email_open_review_assignments, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })
url = urlreverse(ietf.group.views.email_open_review_assignments, kwargs={ 'acronym': group.acronym, 'group_type': group.type_id })
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
@ -351,14 +351,14 @@ class ReviewTests(TestCase):
reviewer = review_req.reviewer.person
url = urlreverse(ietf.group.views_review.change_reviewer_settings, kwargs={
url = urlreverse(ietf.group.views.change_reviewer_settings, kwargs={
"acronym": review_req.team.acronym,
"reviewer_email": review_req.reviewer_id,
})
login_testing_unauthorized(self, reviewer.user.username, url)
url = urlreverse(ietf.group.views_review.change_reviewer_settings, kwargs={
url = urlreverse(ietf.group.views.change_reviewer_settings, kwargs={
"group_type": review_req.team.type_id,
"acronym": review_req.team.acronym,
"reviewer_email": review_req.reviewer_id,
@ -472,13 +472,13 @@ class ReviewTests(TestCase):
secretary = Person.objects.get(user__username="reviewsecretary")
url = urlreverse(ietf.group.views_review.change_review_secretary_settings, kwargs={
url = urlreverse(ietf.group.views.change_review_secretary_settings, kwargs={
"acronym": review_req.team.acronym,
})
login_testing_unauthorized(self, secretary.user.username, url)
url = urlreverse(ietf.group.views_review.change_review_secretary_settings, kwargs={
url = urlreverse(ietf.group.views.change_review_secretary_settings, kwargs={
"group_type": review_req.team.type_id,
"acronym": review_req.team.acronym,
})

View file

@ -1,21 +1,90 @@
# Copyright The IETF Trust 2007, All Rights Reserved
from django.conf.urls import include
from django.conf import settings
from django.conf.urls import include
from django.views.generic import RedirectView
from ietf.group import views, views_ajax, views_edit
from ietf.community import views as community_views
from ietf.doc import views_material as material_views
from ietf.group import views, milestones as milestone_views
from ietf.utils.urls import url
urlpatterns = [
# These are not used externally, only in include statements further down:
info_detail_urls = [
url(r'^$', views.group_home),
url(r'^documents/txt/$', views.group_documents_txt),
url(r'^documents/$', views.group_documents),
url(r'^documents/manage/$', community_views.manage_list),
url(r'^documents/csv/$', community_views.export_to_csv),
url(r'^documents/feed/$', community_views.feed),
url(r'^documents/subscription/$', community_views.subscription),
url(r'^charter/$', views.group_about),
url(r'^about/$', views.group_about),
url(r'^about/status/$', views.group_about_status),
url(r'^about/status/edit/$', views.group_about_status_edit),
url(r'^about/status/meeting/(?P<num>\d+)/$', views.group_about_status_meeting),
url(r'^history/$',views.history),
url(r'^email/$', views.email),
url(r'^deps/(?P<output_type>[\w-]+)/$', views.dependencies),
url(r'^meetings/$', views.meetings),
url(r'^edit/$', views.edit, {'action': "edit"}),
url(r'^edit/(?P<field>\w+)/?$', views.edit, {'action': "edit"}),
url(r'^conclude/$', views.conclude),
url(r'^milestones/$', milestone_views.edit_milestones, {'milestone_set': "current"}, name='ietf.group.milestones.edit_milestones;current'),
url(r'^milestones/charter/$', milestone_views.edit_milestones, {'milestone_set': "charter"}, name='ietf.group.milestones.edit_milestones;charter'),
url(r'^milestones/charter/reset/$', milestone_views.reset_charter_milestones, None, 'ietf.group.milestones.reset_charter_milestones'),
url(r'^workflow/$', views.customize_workflow),
url(r'^materials/$', views.materials),
url(r'^materials/new/$', material_views.choose_material_type),
url(r'^materials/new/(?P<doc_type>[\w-]+)/$', material_views.edit_material, { 'action': "new" }, 'ietf.doc.views_material.edit_material'),
url(r'^archives/$', views.derived_archives),
url(r'^photos/$', views.group_photos),
url(r'^reviews/$', views.review_requests),
url(r'^reviews/manage/(?P<assignment_status>assigned|unassigned)/$', views.manage_review_requests),
url(r'^reviews/email-assignments/$', views.email_open_review_assignments),
url(r'^reviewers/$', views.reviewer_overview),
url(r'^reviewers/(?P<reviewer_email>[\w%+-.@]+)/settings/$', views.change_reviewer_settings),
url(r'^secretarysettings/$', views.change_review_secretary_settings),
url(r'^email-aliases/$', RedirectView.as_view(pattern_name=views.email,permanent=False),name='ietf.group.urls_info_details.redirect.email'),
]
group_urls = [
url(r'^$', views.active_groups),
url(r'^groupmenu.json', views_ajax.group_menu_data, None, 'ietf.group.views_ajax.group_menu_data'),
url(r'^%(acronym)s.json$' % settings.URL_REGEXPS, views_ajax.group_json),
url(r'^groupmenu.json', views.group_menu_data, None, 'ietf.group.views.group_menu_data'),
url(r'^%(acronym)s.json$' % settings.URL_REGEXPS, views.group_json),
url(r'^chartering/$', views.chartering_groups),
url(r'^chartering/create/(?P<group_type>(wg|rg))/$', views_edit.edit, {'action': "charter"}),
url(r'^chartering/create/(?P<group_type>(wg|rg))/$', views.edit, {'action': "charter"}),
url(r'^concluded/$', views.concluded_groups),
url(r'^email-aliases/$', views.email_aliases),
url(r'^all-status/$', views.all_status),
#
url(r'^%(acronym)s/$' % settings.URL_REGEXPS, views.group_home),
url(r'^%(acronym)s/' % settings.URL_REGEXPS, include('ietf.group.urls_info_details')),
url(r'^%(acronym)s/' % settings.URL_REGEXPS, include(info_detail_urls)),
]
stream_urls = [
url(r'^$', views.streams),
url(r'^%(acronym)s/$' % settings.URL_REGEXPS, views.stream_documents, None),
url(r'^%(acronym)s/edit/$' % settings.URL_REGEXPS, views.stream_edit),
]
grouptype_urls = [
url(r'^$', views.active_groups),
url(r'^summary.txt', RedirectView.as_view(url='/wg/1wg-summary.txt', permanent=True)),
url(r'^summary-by-area.txt', RedirectView.as_view(url='/wg/1wg-summary.txt', permanent=True)),
url(r'^summary-by-acronym.txt', RedirectView.as_view(url='/wg/1wg-summary-by-acronym.txt', permanent=True)),
url(r'^1wg-summary.txt', views.wg_summary_area),
url(r'^1wg-summary-by-acronym.txt', views.wg_summary_acronym),
url(r'^1wg-charters.txt', views.wg_charters),
url(r'^1wg-charters-by-acronym.txt', views.wg_charters_by_acronym),
url(r'^chartering/$', RedirectView.as_view(url='/group/chartering/', permanent=True)),
url(r'^chartering/create/$', RedirectView.as_view(url='/group/chartering/create/%(group_type)s/', permanent=True)),
url(r'^bofs/$', views.bofs),
url(r'^email-aliases/$', views.email_aliases),
url(r'^bofs/create/$', views.edit, {'action': "create", }),
url(r'^photos/$', views.chair_photos),
url(r'^%(acronym)s/' % settings.URL_REGEXPS, include(info_detail_urls)),
]

View file

@ -1,26 +0,0 @@
# Copyright The IETF Trust 2008, All Rights Reserved
from django.conf.urls import include
from django.views.generic import RedirectView
from django.conf import settings
from ietf.group import views, views_edit
from ietf.utils.urls import url
urlpatterns = [
url(r'^$', views.active_groups),
url(r'^summary.txt', RedirectView.as_view(url='/wg/1wg-summary.txt', permanent=True)),
url(r'^summary-by-area.txt', RedirectView.as_view(url='/wg/1wg-summary.txt', permanent=True)),
url(r'^summary-by-acronym.txt', RedirectView.as_view(url='/wg/1wg-summary-by-acronym.txt', permanent=True)),
url(r'^1wg-summary.txt', views.wg_summary_area),
url(r'^1wg-summary-by-acronym.txt', views.wg_summary_acronym),
url(r'^1wg-charters.txt', views.wg_charters),
url(r'^1wg-charters-by-acronym.txt', views.wg_charters_by_acronym),
url(r'^chartering/$', RedirectView.as_view(url='/group/chartering/', permanent=True)),
url(r'^chartering/create/$', RedirectView.as_view(url='/group/chartering/create/%(group_type)s/', permanent=True)),
url(r'^bofs/$', views.bofs),
url(r'^email-aliases/$', views.email_aliases),
url(r'^bofs/create/$', views_edit.edit, {'action': "create", }),
url(r'^photos/$', views.chair_photos),
url(r'^%(acronym)s/' % settings.URL_REGEXPS, include('ietf.group.urls_info_details')),
]

View file

@ -1,44 +0,0 @@
from django.views.generic import RedirectView
from ietf.community import views as community_views
from ietf.doc import views_material as material_views
from ietf.group import views, views_edit, views_review, milestones as milestone_views
from ietf.utils.urls import url
urlpatterns = [
url(r'^$', views.group_home),
url(r'^documents/txt/$', views.group_documents_txt),
url(r'^documents/$', views.group_documents),
url(r'^documents/manage/$', community_views.manage_list),
url(r'^documents/csv/$', community_views.export_to_csv),
url(r'^documents/feed/$', community_views.feed),
url(r'^documents/subscription/$', community_views.subscription),
url(r'^charter/$', views.group_about),
url(r'^about/$', views.group_about),
url(r'^about/status/$', views.group_about_status),
url(r'^about/status/edit/$', views.group_about_status_edit),
url(r'^about/status/meeting/(?P<num>\d+)/$', views.group_about_status_meeting),
url(r'^history/$',views.history),
url(r'^email/$', views.email),
url(r'^deps/(?P<output_type>[\w-]+)/$', views.dependencies),
url(r'^meetings/$', views.meetings),
url(r'^edit/$', views_edit.edit, {'action': "edit"}),
url(r'^edit/(?P<field>\w+)/?$', views_edit.edit, {'action': "edit"}),
url(r'^conclude/$', views_edit.conclude),
url(r'^milestones/$', milestone_views.edit_milestones, {'milestone_set': "current"}, name='ietf.group.milestones.edit_milestones;current'),
url(r'^milestones/charter/$', milestone_views.edit_milestones, {'milestone_set': "charter"}, name='ietf.group.milestones.edit_milestones;charter'),
url(r'^milestones/charter/reset/$', milestone_views.reset_charter_milestones, None, 'ietf.group.milestones.reset_charter_milestones'),
url(r'^workflow/$', views_edit.customize_workflow),
url(r'^materials/$', views.materials),
url(r'^materials/new/$', material_views.choose_material_type),
url(r'^materials/new/(?P<doc_type>[\w-]+)/$', material_views.edit_material, { 'action': "new" }, 'ietf.doc.views_material.edit_material'),
url(r'^archives/$', views.derived_archives),
url(r'^photos/$', views.group_photos),
url(r'^reviews/$', views_review.review_requests),
url(r'^reviews/manage/(?P<assignment_status>assigned|unassigned)/$', views_review.manage_review_requests),
url(r'^reviews/email-assignments/$', views_review.email_open_review_assignments),
url(r'^reviewers/$', views_review.reviewer_overview),
url(r'^reviewers/(?P<reviewer_email>[\w%+-.@]+)/settings/$', views_review.change_reviewer_settings),
url(r'^secretarysettings/$', views_review.change_review_secretary_settings),
url(r'^email-aliases/$', RedirectView.as_view(pattern_name=views.email,permanent=False),name='ietf.group.urls_info_details.redirect.email'),
]

View file

@ -1,12 +0,0 @@
# Copyright The IETF Trust 2008, All Rights Reserved
from django.conf import settings
from ietf.group import views_stream
from ietf.utils.urls import url
urlpatterns = [
url(r'^$', views_stream.streams),
url(r'^%(acronym)s/$' % settings.URL_REGEXPS, views_stream.stream_documents, None),
url(r'^%(acronym)s/edit/$' % settings.URL_REGEXPS, views_stream.stream_edit),
]

View file

@ -177,9 +177,9 @@ def construct_group_menu_context(request, group, selected, group_type, others):
if group.features.has_materials and get_group_materials(group).exists():
entries.append(("Materials", urlreverse("ietf.group.views.materials", kwargs=kwargs)))
if group.features.has_reviews:
import ietf.group.views_review
entries.append(("Review requests", urlreverse(ietf.group.views_review.review_requests, kwargs=kwargs)))
entries.append(("Reviewers", urlreverse(ietf.group.views_review.reviewer_overview, kwargs=kwargs)))
import ietf.group.views
entries.append(("Review requests", urlreverse(ietf.group.views.review_requests, kwargs=kwargs)))
entries.append(("Reviewers", urlreverse(ietf.group.views.reviewer_overview, kwargs=kwargs)))
if group.type_id in ('rg','wg','team'):
entries.append(("Meetings", urlreverse("ietf.group.views.meetings", kwargs=kwargs)))
entries.append(("History", urlreverse("ietf.group.views.history", kwargs=kwargs)))
@ -214,23 +214,23 @@ def construct_group_menu_context(request, group, selected, group_type, others):
actions.append((u"Upload material", urlreverse("ietf.doc.views_material.choose_material_type", kwargs=kwargs)))
if group.features.has_reviews and can_manage_review_requests_for_team(request.user, group):
import ietf.group.views_review
actions.append((u"Manage unassigned reviews", urlreverse(ietf.group.views_review.manage_review_requests, kwargs=dict(assignment_status="unassigned", **kwargs))))
actions.append((u"Manage assigned reviews", urlreverse(ietf.group.views_review.manage_review_requests, kwargs=dict(assignment_status="assigned", **kwargs))))
import ietf.group.views
actions.append((u"Manage unassigned reviews", urlreverse(ietf.group.views.manage_review_requests, kwargs=dict(assignment_status="unassigned", **kwargs))))
actions.append((u"Manage assigned reviews", urlreverse(ietf.group.views.manage_review_requests, kwargs=dict(assignment_status="assigned", **kwargs))))
if Role.objects.filter(name="secr", group=group, person__user=request.user).exists():
actions.append((u"Secretary settings", urlreverse(ietf.group.views_review.change_review_secretary_settings, kwargs=kwargs)))
actions.append((u"Secretary settings", urlreverse(ietf.group.views.change_review_secretary_settings, kwargs=kwargs)))
if group.state_id != "conclude" and (is_admin or can_manage):
can_edit_group = True
actions.append((u"Edit group", urlreverse("ietf.group.views_edit.edit", kwargs=dict(kwargs, action="edit"))))
actions.append((u"Edit group", urlreverse("ietf.group.views.edit", kwargs=dict(kwargs, action="edit"))))
if group.features.customize_workflow and (is_admin or can_manage):
actions.append((u"Customize workflow", urlreverse("ietf.group.views_edit.customize_workflow", kwargs=kwargs)))
actions.append((u"Customize workflow", urlreverse("ietf.group.views.customize_workflow", kwargs=kwargs)))
if group.state_id in ("active", "dormant") and not group.type_id in ["sdo", "rfcedtyp", "isoc", ] and can_manage:
actions.append((u"Request closing group", urlreverse("ietf.group.views_edit.conclude", kwargs=kwargs)))
actions.append((u"Request closing group", urlreverse("ietf.group.views.conclude", kwargs=kwargs)))
d = {
"group": group,

File diff suppressed because it is too large Load diff

View file

@ -1,29 +0,0 @@
import json
from collections import defaultdict
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse as urlreverse
from django.utils.html import escape
from django.views.decorators.cache import cache_page, cache_control
from ietf.group.models import Group
def group_json(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
return HttpResponse(json.dumps(group.json_dict(request.build_absolute_uri('/')),
sort_keys=True, indent=2),
content_type="text/json")
@cache_control(public=True, max_age=30*60)
@cache_page(30 * 60)
def group_menu_data(request):
groups = Group.objects.filter(state="active", type__in=("wg", "rg"), parent__state="active").order_by("acronym")
groups_by_parent = defaultdict(list)
for g in groups:
url = urlreverse("ietf.group.views.group_home", kwargs={ 'group_type': g.type_id, 'acronym': g.acronym })
groups_by_parent[g.parent_id].append({ 'acronym': g.acronym, 'name': escape(g.name), 'url': url })
return JsonResponse(groups_by_parent)

View file

@ -1,558 +0,0 @@
# 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,
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
# This function, in addition to encapsulating a group's roles list for
# readability, also ensures that the roles with edit buttons in forms
# are the same which are accepted byt the GroupForm. Please adjust the
# list here if you change the *_roles fields the GroupForm knows about.
def roles_for_group_type(group_type):
roles = ["chair", "secr", "techadv", "delegate", ]
if group_type == "dir":
roles.append("reviewer")
return roles
MAX_GROUP_DELEGATES = 3
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_parent(self):
p = self.cleaned_data["parent"]
seen = set()
if self.group:
seen.add(self.group)
while p != None and p not in seen:
seen.add(p)
p = p.parent
if p is None:
return self.cleaned_data["parent"]
else:
raise forms.ValidationError("A group cannot be its own ancestor. "
"Found that the group '%s' would end up being the ancestor of (%s)" % (p.acronym, ', '.join([g.acronym for g in seen])))
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,
})

View file

@ -1,682 +0,0 @@
import datetime, math
from collections import defaultdict
import debug # pyflakes:ignore
from django.shortcuts import render, redirect, get_object_or_404
from django.http import Http404, HttpResponseForbidden, HttpResponseRedirect
from django.contrib.auth.decorators import login_required
from django.urls import reverse as urlreverse
from django.db.models import Max
from django import forms
from django.template.loader import render_to_string
from ietf.review.models import ReviewRequest, ReviewerSettings, UnavailablePeriod, ReviewSecretarySettings
from ietf.review.utils import (can_manage_review_requests_for_team,
can_access_review_stats_for_team,
close_review_request_states,
extract_revision_ordered_review_requests_for_documents_and_replaced,
assign_review_request_to_reviewer,
close_review_request,
setup_reviewer_field,
suggested_review_requests_for_team,
unavailable_periods_to_list,
current_unavailable_periods_for_reviewers,
email_reviewer_availability_change,
reviewer_rotation_list,
latest_review_requests_for_reviewers,
augment_review_requests_with_events,
get_default_filter_re,
days_needed_to_fulfill_min_interval_for_reviewers,
)
from ietf.doc.models import LastCallDocEvent
from ietf.group.models import Role
from ietf.group.utils import get_group_or_404, construct_group_menu_context
from ietf.person.fields import PersonEmailChoiceField
from ietf.name.models import ReviewRequestStateName
from ietf.utils.mail import send_mail_text, parse_preformatted
from ietf.utils.fields import DatepickerDateField, MultiEmailField
from ietf.ietfauth.utils import user_is_person
from ietf.dbtemplate.models import DBTemplate
from ietf.mailtrigger.utils import gather_address_lists
from ietf.mailtrigger.models import Recipient
def get_open_review_requests_for_team(team, assignment_status=None):
open_review_requests = ReviewRequest.objects.filter(
team=team,
state__in=("requested", "accepted")
).prefetch_related(
"reviewer__person", "type", "state", "doc", "doc__states",
).order_by("-time", "-id")
if assignment_status == "unassigned":
open_review_requests = suggested_review_requests_for_team(team) + list(open_review_requests.filter(reviewer=None))
elif assignment_status == "assigned":
open_review_requests = list(open_review_requests.exclude(reviewer=None))
else:
open_review_requests = suggested_review_requests_for_team(team) + list(open_review_requests)
today = datetime.date.today()
unavailable_periods = current_unavailable_periods_for_reviewers(team)
for r in open_review_requests:
if r.reviewer:
r.reviewer_unavailable = any(p.availability == "unavailable"
for p in unavailable_periods.get(r.reviewer.person_id, []))
r.due = max(0, (today - r.deadline).days)
return open_review_requests
def review_requests(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_reviews:
raise Http404
assigned_review_requests = []
unassigned_review_requests = []
for r in get_open_review_requests_for_team(group):
if r.reviewer:
assigned_review_requests.append(r)
else:
unassigned_review_requests.append(r)
open_review_requests = [
("Unassigned", unassigned_review_requests),
("Assigned", assigned_review_requests),
]
closed_review_requests = ReviewRequest.objects.filter(
team=group,
).exclude(
state__in=("requested", "accepted")
).prefetch_related("reviewer__person", "type", "state", "doc", "result").order_by("-time", "-id")
since_choices = [
(None, "1 month"),
("3m", "3 months"),
("6m", "6 months"),
("1y", "1 year"),
("2y", "2 years"),
("all", "All"),
]
since = request.GET.get("since", None)
if since not in [key for key, label in since_choices]:
since = None
if since != "all":
date_limit = {
None: datetime.timedelta(days=31),
"3m": datetime.timedelta(days=31 * 3),
"6m": datetime.timedelta(days=180),
"1y": datetime.timedelta(days=365),
"2y": datetime.timedelta(days=2 * 365),
}[since]
closed_review_requests = closed_review_requests.filter(time__gte=datetime.date.today() - date_limit)
return render(request, 'group/review_requests.html',
construct_group_menu_context(request, group, "review requests", group_type, {
"open_review_requests": open_review_requests,
"closed_review_requests": closed_review_requests,
"since_choices": since_choices,
"since": since,
"can_manage_review_requests": can_manage_review_requests_for_team(request.user, group),
"can_access_stats": can_access_review_stats_for_team(request.user, group),
}))
def reviewer_overview(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_reviews:
raise Http404
can_manage = can_manage_review_requests_for_team(request.user, group)
reviewers = reviewer_rotation_list(group)
reviewer_settings = { s.person_id: s for s in ReviewerSettings.objects.filter(team=group) }
unavailable_periods = defaultdict(list)
for p in unavailable_periods_to_list().filter(team=group):
unavailable_periods[p.person_id].append(p)
reviewer_roles = { r.person_id: r for r in Role.objects.filter(group=group, name="reviewer").select_related("email") }
today = datetime.date.today()
req_data_for_reviewers = latest_review_requests_for_reviewers(group)
review_state_by_slug = { n.slug: n for n in ReviewRequestStateName.objects.all() }
days_needed = days_needed_to_fulfill_min_interval_for_reviewers(group)
for person in reviewers:
person.settings = reviewer_settings.get(person.pk) or ReviewerSettings(team=group, person=person)
person.settings_url = None
person.role = reviewer_roles.get(person.pk)
if person.role and (can_manage or user_is_person(request.user, person)):
kwargs = { "acronym": group.acronym, "reviewer_email": person.role.email.address }
if group_type:
kwargs["group_type"] = group_type
person.settings_url = urlreverse("ietf.group.views_review.change_reviewer_settings", kwargs=kwargs)
person.unavailable_periods = unavailable_periods.get(person.pk, [])
person.completely_unavailable = any(p.availability == "unavailable"
and (p.start_date is None or p.start_date <= today) and (p.end_date is None or today <= p.end_date)
for p in person.unavailable_periods)
person.busy = person.id in days_needed
MAX_CLOSED_REQS = 10
req_data = req_data_for_reviewers.get(person.pk, [])
open_reqs = sum(1 for d in req_data if d.state in ["requested", "accepted"])
latest_reqs = []
for d in req_data:
if d.state in ["requested", "accepted"] or len(latest_reqs) < MAX_CLOSED_REQS + open_reqs:
latest_reqs.append((d.req_pk, d.doc, d.reviewed_rev, d.assigned_time, d.deadline,
review_state_by_slug.get(d.state),
int(math.ceil(d.assignment_to_closure_days)) if d.assignment_to_closure_days is not None else None))
person.latest_reqs = latest_reqs
return render(request, 'group/reviewer_overview.html',
construct_group_menu_context(request, group, "reviewers", group_type, {
"reviewers": reviewers,
"can_access_stats": can_access_review_stats_for_team(request.user, group)
}))
class ManageReviewRequestForm(forms.Form):
ACTIONS = [
("assign", "Assign"),
("close", "Close"),
]
action = forms.ChoiceField(choices=ACTIONS, widget=forms.HiddenInput, required=False)
close = forms.ModelChoiceField(queryset=close_review_request_states(), required=False)
reviewer = PersonEmailChoiceField(empty_label="(None)", required=False, label_with="person")
add_skip = forms.BooleanField(required=False)
def __init__(self, review_req, *args, **kwargs):
if not "prefix" in kwargs:
if review_req.pk is None:
kwargs["prefix"] = "r{}-{}".format(review_req.type_id, review_req.doc_id)
else:
kwargs["prefix"] = "r{}".format(review_req.pk)
super(ManageReviewRequestForm, self).__init__(*args, **kwargs)
if review_req.pk is None:
self.fields["close"].queryset = self.fields["close"].queryset.filter(slug__in=["no-review-version", "no-review-document"])
close_initial = None
if review_req.pk is None:
close_initial = "no-review-version"
elif review_req.reviewer:
close_initial = "no-response"
else:
close_initial = "overtaken"
if close_initial:
self.fields["close"].initial = close_initial
self.fields["close"].widget.attrs["class"] = "form-control input-sm"
setup_reviewer_field(self.fields["reviewer"], review_req)
self.fields["reviewer"].widget.attrs["class"] = "form-control input-sm"
if self.is_bound:
if self.data.get("action") == "close":
self.fields["close"].required = True
@login_required
def manage_review_requests(request, acronym, group_type=None, assignment_status=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_reviews:
raise Http404
if not can_manage_review_requests_for_team(request.user, group):
return HttpResponseForbidden("You do not have permission to perform this action")
review_requests = get_open_review_requests_for_team(group, assignment_status=assignment_status)
document_requests = extract_revision_ordered_review_requests_for_documents_and_replaced(
ReviewRequest.objects.filter(state__in=("part-completed", "completed"), team=group).prefetch_related("result"),
set(r.doc_id for r in review_requests),
)
# we need a mutable query dict for resetting upon saving with
# conflicts
query_dict = request.POST.copy() if request.method == "POST" else None
for req in review_requests:
req.form = ManageReviewRequestForm(req, query_dict)
# add previous requests
l = []
for r in document_requests.get(req.doc_id, []):
# take all on the latest reviewed rev
if l and l[0].reviewed_rev:
if r.doc_id == l[0].doc_id and r.reviewed_rev:
if int(r.reviewed_rev) > int(l[0].reviewed_rev):
l = [r]
elif int(r.reviewed_rev) == int(l[0].reviewed_rev):
l.append(r)
else:
l = [r]
augment_review_requests_with_events(l)
req.latest_reqs = l
saving = False
newly_closed = newly_opened = newly_assigned = 0
if request.method == "POST":
form_action = request.POST.get("action", "")
saving = form_action.startswith("save")
# check for conflicts
review_requests_dict = { unicode(r.pk): r for r in review_requests }
posted_reqs = set(request.POST.getlist("reviewrequest", []))
current_reqs = set(review_requests_dict.iterkeys())
closed_reqs = posted_reqs - current_reqs
newly_closed = len(closed_reqs)
opened_reqs = current_reqs - posted_reqs
newly_opened = len(opened_reqs)
for r in opened_reqs:
review_requests_dict[r].form.add_error(None, "New request.")
for req in review_requests:
existing_reviewer = request.POST.get(req.form.prefix + "-existing_reviewer")
if existing_reviewer is None:
continue
if existing_reviewer != unicode(req.reviewer_id or ""):
msg = "Assignment was changed."
a = req.form["action"].value()
if a == "assign":
msg += " Didn't assign reviewer."
elif a == "close":
msg += " Didn't close request."
req.form.add_error(None, msg)
req.form.data[req.form.prefix + "-action"] = "" # cancel the action
newly_assigned += 1
form_results = []
for req in review_requests:
form_results.append(req.form.is_valid())
if saving and all(form_results) and not (newly_closed > 0 or newly_opened > 0 or newly_assigned > 0):
for review_req in review_requests:
action = review_req.form.cleaned_data.get("action")
if action == "assign":
assign_review_request_to_reviewer(request, review_req, review_req.form.cleaned_data["reviewer"],review_req.form.cleaned_data["add_skip"])
elif action == "close":
close_review_request(request, review_req, review_req.form.cleaned_data["close"])
kwargs = { "acronym": group.acronym }
if group_type:
kwargs["group_type"] = group_type
if form_action == "save-continue":
if assignment_status:
kwargs["assignment_status"] = assignment_status
return redirect(manage_review_requests, **kwargs)
else:
import ietf.group.views_review
return redirect(ietf.group.views_review.review_requests, **kwargs)
other_assignment_status = {
"unassigned": "assigned",
"assigned": "unassigned",
}.get(assignment_status)
return render(request, 'group/manage_review_requests.html', {
'group': group,
'review_requests': review_requests,
'newly_closed': newly_closed,
'newly_opened': newly_opened,
'newly_assigned': newly_assigned,
'saving': saving,
'assignment_status': assignment_status,
'other_assignment_status': other_assignment_status,
})
class EmailOpenAssignmentsForm(forms.Form):
frm = forms.CharField(label="From", widget=forms.EmailInput(attrs={"readonly":True}))
to = MultiEmailField()
cc = MultiEmailField(required=False)
reply_to = MultiEmailField(required=False)
subject = forms.CharField()
body = forms.CharField(widget=forms.Textarea, strip=False)
@login_required
def email_open_review_assignments(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_reviews:
raise Http404
if not can_manage_review_requests_for_team(request.user, group):
return HttpResponseForbidden("You do not have permission to perform this action")
review_requests = list(ReviewRequest.objects.filter(
team=group,
state__in=("requested", "accepted"),
).exclude(
reviewer=None,
).prefetch_related("reviewer", "type", "state", "doc").distinct().order_by("reviewer","-deadline"))
review_requests.sort(key=lambda r:r.reviewer.person.last_name()+r.reviewer.person.first_name())
for r in review_requests:
if r.doc.telechat_date():
r.section = 'For telechat %s' % r.doc.telechat_date().isoformat()
r.section_order='0'+r.section
elif r.type_id == 'early':
r.section = 'Early review requests:'
r.section_order='2'
else:
r.section = 'Last calls:'
r.section_order='1'
e = r.doc.latest_event(LastCallDocEvent, type="sent_last_call")
r.lastcall_ends = e and e.expires.date().isoformat()
r.earlier_review = ReviewRequest.objects.filter(doc=r.doc,reviewer__in=r.reviewer.person.email_set.all(),state="completed")
if r.earlier_review:
req_rev = r.requested_rev or r.doc.rev
earlier_review_rev = r.earlier_review.aggregate(Max('reviewed_rev'))['reviewed_rev__max']
if req_rev == earlier_review_rev:
r.earlier_review_mark = '**'
else:
r.earlier_review_mark = '*'
review_requests.sort(key=lambda r: r.section_order)
back_url = request.GET.get("next")
if not back_url:
kwargs = { "acronym": group.acronym }
if group_type:
kwargs["group_type"] = group_type
import ietf.group.views_review
back_url = urlreverse(ietf.group.views_review.review_requests, kwargs=kwargs)
if request.method == "POST" and request.POST.get("action") == "email":
form = EmailOpenAssignmentsForm(request.POST)
if form.is_valid():
send_mail_text(request, form.cleaned_data["to"], form.cleaned_data["frm"], form.cleaned_data["subject"], form.cleaned_data["body"],cc=form.cleaned_data["cc"],extra={"Reply-to":", ".join(form.cleaned_data["reply_to"])})
return HttpResponseRedirect(back_url)
else:
(to,cc) = gather_address_lists('review_assignments_summarized',group=group)
reply_to = Recipient.objects.get(slug='group_secretaries').gather(group=group)
frm = request.user.person.formatted_email()
templateqs = DBTemplate.objects.filter(path="/group/%s/email/open_assignments.txt" % group.acronym)
if templateqs.exists():
template = templateqs.first()
else:
template = DBTemplate.objects.get(path="/group/defaults/email/open_assignments.txt")
partial_msg = render_to_string(template.path, {
"review_requests": review_requests,
"rotation_list": reviewer_rotation_list(group)[:10],
"group" : group,
})
(msg,_,_) = parse_preformatted(partial_msg)
body = msg.get_payload()
subject = msg['Subject']
form = EmailOpenAssignmentsForm(initial={
"to": ", ".join(to),
"cc": ", ".join(cc),
"reply_to": ", ".join(reply_to),
"frm": frm,
"subject": subject,
"body": body,
})
return render(request, 'group/email_open_review_assignments.html', {
'group': group,
'review_requests': review_requests,
'form': form,
'back_url': back_url,
})
class ReviewerSettingsForm(forms.ModelForm):
class Meta:
model = ReviewerSettings
fields = ['min_interval', 'filter_re', 'skip_next', 'remind_days_before_deadline','expertise']
def __init__(self, *args, **kwargs):
exclude_fields = kwargs.pop('exclude_fields', [])
super(ReviewerSettingsForm, self).__init__(*args, **kwargs)
for field_name in exclude_fields:
self.fields.pop(field_name)
def clean_skip_next(self):
skip_next = self.cleaned_data.get('skip_next')
if skip_next < 0:
raise forms.ValidationError("Skip next must not be negative")
return skip_next
class AddUnavailablePeriodForm(forms.ModelForm):
class Meta:
model = UnavailablePeriod
fields = ['start_date', 'end_date', 'availability']
def __init__(self, *args, **kwargs):
super(AddUnavailablePeriodForm, self).__init__(*args, **kwargs)
self.fields["start_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["start_date"].label, help_text=self.fields["start_date"].help_text, required=self.fields["start_date"].required)
self.fields["end_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1" }, label=self.fields["end_date"].label, help_text=self.fields["end_date"].help_text, required=self.fields["end_date"].required)
self.fields['availability'].widget = forms.RadioSelect(choices=UnavailablePeriod.LONG_AVAILABILITY_CHOICES)
def clean(self):
start = self.cleaned_data.get("start_date")
end = self.cleaned_data.get("end_date")
if start and end and start > end:
self.add_error("start_date", "Start date must be before or equal to end date.")
return self.cleaned_data
class EndUnavailablePeriodForm(forms.Form):
def __init__(self, start_date, *args, **kwargs):
super(EndUnavailablePeriodForm, self).__init__(*args, **kwargs)
self.fields["end_date"] = DatepickerDateField(date_format="yyyy-mm-dd", picker_settings={"autoclose": "1", "start-date": start_date.isoformat() if start_date else "" })
self.start_date = start_date
def clean_end_date(self):
end = self.cleaned_data["end_date"]
if self.start_date and end < self.start_date:
raise forms.ValidationError("End date must be equal to or come after start date.")
return end
@login_required
def change_reviewer_settings(request, acronym, reviewer_email, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_reviews:
raise Http404
reviewer_role = get_object_or_404(Role, name="reviewer", group=group, email=reviewer_email)
reviewer = reviewer_role.person
if not (user_is_person(request.user, reviewer)
or can_manage_review_requests_for_team(request.user, group)):
return HttpResponseForbidden("You do not have permission to perform this action")
exclude_fields = []
if not can_manage_review_requests_for_team(request.user, group):
exclude_fields.append('skip_next')
settings = ReviewerSettings.objects.filter(person=reviewer, team=group).first()
if not settings:
settings = ReviewerSettings(person=reviewer, team=group)
settings.filter_re = get_default_filter_re(reviewer)
back_url = request.GET.get("next")
if not back_url:
import ietf.group.views_review
kwargs = { "acronym": group.acronym}
if group_type:
kwargs["group_type"] = group_type
back_url = urlreverse(ietf.group.views_review.reviewer_overview, kwargs=kwargs)
# settings
if request.method == "POST" and request.POST.get("action") == "change_settings":
prev_min_interval = settings.get_min_interval_display()
prev_skip_next = settings.skip_next
settings_form = ReviewerSettingsForm(request.POST, instance=settings, exclude_fields=exclude_fields)
if settings_form.is_valid():
settings = settings_form.save()
changes = []
if settings.get_min_interval_display() != prev_min_interval:
changes.append("Frequency changed to \"{}\" from \"{}\".".format(settings.get_min_interval_display() or "Not specified", prev_min_interval or "Not specified"))
if settings.skip_next != prev_skip_next:
changes.append("Skip next assignments changed to {} from {}.".format(settings.skip_next, prev_skip_next))
if changes:
email_reviewer_availability_change(request, group, reviewer_role, "\n\n".join(changes), request.user.person)
return HttpResponseRedirect(back_url)
else:
settings_form = ReviewerSettingsForm(instance=settings,exclude_fields=exclude_fields)
# periods
unavailable_periods = unavailable_periods_to_list().filter(person=reviewer, team=group)
if request.method == "POST" and request.POST.get("action") == "add_period":
period_form = AddUnavailablePeriodForm(request.POST)
if period_form.is_valid():
period = period_form.save(commit=False)
period.team = group
period.person = reviewer
period.save()
today = datetime.date.today()
in_the_past = period.end_date and period.end_date < today
if not in_the_past:
msg = "Unavailable for review: {} - {} ({})".format(
period.start_date.isoformat() if period.start_date else "indefinite",
period.end_date.isoformat() if period.end_date else "indefinite",
period.get_availability_display(),
)
if period.availability == "unavailable":
# the secretary might need to reassign
# assignments, so mention the current ones
review_reqs = ReviewRequest.objects.filter(state__in=["requested", "accepted"], reviewer=reviewer_role.email, team=group)
msg += "\n\n"
if review_reqs:
msg += "{} is currently assigned to review:".format(reviewer_role.person)
for r in review_reqs:
msg += "\n\n"
msg += "{} (deadline: {})".format(r.doc_id, r.deadline.isoformat())
else:
msg += "{} does not have any assignments currently.".format(reviewer_role.person)
email_reviewer_availability_change(request, group, reviewer_role, msg, request.user.person)
return HttpResponseRedirect(request.get_full_path())
else:
period_form = AddUnavailablePeriodForm()
if request.method == "POST" and request.POST.get("action") == "delete_period":
period_id = request.POST.get("period_id")
if period_id is not None:
for period in unavailable_periods:
if str(period.pk) == period_id:
period.delete()
today = datetime.date.today()
in_the_past = period.end_date and period.end_date < today
if not in_the_past:
msg = "Removed unavailable period: {} - {} ({})".format(
period.start_date.isoformat() if period.start_date else "indefinite",
period.end_date.isoformat() if period.end_date else "indefinite",
period.get_availability_display(),
)
email_reviewer_availability_change(request, group, reviewer_role, msg, request.user.person)
return HttpResponseRedirect(request.get_full_path())
for p in unavailable_periods:
if not p.end_date:
p.end_form = EndUnavailablePeriodForm(p.start_date, request.POST if request.method == "POST" and request.POST.get("action") == "end_period" else None)
if request.method == "POST" and request.POST.get("action") == "end_period":
period_id = request.POST.get("period_id")
for period in unavailable_periods:
if str(period.pk) == period_id:
if not period.end_date and period.end_form.is_valid():
period.end_date = period.end_form.cleaned_data["end_date"]
period.save()
msg = "Set end date of unavailable period: {} - {} ({})".format(
period.start_date.isoformat() if period.start_date else "indefinite",
period.end_date.isoformat() if period.end_date else "indefinite",
period.get_availability_display(),
)
email_reviewer_availability_change(request, group, reviewer_role, msg, request.user.person)
return HttpResponseRedirect(request.get_full_path())
return render(request, 'group/change_reviewer_settings.html', {
'group': group,
'reviewer_email': reviewer_email,
'back_url': back_url,
'settings_form': settings_form,
'period_form': period_form,
'unavailable_periods': unavailable_periods,
})
class ReviewSecretarySettingsForm(forms.ModelForm):
class Meta:
model = ReviewSecretarySettings
fields = ['remind_days_before_deadline']
@login_required
def change_review_secretary_settings(request, acronym, group_type=None):
group = get_group_or_404(acronym, group_type)
if not group.features.has_reviews:
raise Http404
if not Role.objects.filter(name="secr", group=group, person__user=request.user).exists():
raise Http404
person = request.user.person
settings = (ReviewSecretarySettings.objects.filter(person=person, team=group).first()
or ReviewSecretarySettings(person=person, team=group))
import ietf.group.views_review
back_url = urlreverse(ietf.group.views_review.review_requests, kwargs={ "acronym": acronym, "group_type": group.type_id })
# settings
if request.method == "POST":
settings_form = ReviewSecretarySettingsForm(request.POST, instance=settings)
if settings_form.is_valid():
settings_form.save()
return HttpResponseRedirect(back_url)
else:
settings_form = ReviewSecretarySettingsForm(instance=settings)
return render(request, 'group/change_review_secretary_settings.html', {
'group': group,
'back_url': back_url,
'settings_form': settings_form,
})

View file

@ -1,78 +0,0 @@
# Copyright The IETF Trust 2008, All Rights Reserved
from django.shortcuts import render, get_object_or_404, redirect
from django.http import Http404, HttpResponseForbidden
from django import forms
from ietf.doc.models import Document
from ietf.doc.utils_search import prepare_document_table
from ietf.group.models import Group, GroupEvent, Role
from ietf.group.utils import save_group_in_history
from ietf.ietfauth.utils import has_role
from ietf.name.models import StreamName
from ietf.person.fields import SearchableEmailsField
from ietf.person.models import Email
import debug # pyflakes:ignore
def streams(request):
streams = [ s.slug for s in StreamName.objects.all().exclude(slug__in=['ietf', 'legacy']) ]
streams = Group.objects.filter(acronym__in=streams)
return render(request, 'group/index.html', {'streams':streams})
def stream_documents(request, acronym):
streams = [ s.slug for s in StreamName.objects.all().exclude(slug__in=['ietf', 'legacy']) ]
if not acronym in streams:
raise Http404("No such stream: %s" % acronym)
group = get_object_or_404(Group, acronym=acronym)
editable = has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")
stream = StreamName.objects.get(slug=acronym)
qs = Document.objects.filter(states__type="draft", states__slug__in=["active", "rfc"], stream=acronym)
docs, meta = prepare_document_table(request, qs)
return render(request, 'group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable } )
class StreamEditForm(forms.Form):
delegates = SearchableEmailsField(required=False, only_users=True)
def stream_edit(request, acronym):
group = get_object_or_404(Group, acronym=acronym)
if not (has_role(request.user, "Secretariat") or group.has_role(request.user, "chair")):
return HttpResponseForbidden("You don't have permission to access this page.")
chairs = Email.objects.filter(role__group=group, role__name="chair").select_related("person")
if request.method == 'POST':
form = StreamEditForm(request.POST)
if form.is_valid():
save_group_in_history(group)
# update roles
attr, slug, title = ('delegates', 'delegate', "Delegates")
new = form.cleaned_data[attr]
old = Email.objects.filter(role__group=group, role__name=slug).select_related("person")
if set(new) != set(old):
desc = "%s changed to <b>%s</b> from %s" % (
title, ", ".join(x.get_name() for x in new), ", ".join(x.get_name() for x in old))
GroupEvent.objects.create(group=group, by=request.user.person, type="info_changed", desc=desc)
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)
return redirect("ietf.group.views_stream.streams")
else:
form = StreamEditForm(initial=dict(delegates=Email.objects.filter(role__group=group, role__name="delegate")))
return render(request, 'group/stream_edit.html',
{
'group': group,
'chairs': chairs,
'form': form,
},
)

View file

@ -182,7 +182,7 @@ class ApiTests(TestCase):
make_meeting_test_data()
group = Group.objects.get(acronym="mars")
url = urlreverse("ietf.group.views_ajax.group_json", kwargs=dict(acronym=group.acronym))
url = urlreverse("ietf.group.views.group_json", kwargs=dict(acronym=group.acronym))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
info = json.loads(r.content)

View file

@ -407,7 +407,7 @@ def email_reviewer_availability_change(request, team, reviewer_role, msg, by):
subject = "Reviewer availability of {} changed in {}".format(reviewer_role.person, team.acronym)
url = urlreverse("ietf.group.views_review.reviewer_overview", kwargs={ "group_type": team.type_id, "acronym": team.acronym })
url = urlreverse("ietf.group.views.reviewer_overview", kwargs={ "group_type": team.type_id, "acronym": team.acronym })
url = request.build_absolute_uri(url)
send_mail(request, to, None, subject, "review/reviewer_availability_changed.txt", {
"reviewer_overview_url": url,
@ -910,8 +910,8 @@ def email_secretary_reminder(review_request, secretary_role):
subject = "Reminder: deadline for review of {} in {} is {}".format(review_request.doc_id, team.acronym, review_request.deadline.isoformat())
import ietf.group.views_review
settings_url = urlreverse(ietf.group.views_review.change_review_secretary_settings, kwargs={ "acronym": team.acronym, "group_type": team.type_id })
import ietf.group.views
settings_url = urlreverse(ietf.group.views.change_review_secretary_settings, kwargs={ "acronym": team.acronym, "group_type": team.type_id })
import ietf.doc.views_review
request_url = urlreverse(ietf.doc.views_review.review_request, kwargs={ "name": review_request.doc_id, "request_id": review_request.pk })

View file

@ -43,7 +43,7 @@
<link rel="apple-touch-icon" href="{% static 'ietf/images/apple-touch-icon.png' %}">
</head>
<body {% block bodyAttrs %}{%endblock%} data-group-menu-data-url="{% url 'ietf.group.views_ajax.group_menu_data' %}">
<body {% block bodyAttrs %}{%endblock%} data-group-menu-data-url="{% url 'ietf.group.views.group_menu_data' %}">
<nav class="navbar {% if server_mode and server_mode != "production" %}navbar-default{% else %}navbar-inverse{% endif %} {% if navbar_mode %}{{ navbar_mode }}{% else %}navbar-fixed-top{% endif %}">
<div class="container-fluid">
<div class="navbar-header">

View file

@ -69,7 +69,7 @@
{% if flavor == "top" %}<li class="divider hidden-xs"></li>{% endif %}
<li {%if flavor == "top" %}class="dropdown-header hidden-xs"{% else %}class="nav-header"{% endif %}>Review Teams</li>
{% for g in user|managed_review_groups %}
<li><a href="{% url "ietf.group.views_review.review_requests" g.acronym %}">{{ g.acronym }} reviews</a></li>
<li><a href="{% url "ietf.group.views.review_requests" g.acronym %}">{{ g.acronym }} reviews</a></li>
{% endfor %}
{% endif %}
@ -79,9 +79,9 @@
{% endif %}
{% if flavor == "top" %}<li class="divider hidden-xs"></li>{% endif %}
<li {%if flavor == "top" %}class="dropdown-header hidden-xs"{% else %}class="nav-header"{% endif %}>RFC streams</li>
<li><a href="{% url "ietf.group.views_stream.stream_documents" acronym="iab" %}">IAB</a></li>
<li><a href="{% url "ietf.group.views_stream.stream_documents" acronym="irtf" %}">IRTF</a></li>
<li><a href="{% url "ietf.group.views_stream.stream_documents" acronym="ise" %}">ISE</a></li>
<li><a href="{% url "ietf.group.views.stream_documents" acronym="iab" %}">IAB</a></li>
<li><a href="{% url "ietf.group.views.stream_documents" acronym="irtf" %}">IRTF</a></li>
<li><a href="{% url "ietf.group.views.stream_documents" acronym="ise" %}">ISE</a></li>
{% if flavor == "top" %}</ul>{% endif %}

View file

@ -3,6 +3,6 @@
{% if editable_streams %}
<li class="sect">Streams</li>
{% for stream in editable_streams %}
<li{% if forloop.last %} style="margin-bottom: 2px;"{% endif %}><a href="{% url "ietf.group.views_stream.stream_edit" stream.slug %}">{{ stream.name }} stream</a></li>
<li{% if forloop.last %} style="margin-bottom: 2px;"{% endif %}><a href="{% url "ietf.group.views.stream_edit" stream.slug %}">{{ stream.name }} stream</a></li>
{% endfor %}
{% endif %}

View file

@ -44,7 +44,7 @@
<tr>
<th></th>
<th>Team</th>
<td><a href="{% url "ietf.group.views_review.review_requests" group_type=review_req.team.type_id acronym=review_req.team.acronym %}">{{ review_req.team.acronym|upper }}</a></td>
<td><a href="{% url "ietf.group.views.review_requests" group_type=review_req.team.type_id acronym=review_req.team.acronym %}">{{ review_req.team.acronym|upper }}</a></td>
</tr>
<tr>

View file

@ -17,7 +17,7 @@
<p>Groups in the <i>BOF</i> state.</p>
{% if user|has_role:"Area Director,Secretariat" %}
<p><a class="btn btn-default" role="button" href="{% url 'ietf.group.views_edit.edit' group_type='wg' action='create' %}">Create new BOF</a></p>
<p><a class="btn btn-default" role="button" href="{% url 'ietf.group.views.edit' group_type='wg' action='create' %}">Create new BOF</a></p>
{% endif %}
{% if not groups %}
@ -47,4 +47,4 @@
{% block js %}
<script src="{% static "jquery.tablesorter/js/jquery.tablesorter.combined.min.js" %}"></script>
{% endblock %}
{% endblock %}

View file

@ -28,7 +28,7 @@
<h2>{{ t.name }}s</h2>
{% if t.can_manage %}
<p><a class="btn btn-default" role="button" href="{% url 'ietf.group.views_edit.edit' group_type=t.pk action='charter' %}">Charter new {{ t.name }}</a></p>
<p><a class="btn btn-default" role="button" href="{% url 'ietf.group.views.edit' group_type=t.pk action='charter' %}">Charter new {{ t.name }}</a></p>
{% endif %}
{% if not t.chartering_groups %}

View file

@ -18,7 +18,7 @@
<th>Name</th>
<td class="edit">
{% if can_edit_group %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views_edit.edit' acronym=group.acronym field='name' %}">Edit</a>
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='name' %}">Edit</a>
{% endif %}
</td>
<td>{{ group.name }}</td>
@ -49,7 +49,7 @@
<th>State</th>
<td class="edit">
{% if can_edit_group %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views_edit.edit' acronym=group.acronym field='state' %}">Edit</a>
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='state' %}">Edit</a>
{% endif %}
</td>
<td>
@ -120,7 +120,7 @@
<th>Additional URLs</th>
<td class="edit">
{% if can_edit_group %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views_edit.edit' acronym=group.acronym field='urls' %}">Edit</a>
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='urls' %}">Edit</a>
{% endif %}
</td>
<td>
@ -144,7 +144,7 @@
<th>{{ label }}</th>
<td class="edit">
{% if can_edit_group and slug in editable_roles %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views_edit.edit' acronym=group.acronym field=slug %}">Edit</a>
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field=slug %}">Edit</a>
{% endif %}
</td>
<td>
@ -167,7 +167,7 @@
<th>Address</th>
<td class="edit">
{% if can_edit_group %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views_edit.edit' acronym=group.acronym field='list_email' %}">Edit</a>
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='list_email' %}">Edit</a>
{% endif %}
</td>
<td>{{ group.list_email|urlize }}</td>
@ -177,7 +177,7 @@
<th>To subscribe</th>
<td class="edit">
{% if can_edit_group %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views_edit.edit' acronym=group.acronym field='list_subscribe' %}">Edit</a>
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='list_subscribe' %}">Edit</a>
{% endif %}
</td>
<td>{{ group.list_subscribe|urlize }}</td>
@ -187,7 +187,7 @@
<th>Archive</th>
<td class="edit">
{% if can_edit_group %}
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views_edit.edit' acronym=group.acronym field='list_archive' %}">Edit</a>
<a class="btn btn-default btn-xs" href="{% url 'ietf.group.views.edit' acronym=group.acronym field='list_archive' %}">Edit</a>
{% endif %}
</td>
<td>{{ group.list_archive|urlize }}</td>

View file

@ -16,11 +16,11 @@
<h1>Manage {{ assignment_status }} open review requests for {{ group.acronym }}</h1>
<p>Other options:
<a href="{% url "ietf.group.views_review.review_requests" group_type=group.type_id acronym=group.acronym %}">All review requests</a>
- <a href="{% url "ietf.group.views_review.reviewer_overview" group_type=group.type_id acronym=group.acronym %}">Reviewers</a>
- <a href="{% url "ietf.group.views_review.email_open_review_assignments" group_type=group.type_id acronym=group.acronym %}?next={{ request.get_full_path|urlencode }}">Email open assignments summary</a>
<a href="{% url "ietf.group.views.review_requests" group_type=group.type_id acronym=group.acronym %}">All review requests</a>
- <a href="{% url "ietf.group.views.reviewer_overview" group_type=group.type_id acronym=group.acronym %}">Reviewers</a>
- <a href="{% url "ietf.group.views.email_open_review_assignments" group_type=group.type_id acronym=group.acronym %}?next={{ request.get_full_path|urlencode }}">Email open assignments summary</a>
{% if other_assignment_status %}
- <a href="{% url "ietf.group.views_review.manage_review_requests" group_type=group.type_id acronym=group.acronym assignment_status=other_assignment_status %}">Manage {{ other_assignment_status }} reviews</a>
- <a href="{% url "ietf.group.views.manage_review_requests" group_type=group.type_id acronym=group.acronym assignment_status=other_assignment_status %}">Manage {{ other_assignment_status }} reviews</a>
{% endif %}
</p>
@ -169,7 +169,7 @@
{% endfor %}
{% buttons %}
<a href="{% url "ietf.group.views_review.review_requests" group_type=group.type_id acronym=group.acronym %}" class="btn btn-default pull-right">Cancel</a>
<a href="{% url "ietf.group.views.review_requests" group_type=group.type_id acronym=group.acronym %}" class="btn btn-default pull-right">Cancel</a>
<button class="btn btn-primary" type="submit" name="action" value="save">Save changes</button>
<button class="btn btn-primary" type="submit" name="action" value="save-continue">Save and continue editing</button>
<button class="btn btn-default" type="submit" name="action" value="refresh">Refresh (keeping changes)</button>

View file

@ -16,7 +16,7 @@
{% if editable %}
<p>
<a class="btn btn-default" href="{% url "ietf.group.views_stream.stream_edit" stream.slug %}">Assign delegates</a>
<a class="btn btn-default" href="{% url "ietf.group.views.stream_edit" stream.slug %}">Assign delegates</a>
</p>
{% endif %}

View file

@ -36,7 +36,7 @@
{% buttons %}
<button type="submit" class="btn btn-primary">Save</button>
<a class="btn btn-default pull-right" href="{% url "ietf.group.views_stream.streams" %}{{ group.acronym }}">Back</a>
<a class="btn btn-default pull-right" href="{% url "ietf.group.views.streams" %}{{ group.acronym }}">Back</a>
{% endbuttons %}
</form>

View file

@ -35,7 +35,7 @@
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{{ r.doc.name }}</a></td>
<td>{% if r.requested_rev %}{{ r.requested_rev }}{% else %}Current{% endif %}</td>
<td>{{r.doc.rev}}</td>
<td><a href="{% url 'ietf.group.views_review.review_requests' acronym=r.team.acronym %}">{{ r.team.acronym }}</a></td>
<td><a href="{% url 'ietf.group.views.review_requests' acronym=r.team.acronym %}">{{ r.team.acronym }}</a></td>
<td>{{ r.type.name }}</td>
<td>
{{ r.deadline|date:"Y-m-d" }}
@ -70,7 +70,7 @@
<tr>
<td><a {% if r.pk != None %}href="{% url "ietf.doc.views_review.review_request" name=r.doc.name request_id=r.pk %}"{% endif %}>{{ r.doc.name }}</a></td>
<td>{{r.reviewed_rev|default:"See review"}}{% if r.requested_rev %}{% if r.requested_rev != r.reviewed_rev %}({{ r.requested_rev }} requested){% endif %}{% endif %}</td>
<td><a href="{% url 'ietf.group.views_review.review_requests' acronym=r.team.acronym %}">{{ r.team.acronym }}</a></td>
<td><a href="{% url 'ietf.group.views.review_requests' acronym=r.team.acronym %}">{{ r.team.acronym }}</a></td>
<td>{{ r.type.name }}</td>
<td>
{{ r.deadline|date:"Y-m-d" }}
@ -158,7 +158,7 @@
</table>
<div>
<a class="btn btn-default" href="{% url "ietf.group.views_review.change_reviewer_settings" group_type=t.type_id acronym=t.acronym reviewer_email=t.role.email.address %}?next={{ request.get_full_path|urlencode }}">Change settings</a>
<a class="btn btn-default" href="{% url "ietf.group.views.change_reviewer_settings" group_type=t.type_id acronym=t.acronym reviewer_email=t.role.email.address %}?next={{ request.get_full_path|urlencode }}">Change settings</a>
</div>

View file

@ -13,6 +13,7 @@ import debug # pyflakes:ignore
from ietf import api
from ietf.doc import views_search
from ietf.group.urls import group_urls, grouptype_urls, stream_urls
from ietf.help import views as help_views
from ietf.ipr.sitemaps import IPRMap
from ietf.liaisons.sitemaps import LiaisonMap
@ -46,7 +47,7 @@ urlpatterns = [
url(r'^drafts/', include('ietf.doc.redirect_drafts_urls')),
url(r'^mailtrigger/',include('ietf.mailtrigger.urls')),
url(r'^feed/', include('ietf.feed_urls')),
url(r'^group/', include('ietf.group.urls')),
url(r'^group/', include(group_urls)),
url(r'^help/', include('ietf.help.urls')),
url(r'^idtracker/', include('ietf.doc.redirect_idtracker_urls')),
url(r'^iesg/', include('ietf.iesg.urls')),
@ -61,11 +62,11 @@ urlpatterns = [
url(r'^sitemap-(?P<section>.+).xml$', sitemap_views.sitemap, {'sitemaps': sitemaps}),
url(r'^sitemap.xml$', sitemap_views.index, { 'sitemaps': sitemaps}),
url(r'^stats/', include('ietf.stats.urls')),
url(r'^stream/', include('ietf.group.urls_stream')),
url(r'^stream/', include(stream_urls)),
url(r'^submit/', include('ietf.submit.urls')),
url(r'^sync/', include('ietf.sync.urls')),
url(r'^templates/', include('ietf.dbtemplate.urls')),
url(r'^(?P<group_type>(wg|rg|ag|team|dir|area|program))/', include('ietf.group.urls_info')),
url(r'^(?P<group_type>(wg|rg|ag|team|dir|area|program))/', include(grouptype_urls)),
# Redirects
url(r'^(?P<path>public)/', include('ietf.redirects.urls')),