# -*- coding: utf-8 -*- # Copyright The IETF Trust 2009-2020, All Rights Reserved # # Portion Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies). # All rights reserved. Contact: Pasi Eronen # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following # disclaimer in the documentation and/or other materials provided # with the distribution. # # * Neither the name of the Nokia Corporation and/or its # subsidiary(-ies) nor the names of its contributors may be used # to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import copy import datetime import itertools import io import json from traceback import print_list import markdown import math import os import re from tempfile import mkstemp from collections import OrderedDict, defaultdict from simple_history.utils import update_change_reason from django import forms from django.conf import settings from django.contrib.auth.decorators import login_required from django.db.models import Q, Count from django.http import HttpResponse, HttpResponseRedirect, Http404, JsonResponse from django.shortcuts import render, redirect, get_object_or_404 from django.template.loader import render_to_string from django.urls import reverse as urlreverse from django.utils.html import escape from django.views.decorators.cache import cache_page, cache_control import debug # pyflakes:ignore from ietf.community.models import CommunityList, EmailSubscription from ietf.community.utils import docs_tracked_by_community_list from ietf.doc.models import DocTagName, State, DocAlias, RelatedDocument, Document, Person from ietf.doc.templatetags.ietf_filters import clean_whitespace from ietf.doc.utils import get_chartering_type, get_tags_for_stream_id from ietf.doc.utils_charter import charter_name_for_group, replace_charter_of_replaced_group from ietf.doc.utils_search import prepare_document_table # from ietf.group.dot import make_dot from ietf.group.forms import (GroupForm, StatusUpdateForm, ConcludeGroupForm, StreamEditForm, ManageReviewRequestForm, EmailOpenAssignmentsForm, ReviewerSettingsForm, AddUnavailablePeriodForm, EndUnavailablePeriodForm, ReviewSecretarySettingsForm, ) from ietf.group.mails import email_admin_re_charter, email_personnel_change, email_comment from ietf.group.models import ( Group, Role, GroupEvent, GroupStateTransitions, ChangeStateGroupEvent, GroupFeatures ) from ietf.group.utils import (get_charter_text, can_manage_all_groups_of_type, milestone_reviewer_for_group_type, can_provide_status_update, can_manage_materials, construct_group_menu_context, get_group_materials, save_group_in_history, can_manage_group, get_group_or_404, setup_default_community_list_for_group, ) # from ietf.ietfauth.utils import has_role, is_authorized_in_group from ietf.mailtrigger.utils import gather_relevant_expansions from ietf.meeting.helpers import get_meeting from ietf.meeting.utils import group_sessions, add_event_info_to_session_qs from ietf.name.models import GroupTypeName, StreamName from ietf.person.models import Email, Person from ietf.review.models import (ReviewRequest, ReviewAssignment, ReviewerSettings, ReviewSecretarySettings, UnavailablePeriod ) from ietf.review.policies import get_reviewer_queue_policy from ietf.review.utils import (can_manage_review_requests_for_team, can_access_review_stats_for_team, extract_revision_ordered_review_requests_for_documents_and_replaced, assign_review_request_to_reviewer, close_review_request, suggested_review_requests_for_team, unavailable_periods_to_list, current_unavailable_periods_for_reviewers, email_reviewer_availability_change, latest_review_assignments_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.name.models import ReviewAssignmentStateName from ietf.utils.mail import send_mail_text, parse_preformatted 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 from ietf.settings import MAILING_LIST_INFO_URL from ietf.utils.pipe import pipe from ietf.utils.response import permission_denied from ietf.utils.text import strip_suffix # --- Helpers ---------------------------------------------------------- def roles(group, role_name): return Role.objects.filter(group=group, name=role_name).select_related("email", "person") def fill_in_charter_info(group, include_drafts=False): group.areadirector = getattr(group.ad_role(),'email',None) personnel = {} for r in Role.objects.filter(group=group).order_by('person__name').select_related("email", "person", "name"): if r.name_id not in personnel: personnel[r.name_id] = [] personnel[r.name_id].append(r) if group.parent and group.parent.type_id == "area" and group.ad_role() and "ad" not in personnel: ad_roles = list(Role.objects.filter(group=group.parent, name="ad", person=group.ad_role().person)) if ad_roles: personnel["ad"] = ad_roles group.personnel = [] for role_name_slug, roles in personnel.items(): label = roles[0].name.name if len(roles) > 1: if label.endswith("y"): label = label[:-1] + "ies" else: label += "s" group.personnel.append((role_name_slug, label, roles)) group.personnel.sort(key=lambda t: t[2][0].name.order) milestone_state = "charter" if group.state_id == "proposed" else "active" group.milestones = group.groupmilestone_set.filter(state=milestone_state) if group.uses_milestone_dates: group.milestones = group.milestones.order_by('resolved', 'due') else: group.milestones = group.milestones.order_by('resolved', 'order') if group.charter: group.charter_text = get_charter_text(group) else: group.charter_text = "Not chartered yet." def extract_last_name(role): return role.person.name_parts()[3] def fill_in_wg_roles(group): def get_roles(slug, default): for role_slug, label, roles in group.personnel: if slug == role_slug: return roles return default group.chairs = get_roles("chair", []) ads = get_roles("ad", []) group.areadirector = ads[0] if ads else None group.techadvisors = get_roles("techadv", []) group.editors = get_roles("editor", []) group.secretaries = get_roles("secr", []) def fill_in_wg_drafts(group): aliases = DocAlias.objects.filter(docs__type="draft", docs__group=group).prefetch_related('docs').order_by("name") group.drafts = [] group.rfcs = [] for a in aliases: if a.name.startswith("draft"): group.drafts.append(a) else: group.rfcs.append(a) a.remote_field = RelatedDocument.objects.filter(source=a.document,relationship_id__in=['obs','updates']).distinct() a.invrel = RelatedDocument.objects.filter(target=a,relationship_id__in=['obs','updates']).distinct() def check_group_email_aliases(): pattern = re.compile(r'expand-(.*?)(-\w+)@.*? +(.*)$') tot_count = 0 good_count = 0 with io.open(settings.GROUP_VIRTUAL_PATH,"r") as virtual_file: for line in virtual_file.readlines(): m = pattern.match(line) tot_count += 1 if m: good_count += 1 if good_count > 50 and tot_count < 3*good_count: return True return False # --- View functions --------------------------------------------------- def wg_summary_area(request, group_type): if group_type != "wg": raise Http404 areas = Group.objects.filter(type="area", state="active").order_by("name") for area in areas: area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym") for group in area.groups: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) areas = [a for a in areas if a.groups] return render(request, 'group/1wg-summary.txt', { 'areas': areas }, content_type='text/plain; charset=UTF-8') def wg_summary_acronym(request, group_type): if group_type != "wg": raise Http404 areas = Group.objects.filter(type="area", state="active").order_by("name") groups = Group.objects.filter(type="wg", state="active").order_by("acronym").select_related("parent") for group in groups: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) return render(request, 'group/1wg-summary-by-acronym.txt', { 'areas': areas, 'groups': groups }, content_type='text/plain; charset=UTF-8') @cache_page ( 60 * 60, cache="slowpages" ) def wg_charters(request, group_type): if group_type != "wg": raise Http404 areas = Group.objects.filter(type="area", state="active").order_by("name") for area in areas: area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("name") for group in area.groups: fill_in_charter_info(group) fill_in_wg_roles(group) fill_in_wg_drafts(group) return render(request, 'group/1wg-charters.txt', { 'areas': areas }, content_type='text/plain; charset=UTF-8') @cache_page ( 60 * 60, cache="slowpages" ) def wg_charters_by_acronym(request, group_type): if group_type != "wg": raise Http404 groups = Group.objects.filter(type="wg", state="active").exclude(parent=None).order_by("acronym") for group in groups: fill_in_charter_info(group) fill_in_wg_roles(group) fill_in_wg_drafts(group) return render(request, 'group/1wg-charters-by-acronym.txt', { 'groups': groups }, content_type='text/plain; charset=UTF-8') def active_groups(request, group_type=None): if not group_type: return active_group_types(request) elif group_type == "wg": return active_wgs(request) elif group_type == "rg": return active_rgs(request) elif group_type == "ag": return active_ags(request) elif group_type == "rag": return active_rags(request) elif group_type == "area": return active_areas(request) elif group_type == "team": return active_teams(request) elif group_type == "dir": return active_dirs(request) elif group_type == "review": return active_review_dirs(request) elif group_type in ("program", "iabasg"): return active_iab(request) elif group_type == "adm": return active_adm(request) else: raise Http404 def active_group_types(request): grouptypes = GroupTypeName.objects.filter(slug__in=['wg','rg','ag','rag','team','dir','review','area','program','iabasg','adm']).filter(group__state='active').annotate(group_count=Count('group')) return render(request, 'group/active_groups.html', {'grouptypes':grouptypes}) def active_dirs(request): dirs = Group.objects.filter(type__in=['dir', 'review'], state="active").order_by("name") for group in dirs: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) group.secretaries = sorted(roles(group, "secr"), key=extract_last_name) return render(request, 'group/active_dirs.html', {'dirs' : dirs }) def active_review_dirs(request): dirs = Group.objects.filter(type="review", state="active").order_by("name") for group in dirs: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) group.secretaries = sorted(roles(group, "secr"), key=extract_last_name) return render(request, 'group/active_review_dirs.html', {'dirs' : dirs }) def active_teams(request): teams = Group.objects.filter(type="team", state="active").order_by("name") for group in teams: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) return render(request, 'group/active_teams.html', {'teams' : teams }) def active_iab(request): iabgroups = Group.objects.filter(type__in=("program","iabasg"), state="active").order_by("-type_id","name") for group in iabgroups: group.leads = sorted(roles(group, "lead"), key=extract_last_name) return render(request, 'group/active_iabgroups.html', {'iabgroups' : iabgroups }) def active_adm(request): adm = Group.objects.filter(type="adm", state="active").order_by("parent","name") return render(request, 'group/active_adm.html', {'adm' : adm }) def active_areas(request): areas = Group.objects.filter(type="area", state="active").order_by("name") return render(request, 'group/active_areas.html', {'areas': areas }) def active_wgs(request): areas = Group.objects.filter(type="area", state="active").order_by("name") for area in areas: # dig out information for template area.ads_and_pre_ads = ( list(area.ads) + list(sorted(roles(area, "pre-ad"), key=extract_last_name)) ) area.groups = Group.objects.filter(parent=area, type="wg", state="active").order_by("acronym") area.urls = area.groupextresource_set.all().order_by("name") for group in area.groups: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) group.ad_out_of_area = group.ad_role() and group.ad_role().person not in [role.person for role in area.ads_and_pre_ads] # get the url for mailing list subscription if group.list_subscribe.startswith('http'): group.list_subscribe_url = group.list_subscribe elif group.list_email.endswith('@ietf.org'): group.list_subscribe_url = MAILING_LIST_INFO_URL % {'list_addr':group.list_email.split('@')[0]} else: group.list_subscribe_url = "mailto:"+group.list_subscribe return render(request, 'group/active_wgs.html', { 'areas':areas }) def active_rgs(request): irtf = Group.objects.get(acronym="irtf") irtf.chair = roles(irtf, "chair").first() groups = Group.objects.filter(type="rg", state="active").order_by("acronym") for group in groups: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) return render(request, 'group/active_rgs.html', { 'irtf': irtf, 'groups': groups }) def active_ags(request): groups = Group.objects.filter(type="ag", state="active").order_by("acronym") for group in groups: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) return render(request, 'group/active_ags.html', { 'groups': groups }) def active_rags(request): groups = Group.objects.filter(type="rag", state="active").order_by("acronym") for group in groups: group.chairs = sorted(roles(group, "chair"), key=extract_last_name) return render(request, 'group/active_rags.html', { 'groups': groups }) def bofs(request, group_type): groups = Group.objects.filter(type=group_type, state="bof") return render(request, 'group/bofs.html',dict(groups=groups)) def chartering_groups(request): charter_states = State.objects.filter(used=True, type="charter").exclude(slug__in=("approved", "notrev")) group_type_slugs = [ f.type.slug for f in GroupFeatures.objects.filter(has_chartering_process=True) ] group_types = GroupTypeName.objects.filter(slug__in=group_type_slugs) for t in group_types: t.chartering_groups = Group.objects.filter(type=t, charter__states__in=charter_states,state_id__in=('active','bof','proposed','dormant')).select_related("state", "charter").order_by("acronym") t.can_manage = can_manage_all_groups_of_type(request.user, t.slug) for g in t.chartering_groups: g.chartering_type = get_chartering_type(g.charter) g.charter.ballot = g.charter.active_ballot() return render(request, 'group/chartering_groups.html', dict(charter_states=charter_states, group_types=group_types)) def concluded_groups(request): sections = OrderedDict() sections['WGs'] = Group.objects.filter(type='wg', state="conclude").select_related("state", "charter").order_by("parent__name","acronym") sections['RGs'] = Group.objects.filter(type='rg', state="conclude").select_related("state", "charter").order_by("parent__name","acronym") sections['BOFs'] = Group.objects.filter(type='wg', state="bof-conc").select_related("state", "charter").order_by("parent__name","acronym") sections['AGs'] = Group.objects.filter(type='ag', state="conclude").select_related("state", "charter").order_by("parent__name","acronym") sections['RAGs'] = Group.objects.filter(type='rag', state="conclude").select_related("state", "charter").order_by("parent__name","acronym") sections['Directorates'] = Group.objects.filter(type='dir', state="conclude").select_related("state", "charter").order_by("parent__name","acronym") sections['Review teams'] = Group.objects.filter(type='review', state="conclude").select_related("state", "charter").order_by("parent__name","acronym") sections['Teams'] = Group.objects.filter(type='team', state="conclude").select_related("state", "charter").order_by("parent__name","acronym") sections['Programs'] = Group.objects.filter(type='program', state="conclude").select_related("state", "charter").order_by("parent__name","acronym") for name, groups in sections.items(): # add start/conclusion date d = dict((g.pk, g) for g in groups) for g in groups: g.start_date = g.conclude_date = None for e in ChangeStateGroupEvent.objects.filter(group__in=groups, state="active").order_by("-time"): d[e.group_id].start_date = e.time for e in ChangeStateGroupEvent.objects.filter(group__in=groups, state="conclude").order_by("time"): d[e.group_id].conclude_date = e.time return render(request, 'group/concluded_groups.html', dict(sections=sections)) def prepare_group_documents(request, group, clist): found_docs, meta = prepare_document_table(request, docs_tracked_by_community_list(clist), request.GET, max_results=500) docs = [] docs_related = [] # split results for d in found_docs: # non-WG drafts and call for WG adoption are considered related if (d.group != group or (d.stream_id and d.get_state_slug("draft-stream-%s" % d.stream_id) in ("c-adopt", "wg-cand"))): d.search_heading = "Related Internet-Draft" docs_related.append(d) else: docs.append(d) meta_related = meta.copy() return docs, meta, docs_related, meta_related def group_home(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) kwargs = dict(acronym=group.acronym) if group_type: kwargs["group_type"] = group_type return HttpResponseRedirect(urlreverse(group.features.default_tab, kwargs=kwargs)) def group_documents(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) if not group.features.has_documents: raise Http404 if not group.communitylist_set.exists(): setup_default_community_list_for_group(group) clist = group.communitylist_set.first() docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist) subscribed = request.user.is_authenticated and EmailSubscription.objects.filter(community_list=clist, email__person__user=request.user) context = construct_group_menu_context(request, group, "documents", group_type, { 'docs': docs, 'meta': meta, 'docs_related': docs_related, 'meta_related': meta_related, 'subscribed': subscribed, 'clist': clist, }) return render(request, 'group/group_documents.html', context) def group_documents_txt(request, acronym, group_type=None): """Return tabulator-separated rows with documents for group.""" group = get_group_or_404(acronym, group_type) if not group.features.has_documents: raise Http404 clist = get_object_or_404(CommunityList, group=group) docs, meta, docs_related, meta_related = prepare_group_documents(request, group, clist) for d in docs: d.prefix = d.get_state().name for d in docs_related: d.prefix = "Related %s" % d.get_state().name rows = [] for d in itertools.chain(docs, docs_related): rfc_number = d.rfc_number() if rfc_number != None: name = rfc_number else: name = "%s-%s" % (d.name, d.rev) rows.append("\t".join((d.prefix, name, clean_whitespace(d.title)))) return HttpResponse("\n".join(rows), content_type='text/plain; charset=UTF-8') def group_about(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) fill_in_charter_info(group) e = group.latest_event(type__in=("changed_state", "requested_close",)) requested_close = group.state_id != "conclude" and e and e.type == "requested_close" e = None if group.state_id == "conclude": e = group.latest_event(type='closing_note') can_manage = can_manage_all_groups_of_type(request.user, group.type_id) charter_submit_url = "" if group.features.has_chartering_process: charter_submit_url = urlreverse('ietf.doc.views_charter.submit', kwargs={ "name": charter_name_for_group(group) }) can_provide_update = can_provide_status_update(request.user, group) status_update = group.latest_event(type="status_update") return render(request, 'group/group_about.html', construct_group_menu_context(request, group, "about", group_type, { "milestones_in_review": group.groupmilestone_set.filter(state="review"), "milestone_reviewer": milestone_reviewer_for_group_type(group_type), "requested_close": requested_close, "can_manage": can_manage, "can_provide_status_update": can_provide_update, "status_update": status_update, "charter_submit_url": charter_submit_url, "editable_roles": group.used_roles or group.features.default_used_roles, "closing_note": e, })) def all_status(request): wgs = Group.objects.filter(type='wg',state__in=['active','bof']) rgs = Group.objects.filter(type='rg',state__in=['active','proposed']) wg_reports = [] for wg in wgs: e = wg.latest_event(type='status_update') if e: wg_reports.append(e) wg_reports.sort(key=lambda x: (x.group.parent.acronym,datetime.datetime.now()-x.time)) rg_reports = [] for rg in rgs: e = rg.latest_event(type='status_update') if e: rg_reports.append(e) return render(request, 'group/all_status.html', { 'wg_reports': wg_reports, 'rg_reports': rg_reports, } ) def group_about_rendertest(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) charter = None if group.charter: charter = get_charter_text(group) try: rendered = markdown.markdown(charter, extensions=['extra']) except Exception as e: rendered = f'Markdown rendering failed: {e}' return render(request, 'group/group_about_rendertest.html', {'group':group, 'charter':charter, 'rendered':rendered}) def group_about_status(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) status_update = group.latest_event(type='status_update') can_provide_update = can_provide_status_update(request.user, group) return render(request, 'group/group_about_status.html', { 'group' : group, 'status_update': status_update, 'can_provide_status_update': can_provide_update, } ) def group_about_status_meeting(request, acronym, num, group_type=None): meeting = get_meeting(num) group = get_group_or_404(acronym, group_type) status_update = group.status_for_meeting(meeting) return render(request, 'group/group_about_status_meeting.html', { 'group' : group, 'status_update': status_update, 'meeting': meeting, } ) def group_about_status_edit(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) if not can_provide_status_update(request.user, group): raise Http404 old_update = group.latest_event(type='status_update') login = request.user.person if request.method == 'POST': if 'submit_response' in request.POST: form = StatusUpdateForm(request.POST, request.FILES) if form.is_valid(): from_file = form.cleaned_data['txt'] if from_file: update_text = from_file else: update_text = form.cleaned_data['content'] group.groupevent_set.create( by=login, type='status_update', desc=update_text, ) return redirect('ietf.group.views.group_about',acronym=group.acronym) else: form = None else: form = None if not form: form = StatusUpdateForm(initial={"content": old_update.desc if old_update else ""}) return render(request, 'group/group_about_status_edit.html', { 'form': form, 'group':group, } ) def get_group_email_aliases(acronym, group_type): if acronym: pattern = re.compile(r'expand-(%s)(-\w+)@.*? +(.*)$'%acronym) else: pattern = re.compile(r'expand-(.*?)(-\w+)@.*? +(.*)$') aliases = [] with io.open(settings.GROUP_VIRTUAL_PATH,"r") as virtual_file: for line in virtual_file.readlines(): m = pattern.match(line) if m: if acronym or not group_type or Group.objects.filter(acronym=m.group(1),type__slug=group_type): aliases.append({'acronym':m.group(1),'alias_type':m.group(2),'expansion':m.group(3)}) return aliases def email(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) aliases = get_group_email_aliases(acronym, group_type) expansions = gather_relevant_expansions(group=group) return render(request, 'group/email.html', construct_group_menu_context(request, group, "email expansions", group_type, { 'expansions':expansions, 'aliases':aliases, 'group':group, 'ietf_domain':settings.IETF_DOMAIN, })) def history(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) events = group.groupevent_set.all().select_related('by').order_by('-time', '-id') can_add_comment = is_authorized_in_group(request.user,group) return render(request, 'group/history.html', construct_group_menu_context(request, group, "history", group_type, { "group": group, "events": events, "can_add_comment": can_add_comment, })) def materials(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) if not group.features.has_nonsession_materials: raise Http404 docs = get_group_materials(group).order_by("type__order", "-time").select_related("type") doc_types = OrderedDict() for d in docs: if d.type not in doc_types: doc_types[d.type] = [] doc_types[d.type].append(d) return render(request, 'group/materials.html', construct_group_menu_context(request, group, "materials", group_type, { "doc_types": list(doc_types.items()), "can_manage_materials": can_manage_materials(request.user, group) })) @cache_page(60 * 60) def dependencies(request, acronym, group_type=None, output_type="pdf"): group = get_group_or_404(acronym, group_type) if not group.features.has_documents or output_type not in ["dot", "pdf", "svg"]: raise Http404 dothandle, dotname = mkstemp() os.close(dothandle) dotfile = io.open(dotname, "w") dotfile.write(make_dot(group)) dotfile.close() if (output_type == "dot"): return HttpResponse(make_dot(group), content_type='text/plain; charset=UTF-8' ) unflathandle, unflatname = mkstemp() os.close(unflathandle) outhandle, outname = mkstemp() os.close(outhandle) pipe("%s -f -l 10 -o %s %s" % (settings.UNFLATTEN_BINARY, unflatname, dotname)) pipe("%s -T%s -o %s %s" % (settings.DOT_BINARY, output_type, outname, unflatname)) outhandle = io.open(outname, "rb") out = outhandle.read() outhandle.close() os.unlink(outname) os.unlink(unflatname) os.unlink(dotname) if (output_type == "pdf"): output_type = "application/pdf" elif (output_type == "svg"): output_type = "image/svg+xml" return HttpResponse(out, content_type=output_type) def email_aliases(request, acronym=None, group_type=None): group = get_group_or_404(acronym,group_type) if acronym else None if not acronym: # require login for the overview page, but not for the group-specific # pages if not request.user.is_authenticated: return redirect('%s?next=%s' % (settings.LOGIN_URL, request.path)) aliases = get_group_email_aliases(acronym, group_type) return render(request,'group/email_aliases.html',{'aliases':aliases,'ietf_domain':settings.IETF_DOMAIN,'group':group}) def meetings(request, acronym=None, group_type=None): group = get_group_or_404(acronym,group_type) if acronym else None four_years_ago = datetime.datetime.now()-datetime.timedelta(days=4*365) sessions = add_event_info_to_session_qs( group.session_set.filter( meeting__date__gt=four_years_ago, type__in=['regular','plenary','other'] ) ).filter( current_status__in=['sched','schedw','appr','canceled'], ) future, in_progress, recent, past = group_sessions(sessions) can_edit = group.has_role(request.user,group.features.groupman_roles) can_always_edit = has_role(request.user,["Secretariat","Area Director"]) return render(request,'group/meetings.html', construct_group_menu_context(request, group, "meetings", group_type, { 'group':group, 'future':future, 'in_progress':in_progress, 'recent':recent, 'past':past, 'can_edit':can_edit, 'can_always_edit':can_always_edit, })) def chair_photos(request, group_type=None): roles = sorted(Role.objects.filter(group__type=group_type, group__state='active', name_id='chair'),key=lambda x: x.person.last_name()+x.person.name+x.group.acronym) for role in roles: role.last_initial = role.person.last_name()[0] return render(request, 'group/all_photos.html', {'group_type': group_type, 'role': 'Chair', 'roles': roles }) def reorder_roles(roles, role_names): list = [] for name in role_names: list += [ r for r in roles if r.name_id == name ] list += [ r for r in roles if not r in list ] return list def group_photos(request, group_type=None, acronym=None): group = get_object_or_404(Group, acronym=acronym) roles = sorted(Role.objects.filter(group__acronym=acronym),key=lambda x: x.name.name+x.person.last_name()) roles = reorder_roles(roles, group.features.role_order) for role in roles: role.last_initial = role.person.last_name()[0] return render(request, 'group/group_photos.html', construct_group_menu_context(request, group, "photos", group_type, { 'group_type': group_type, 'roles': roles, 'group':group })) ## 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).docs.add(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): # permission_denied(request, "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.""" def desc(attr, new, old): entry = "%(attr)s changed to %(new)s from %(old)s" if new_group: entry = "%(attr)s changed to %(new)s" return entry % dict(attr=attr, new=new, old=old) def format_resources(resources, fs="\n"): res = [] for r in resources: if r.display_name: res.append("%s %s (%s)" % (r.name.slug, r.value, r.display_name.strip('()'))) else: res.append("%s %s" % (r.name.slug, r.value)) # TODO: This is likely problematic if value has spaces. How then to delineate value and display_name? Perhaps in the short term move to comma or pipe separation. # Might be better to shift to a formset instead of parsing these lines. return fs.join(res) 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]) 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.groupman_roles)): permission_denied(request, "You don't have permission to access this view") else: # This allows ADs to create RG and the IRTF Chair to create WG, but we trust them not to if not has_role(request.user, ("Secretariat", "Area Director", "IRTF Chair")): permission_denied(request, "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) changes = [] # 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.items(): if not attr.endswith("_roles"): continue slug = attr slug = strip_suffix(slug, "_roles") title = f.label new = clean[attr] 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(sorted(x.get_name() for x in new)), ", ".join(sorted(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) if not e.origin or (e.person.user and e.origin == e.person.user.username): e.origin = "role: %s %s" % (group.acronym, slug) e.save() 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" today = datetime.date.today() for deleted_email in deleted: # Verify the person doesn't have a separate reviewer role for the group with a different address if not group.role_set.filter(name_id='reviewer',person=deleted_email.person).exists(): active_assignments = ReviewAssignment.objects.filter( review_request__team=group, reviewer__person=deleted_email.person, state_id__in=['accepted', 'assigned'], ) for assignment in active_assignments: if assignment.review_request.deadline > today: assignment.state_id = 'withdrawn' else: assignment.state_id = 'no-response' # save() will update review_request state to 'requested' # if needed, so that the review can be assigned to someone else assignment.save() changed_personnel.update(set(old)^set(new)) if personnel_change_text!="": changed_personnel = [ str(p) for p in changed_personnel ] personnel_change_text = "%s has updated %s personnel:\n\n" % (request.user.person.plain_name(), group.acronym.upper() ) + personnel_change_text email_personnel_change(request, group, personnel_change_text, changed_personnel) if 'resources' in clean: old_resources = sorted(format_resources(group.groupextresource_set.all()).splitlines()) new_resources = sorted(clean['resources']) if old_resources != new_resources: group.groupextresource_set.all().delete() for u in new_resources: parts = u.split(None, 2) name = parts[0] value = parts[1] display_name = ' '.join(parts[2:]).strip('()') group.groupextresource_set.create(value=value, name_id=name, display_name=display_name) changes.append(('resources', new_resources, desc('Resources', ", ".join(new_resources), ", ".join(old_resources)))) 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) if new.slug == 'replaced': replace_charter_of_replaced_group(group=group, by=request.user.person) else: GroupEvent.objects.create(group=group, time=group.time, by=request.user.person, type="info_changed", desc=desc) group.save() #Handle changes to Closing Note, if any. It's an event, not a group attribute like the others closing_note = "" e = group.latest_event(type='closing_note') if e: closing_note = e.desc if closing_note != clean.get("closing_note", ""): closing_note = clean.get("closing_note", "") e = GroupEvent(group=group, by=request.user.person) e.type = "closing_note" if closing_note == "": e.desc = "(Closing note deleted)" #Flag value so something shows up in history else: e.desc = closing_note e.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: # Not POST: if not new_group: closing_note = "" e = group.latest_event(type='closing_note') if e: closing_note = e.desc if closing_note == "(Closing note deleted)": closing_note = "" init = dict(name=group.name, acronym=group.acronym, state=group.state, 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, resources=format_resources(group.groupextresource_set.all()), closing_note = closing_note, ) else: init = dict() 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)) @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_all_groups_of_type(request.user, group.type_id): permission_denied(request, "You don't have permission to access this view") if request.method == 'POST': form = ConcludeGroupForm(request.POST) if form.is_valid(): instructions = form.cleaned_data['instructions'] closing_note = form.cleaned_data['closing_note'] if closing_note != "": instructions = instructions+"\n\n=====\nClosing note:\n\n"+closing_note 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() if closing_note != "": e = GroupEvent(group=group, by=request.user.person) e.type = "closing_note" e.desc = closing_note e.save() kwargs = {'acronym':group.acronym} if group_type: kwargs['group_type'] = group_type return redirect(group.features.about_page, **kwargs) else: form = ConcludeGroupForm() 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.groupman_roles)): permission_denied(request, "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.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.clear() transitions.next_states.set(next_states) return redirect("ietf.group.views.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.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, }) 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, max_results=1000) return render(request, 'group/stream_documents.html', {'stream':stream, 'docs':docs, 'meta':meta, 'editable':editable } ) 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")): permission_denied(request, "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 %s 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) if not e.origin or e.origin == e.person.user.username: e.origin = "role: %s %s" % (group.acronym, slug) e.save() return redirect("ietf.group.views.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, }, ) 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="application/json") @cache_control(public=True, max_age=30*60) @cache_page(30 * 60) def group_menu_data(request): groups = Group.objects.filter(state="active", parent__state="active").filter(Q(type__features__acts_like_wg=True)|Q(type_id__in=['program','iabasg'])|Q(parent__acronym='ietfadminllc')).order_by("-type_id","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 }) groups_by_parent[g.parent_id].append({ 'acronym': g.acronym, 'name': escape(g.name), 'type': escape(g.type.verbose_name or g.type.name), 'url': url }) return JsonResponse(groups_by_parent) # --- Review views ----------------------------------------------------- def get_open_review_requests_for_team(team, assignment_status=None): open_review_requests = ReviewRequest.objects.filter(team=team).filter( Q(state_id='requested') | Q(state_id='assigned',reviewassignment__state__in=('assigned','accepted')) ).prefetch_related( "type", "state", "doc", "doc__states", ).order_by("-time", "-id").distinct() if assignment_status == "unassigned": open_review_requests = suggested_review_requests_for_team(team) + list(open_review_requests.filter(state_id='requested')) elif assignment_status == "assigned": open_review_requests = list(open_review_requests.filter(state_id='assigned')) 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 unassigned_review_requests = [r for r in get_open_review_requests_for_team(group) if not r.state_id=='assigned'] unassigned_review_requests.sort(key=lambda r: r.doc.name) open_review_assignments = list(ReviewAssignment.objects.filter(review_request__team=group, state_id__in=('assigned','accepted')).order_by('-assigned_on')) today = datetime.date.today() unavailable_periods = current_unavailable_periods_for_reviewers(group) for a in open_review_assignments: a.reviewer_unavailable = any(p.availability == "unavailable" for p in unavailable_periods.get(a.reviewer.person_id, [])) a.due = max(0, (today - a.review_request.deadline).days) closed_review_assignments = ReviewAssignment.objects.filter(review_request__team=group).exclude(state_id__in=('assigned','accepted')).prefetch_related("state","result").order_by('-assigned_on') closed_review_requests = ReviewRequest.objects.filter(team=group).exclude(state__in=("requested", "assigned")).prefetch_related("type", "state", "doc").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( Q(reviewrequestdocevent__type='closed_review_request', reviewrequestdocevent__time__gte=datetime.date.today() - date_limit) | Q(reviewrequestdocevent__isnull=True, time__gte=datetime.date.today() - date_limit) ).distinct() closed_review_assignments = closed_review_assignments.filter(completed_on__gte = datetime.date.today() - date_limit) return render(request, 'group/review_requests.html', construct_group_menu_context(request, group, "review requests", group_type, { "unassigned_review_requests": unassigned_review_requests, "open_review_assignments": open_review_assignments, "closed_review_requests": closed_review_requests, "closed_review_assignments": closed_review_assignments, "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) can_reset_next_reviewer = can_manage and group.reviewteamsettings.reviewer_queue_policy_id == 'RotateAlphabetically' reviewers = get_reviewer_queue_policy(group).default_reviewer_rotation_list(include_unavailable=True) 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() max_closed_reqs = settings.GROUP_REVIEW_MAX_ITEMS_TO_SHOW_IN_REVIEWER_LIST days_back = settings.GROUP_REVIEW_DAYS_TO_SHOW_IN_REVIEWER_LIST if can_manage: secretary_settings = (ReviewSecretarySettings.objects.filter(person= request.user.person, team=group).first() or ReviewSecretarySettings(person=request.user.person, team=group)) if secretary_settings: max_closed_reqs = secretary_settings.max_items_to_show_in_reviewer_list days_back = secretary_settings.days_to_show_in_reviewer_list if max_closed_reqs == None: max_closed_reqs = 10 if days_back == None: days_back = 365 req_data_for_reviewers = latest_review_assignments_for_reviewers(group, days_back) assignment_state_by_slug = { n.slug: n for n in ReviewAssignmentStateName.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.change_reviewer_settings", kwargs=kwargs) if can_access_review_stats_for_team(request.user, group): 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 days_since = 9999 req_data = req_data_for_reviewers.get(person.pk, []) closed_reqs = 0 latest_reqs = [] for d in req_data: if d.state in ["assigned", "accepted"] or closed_reqs < max_closed_reqs: if not d.state in ["assigned", "accepted"]: closed_reqs += 1 latest_reqs.append((d.assignment_pk, d.request_pk, d.doc_name, d.reviewed_rev, d.assigned_time, d.deadline, assignment_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)) if d.state in ["completed", "completed_in_time", "completed_late"]: if d.assigned_time is not None: delta = datetime.datetime.now() - d.assigned_time if d.assignment_to_closure_days is not None: days = int(delta.days - d.assignment_to_closure_days) if days_since > days: days_since = days person.latest_reqs = latest_reqs person.days_since_completed_review = days_since 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), "can_reset_next_reviewer": can_reset_next_reviewer, })) @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): permission_denied(request, "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", "assigned"), team=group).prefetch_related("reviewassignment_set__result"), set(r.doc.name 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 = [] rev = None for r in document_requests.get(req.doc.name, []): # take all on the latest reviewed rev for a in r.reviewassignment_set.all(): if l and rev: if r.doc_id == l[0].doc_id and a.reviewed_rev: if int(a.reviewed_rev) > int(rev): l = [r] elif int(a.reviewed_rev) == int(rev): l.append(r) else: l = [r] rev = l[0].reviewassignment_set.first().reviewed_rev augment_review_requests_with_events(l) req.latest_reqs = l req.wg_chairs = None if req.doc.group: req.wg_chairs = [role.person for role in req.doc.group.role_set.filter(name__slug='chair')] 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 = { str(r.pk): r for r in review_requests if r.pk} posted_reqs = set(request.POST.getlist("reviewrequest", [])) posted_reqs.discard(u'None') current_reqs = set(review_requests_dict.keys()) 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.") 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): reqs_to_assign = [] for review_req in review_requests: action = review_req.form.cleaned_data.get("action") if action=="close": close_review_request(request, review_req, review_req.form.cleaned_data["close"], review_req.form.cleaned_data["close_comment"]) elif action=="assign" and review_req.form.cleaned_data["reviewer"]: if review_req.form.cleaned_data.get("review_type"): review_req.type = review_req.form.cleaned_data.get("review_type") reqs_to_assign.append(review_req) assignments_by_person = dict() for r in reqs_to_assign: person = r.form.cleaned_data["reviewer"].person if not person in assignments_by_person: assignments_by_person[person] = [] assignments_by_person[person].append(r) # Make sure the any assignments to the person at the head # of the rotation queue are processed first so that the queue # rotates before any more assignments are processed reviewer_policy = get_reviewer_queue_policy(group) head_of_rotation = reviewer_policy.default_reviewer_rotation_list_without_skipped()[0] while head_of_rotation in assignments_by_person: for review_req in assignments_by_person[head_of_rotation]: assign_review_request_to_reviewer(request, review_req, review_req.form.cleaned_data["reviewer"],review_req.form.cleaned_data["add_skip"]) reqs_to_assign.remove(review_req) del assignments_by_person[head_of_rotation] head_of_rotation = reviewer_policy.default_reviewer_rotation_list_without_skipped()[0] for review_req in reqs_to_assign: assign_review_request_to_reviewer(request, review_req, review_req.form.cleaned_data["reviewer"],review_req.form.cleaned_data["add_skip"]) 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 return redirect(ietf.group.views.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, }) @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): permission_denied(request, "You do not have permission to perform this action") review_assignments = list(ReviewAssignment.objects.filter( review_request__team=group, state__in=("assigned", "accepted"), ).prefetch_related("reviewer", "review_request__type", "state", "review_request__doc").distinct().order_by("reviewer","-review_request__deadline")) for r in review_assignments: if r.review_request.doc.telechat_date(): r.section = 'For telechat %s' % r.review_request.doc.telechat_date().isoformat() r.section_order='0'+r.section elif r.review_request.type_id == 'early': r.section = 'Early review requests:' r.section_order='2' else: r.section = 'Last calls:' r.section_order='1' e = r.review_request.doc.latest_event(LastCallDocEvent, type="sent_last_call") r.lastcall_ends = e and e.expires.date().isoformat() r.earlier_review = ReviewAssignment.objects.filter(review_request__doc=r.review_request.doc,reviewer__in=r.reviewer.person.email_set.all(),state="completed") if r.earlier_review: earlier_reviews_formatted = ['-{} {} reviewed'.format(ra.reviewed_rev, ra.review_request.type.slug) for ra in r.earlier_review] r.earlier_reviews = '({})'.format(', '.join(earlier_reviews_formatted)) # If a document is both scheduled for a telechat and a last call review, replicate # a copy of the review assignment in the last calls section (#2118) def should_be_replicated_in_last_call_section(r): return r.section.startswith('For telechat') and r.review_request.type_id != 'early' for r in filter(should_be_replicated_in_last_call_section, review_assignments): r_new = copy.copy(r) r_new.section = 'Last calls:' r_new.section_order = '1' review_assignments.append(r_new) review_assignments.sort(key=lambda r: r.section_order + r.reviewer.person.last_name() + r.reviewer.person.first_name()) 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 back_url = urlreverse(ietf.group.views.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, to=form.cleaned_data["to"], frm=form.cleaned_data["frm"], subject=form.cleaned_data["subject"], txt=form.cleaned_data["body"], cc=form.cleaned_data["cc"], extra={"Reply-To": 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_assignments": review_assignments, "rotation_list": get_reviewer_queue_policy(group).default_reviewer_rotation_list()[: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_assignments': review_assignments, 'form': form, 'back_url': back_url, }) @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)): permission_denied(request, "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 kwargs = { "acronym": group.acronym} if group_type: kwargs["group_type"] = group_type back_url = urlreverse(ietf.group.views.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() if settings_form.has_changed(): update_change_reason(settings, "Updated %s" % ", ".join(settings_form.changed_data) ) 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 settings.request_assignment_next: changes.append("Reviewer has requested to be the next person selected for an " "assignment, as soon as possible, and will be on the top of " "the queue.") 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() update_change_reason(period, "Added unavailability period: {}".format(period)) today = datetime.date.today() in_the_past = period.end_date and period.end_date < today if not in_the_past: msg = "{} -- {} {}\n {}".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(), period.reason, ) if period.availability == "unavailable": # the secretary might need to reassign # assignments, so mention the current ones review_assignments = ReviewAssignment.objects.filter(state__in=["assigned", "accepted"], reviewer=reviewer_role.email, review_request__team=group) msg += "\n\n" if review_assignments: msg += "{} is currently assigned to review:".format(reviewer_role.person) for r in review_assignments: msg += "\n\n" msg += "{} (deadline: {})".format(r.review_request.doc.name, r.review_request.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() update_change_reason(period, "Removed unavailability period: {}".format(period)) 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() update_change_reason(period, "Set end date of unavailability period: {}".format(period)) 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, 'unavailable_periods_history': UnavailablePeriod.history.filter(person=reviewer, team=group), 'reviewersettings': settings, }) @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 back_url = urlreverse(ietf.group.views.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, }) class AddCommentForm(forms.Form): comment = forms.CharField(required=True, widget=forms.Textarea, strip=False) def add_comment(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) if not is_authorized_in_group(request.user,group): permission_denied(request, "You need to a chair, secretary, or delegate of this group to add a comment.") if request.method == 'POST': form = AddCommentForm(request.POST) if form.is_valid(): comment = form.cleaned_data['comment'] event = GroupEvent.objects.create(group=group,desc=comment,type="added_comment",by=request.user.person) email_comment(request,event) return redirect('ietf.group.views.history', acronym=group.acronym) else: form = AddCommentForm() return render(request, 'group/add_comment.html', { 'group':group, 'form':form, }) class ResetNextReviewerForm(forms.Form): next_reviewer = forms.ChoiceField() def __init__(self, *args, **kwargs): instance = kwargs.pop('instance') super(ResetNextReviewerForm, self).__init__(*args, **kwargs) self.fields['next_reviewer'].choices = [ (p.pk, p.plain_name()) for p in get_reviewer_queue_policy(instance.team).default_reviewer_rotation_list(include_unavailable=True)] @login_required def reset_next_reviewer(request, acronym, group_type=None): group = get_group_or_404(acronym, group_type) if not group.features.has_reviews: raise Http404 if group.reviewteamsettings.reviewer_queue_policy_id != 'RotateAlphabetically': raise Http404 if not Role.objects.filter(name="secr", group=group, person__user=request.user).exists() and not has_role(request.user, "Secretariat"): permission_denied(request, "You don't have permission to access this view") instance = group.nextreviewerinteam_set.first() if not instance: raise Http404 if request.method == 'POST': form = ResetNextReviewerForm(request.POST,instance=instance) if form.is_valid(): instance.next_reviewer = Person.objects.get(pk=form.cleaned_data['next_reviewer']) instance.save() return redirect('ietf.group.views.reviewer_overview', acronym = group.acronym ) else: form = ResetNextReviewerForm(instance=instance) return render(request, 'group/reset_next_reviewer.html', { 'group':group, 'form': form,})