feat: move IAB appeals into the datatracker ()

* feat: basic models for appeals

* fix: modify appeal model to point to group

* fix: explicit date on Appeal objects

* feat: appeals importing management command

* feat: display appeals

* feat: admin for appeals

* fix: limit admin contentype choices

* feat: tastypie resources

* feat: factories and tests

* chore: update group migration

* fix: remove charset from pdf content type

* test: unittest download_name

* fix: admin for new name
This commit is contained in:
Robert Sparks 2023-08-29 14:07:30 -05:00 committed by GitHub
parent 79e7145363
commit 852f9d90b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 919 additions and 36 deletions

View file

@ -2799,7 +2799,7 @@ class PdfizedTests(TestCase):
url = urlreverse(self.view, kwargs=argdict)
r = self.client.get(url)
self.assertEqual(r.status_code,200)
self.assertEqual(r.get('Content-Type'),'application/pdf;charset=utf-8')
self.assertEqual(r.get('Content-Type'),'application/pdf')
def should_404(self, argdict):
url = urlreverse(self.view, kwargs=argdict)

View file

@ -976,7 +976,7 @@ def document_pdfized(request, name, rev=None, ext=None):
pdf = doc.pdfized()
if pdf:
return HttpResponse(pdf,content_type='application/pdf;charset=utf-8')
return HttpResponse(pdf,content_type='application/pdf')
else:
raise Http404

View file

@ -5,6 +5,8 @@ import re
from functools import update_wrapper
from base64 import b64encode
import debug # pyflakes:ignore
from django import forms
@ -12,6 +14,7 @@ from django import forms
from django.contrib import admin
from django.contrib.admin.utils import unquote
from django.core.management import load_command_class
from django.db.models import BinaryField
from django.http import Http404
from django.shortcuts import render
from django.utils.encoding import force_str
@ -20,7 +23,7 @@ from django.utils.translation import gettext as _
from ietf.group.models import (Group, GroupFeatures, GroupHistory, GroupEvent, GroupURL, GroupMilestone,
GroupMilestoneHistory, GroupStateTransitions, Role, RoleHistory, ChangeStateGroupEvent,
MilestoneGroupEvent, GroupExtResource, )
MilestoneGroupEvent, GroupExtResource, Appeal, AppealArtifact )
from ietf.name.models import GroupTypeName
from ietf.utils.validators import validate_external_resource_value
@ -291,3 +294,52 @@ class GroupExtResourceAdmin(admin.ModelAdmin):
search_fields = ['group__acronym', 'value', 'display_name', 'name__slug',]
raw_id_fields = ['group', ]
admin.site.register(GroupExtResource, GroupExtResourceAdmin)
class AppealAdmin(admin.ModelAdmin):
list_display = ["group", "date", "name"]
search_fields = ["group__acronym", "date", "name"]
raw_id_fields = ["group"]
admin.site.register(Appeal, AppealAdmin)
# From https://stackoverflow.com/questions/58529099/adding-file-upload-widget-for-binaryfield-to-django-admin
class BinaryFileInput(forms.ClearableFileInput):
def is_initial(self, value):
"""
Return whether value is considered to be initial value.
"""
return bool(value)
def format_value(self, value):
"""Format the size of the value in the db.
We can't render it's name or url, but we'd like to give some information
as to wether this file is not empty/corrupt.
"""
if self.is_initial(value):
return f'{len(value)} bytes'
def value_from_datadict(self, data, files, name):
"""Return the file contents so they can be put in the db."""
upload = super().value_from_datadict(data, files, name)
if upload:
bits = upload.read()
return b64encode(bits).decode("ascii") # Who made this so hard?
class RestrictContentTypeChoicesForm(forms.ModelForm):
content_type = forms.ChoiceField(
choices=(
( "text/markdown;charset=utf-8", "Markdown"),
( "application/pdf", "PDF")
)
)
class AppealArtifactAdmin(admin.ModelAdmin):
list_display = ["display_title", "appeal","date"]
ordering = ["-appeal__date", "date"]
formfield_overrides = {
BinaryField: { "widget": BinaryFileInput() },
}
form = RestrictContentTypeChoicesForm
admin.site.register(AppealArtifact, AppealArtifactAdmin)

View file

@ -7,8 +7,16 @@ from typing import List # pyflakes:ignore
from django.utils import timezone
from ietf.group.models import Group, Role, GroupEvent, GroupMilestone, \
GroupHistory, RoleHistory
from ietf.group.models import (
Appeal,
AppealArtifact,
Group,
GroupEvent,
GroupMilestone,
GroupHistory,
Role,
RoleHistory
)
from ietf.review.factories import ReviewTeamSettingsFactory
from ietf.utils.timezone import date_today
@ -120,3 +128,34 @@ class RoleHistoryFactory(factory.django.DjangoModelFactory):
person = factory.SubFactory('ietf.person.factories.PersonFactory')
email = factory.LazyAttribute(lambda obj: obj.person.email())
class AppealFactory(factory.django.DjangoModelFactory):
class Meta:
model=Appeal
name=factory.Faker("sentence")
group=factory.SubFactory(GroupFactory, acronym="iab")
class AppealArtifactFactory(factory.django.DjangoModelFactory):
class Meta:
model=AppealArtifact
appeal = factory.SubFactory(AppealFactory)
artifact_type = factory.SubFactory("ietf.name.factories.AppealArtifactTypeNameFactory", slug="appeal")
content_type = "text/markdown;charset=utf-8"
# Needs newer factory_boy
# bits = factory.Transformer(
# "Some example **Markdown**",
# lambda o: memoryview(o.encode("utf-8") if isinstance(o,str) else o)
# )
#
# Usage: a = AppealArtifactFactory(set_bits__using="foo bar") or
# a = AppealArtifactFactory(set_bits__using=b"foo bar")
@factory.post_generation
def set_bits(obj, create, extracted, **kwargs):
if not create:
return
using = kwargs.pop("using","Some example **Markdown**")
if isinstance(using, str):
using = using.encode("utf-8")
obj.bits = memoryview(using)

View file

@ -0,0 +1,205 @@
# Copyright The IETF Trust 2023, All Rights Reserved
import debug # pyflakes: ignore
import datetime
import shutil
import subprocess
import tempfile
from pathlib import Path
from django.core.management.base import BaseCommand
from ietf.group.models import Appeal, AppealArtifact
from ietf.name.models import AppealArtifactTypeName
PDF_FILES = [
"2006-01-04-appeal.pdf",
"2006-08-24-appeal.pdf",
"2006-09-11-appeal.pdf",
"2008-11-29-appeal.pdf",
"2010-06-07-appeal.pdf",
"2010-06-07-response.pdf",
"2013-07-08-appeal.pdf",
"2015-06-22-appeal.pdf",
"2019-01-31-appeal.pdf",
"2019-01-31-response.pdf",
]
NAME_PART_MAP = {
"appeal": "appeal",
"response": "response",
"appeal_with_response": "response",
"reply_to_response": "reply",
}
def bits_name(date, part):
part_type = part["type"]
name_fragment = NAME_PART_MAP[part_type]
prefix = f"{date:%Y-%m-%d}-{name_fragment}"
if f"{prefix}.pdf" in PDF_FILES:
ext = "pdf"
else:
ext = "md"
return f"{prefix}.{ext}"
def date_from_string(datestring):
year, month, day = [int(part) for part in datestring.split("-")]
return datetime.date(year, month, day)
def work_to_do():
# Taken from https://www.iab.org/appeals/ on 2023-08-24 - some lines carved out below as exceptions
input = """
2020-07-31 IAB appeal for arpa assignment (Timothy McSweeney) IAB Response (2020-08-26)
2019-01-31 An appeal to make the procedure related to Independent Submission Stream more transparent (Shyam Bandyopadhyay) IAB Response (2019-03-06)
2015-06-22 Appeal to the IAB concerning the IESG response to his appeal concerning the IESG approval of the draft-ietf-ianaplan-icg-response (JFC Morfin) IAB Response (2015-07-08)
2013-07-08 Appeal to the IAB irt. RFC 6852 (JFC Morfin) IAB Response (2013-07-17)
2010-06-07 Appeal over the IESG Publication of the IDNA2008 Document Set Without Appropriate Explanation to the Internet Community (JFC Morfin) IAB Response (2010-08-20)
2008-11-29 Appeal to the IAB Concerning the Way Users Are Not Permitted To Adequately Contribute to the IETF (JFC Morfin) IAB Response (2009-01-28)
2006-10-10 Complaints about suspension from the ietf@ietf.org mailing list (Todd Glassey) IAB Response (2006-10-31)
2006-09-11 Appeal to the IAB over IESG dismissed appeals from J-F C. Morfin (JFC Morfin) IAB Response (2006-12-05)
2006-09-10 Appeal of IESG Decision of July 10, 2006 from Dean Anderson (Dean Anderson) IAB Response (2006-09-27)
2006-08-24 Appeal Against the decision to consider expediting an RFC Publication from J-F C. Morfin (JFC Morfin) IAB Response (2006-09-07)
2006-04-18 Appeal Against IESG PR-Action from Dean Anderson (Dean Anderson) IAB Response (2006-07-13)
2006-02-08 Appeal Against IESG Decision by Julian Mehnle (Julian Mehnle) IAB Response (2006-03-02)
2006-01-04 Appeal Against IESG Decision by Jefsey Morfin (JFC Morfin) IAB Response (2006-01-31)
2003-01-04 Appeal against IESG decision (Robert Elz) IAB Response (includes original appeal)(2003-02-15)
2000-11-15 Appeal Against IESG Action by Mr. D J Bernstein (D J Bernstein) IAB Response (2001-02-26)
1999-10-23 Appeal against IESG Inaction by W.A. Simpson (William Allen Simpson) IAB Response (2000-01-11)
1999-05-01 Appeal against IESG action (William Allen Simpson) IAB Response (1999-10-05)
1996-03-06 Appeal SNMPv2 SMI Appeal by Mr. David T. Perkins, IAB consideration (David Perkins) IAB Response (includes original appeal) (1996-03-06)
"""
work = []
for line in input.split("\n"):
line = line.strip()
if line == "":
continue
appeal_date = line[:10]
response_date = line[-11:-1]
title = line[11:-12].strip().split(")")[0] + ")"
item = dict(title=title, date=appeal_date, parts=[])
if appeal_date in [
"2006-10-10",
"2000-11-15",
"1999-10-23",
"1999-05-01",
"1996-03-06",
]:
item["parts"].append(dict(type="appeal_with_response", date=response_date))
else:
item["parts"].append(dict(type="appeal", date=appeal_date))
item["parts"].append(dict(type="response", date=response_date))
work.append(item)
# Hand building the items for the following
# exceptions="""
# 2003-10-09 Appeal to the IAB on the site-local issue (Tony Hain)
# IAB Response (2003-11-12)
# Tony Hain reply to IAB Response (2003-11-18)
# 1995-02-18 (etc.) Appeal Against IESG Inaction by Mr. Dave Cocker, Mr W. Simpson (Dave Crocker, William Allen Simpson) IAB Response (1995-04-04 and 1995-04-05)
# """
item = dict(
title="Appeal to the IAB on the site-local issue (Tony Hain)",
date="2003-10-09",
parts=[],
)
item["parts"].append(
dict(
type="appeal",
date="2003-10-09",
)
)
item["parts"].append(
dict(
type="response",
date="2003-11-12",
)
)
item["parts"].append(
dict(
type="reply_to_response",
date="2003-11-18",
)
)
work.append(item)
item = dict(
title="Appeal Against IESG Inaction by Mr. Dave Cocker, Mr W. Simpson (Dave Crocker, William Allen Simpson)",
date="1995-02-18",
parts=[],
)
item["parts"].append(
dict(
type="appeal",
date="1995-02-18",
)
)
item["parts"].append(
dict(
type="response",
date="1995-04-05",
)
)
work.append(item)
for item in work:
item["date"] = date_from_string(item["date"])
for part in item["parts"]:
part["date"] = date_from_string(part["date"])
work.sort(key=lambda o: o["date"])
return work
class Command(BaseCommand):
help = "Performs a one-time import of IAB appeals"
def handle(self, *args, **options):
tmpdir = tempfile.mkdtemp()
process = subprocess.Popen(
["git", "clone", "https://github.com/kesara/iab-scraper.git", tmpdir],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
stdout, stderr = process.communicate()
if not Path(tmpdir).joinpath("iab_appeals", "1995-02-18-appeal.md").exists():
print("Git clone of the iab-scraper directory did not go as expected")
print("stdout:", stdout)
print("stderr:", stderr)
print(f"Clean up {tmpdir} manually")
exit(-1)
work = work_to_do()
for item in work:
# IAB is group 7
appeal = Appeal.objects.create(name=item["title"], date=item["date"], group_id=7)
for part in item["parts"]:
bits_file_name = bits_name(item["date"], part)
if bits_file_name.endswith(".pdf"):
content_type = "application/pdf"
else:
content_type = "text/markdown;charset=utf-8"
with Path(tmpdir).joinpath("iab_appeals", bits_file_name).open(
"rb"
) as source_file:
bits = source_file.read()
artifact_type = AppealArtifactTypeName.objects.get(slug=part["type"])
AppealArtifact.objects.create(
appeal = appeal,
artifact_type=artifact_type,
date=part["date"],
content_type=content_type,
bits=bits,
)
shutil.rmtree(tmpdir)

View file

@ -0,0 +1,83 @@
# Copyright The IETF Trust 2023, All Rights Reserved
from django.db import migrations, models
import django.db.models.deletion
import ietf.utils.models
import ietf.utils.timezone
class Migration(migrations.Migration):
dependencies = [
("name", "0007_appeal_artifact_typename"),
("group", "0001_initial"),
]
operations = [
migrations.CreateModel(
name="Appeal",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=512)),
("date", models.DateField(default=ietf.utils.timezone.date_today)),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="group.group"
),
),
],
options={
"ordering": ["-date", "-id"],
},
),
migrations.CreateModel(
name="AppealArtifact",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("date", models.DateField(default=ietf.utils.timezone.date_today)),
(
"title",
models.CharField(
blank=True,
help_text="The artifact_type.name will be used if this field is blank",
max_length=256,
),
),
("order", models.IntegerField(default=0)),
("content_type", models.CharField(max_length=32)),
("bits", models.BinaryField(editable=True)),
(
"appeal",
ietf.utils.models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="group.appeal"
),
),
(
"artifact_type",
ietf.utils.models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="name.appealartifacttypename",
),
),
],
options={
"ordering": ["date", "order", "artifact_type__order"],
},
),
]

View file

@ -13,16 +13,19 @@ from django.db import models
from django.db.models.deletion import CASCADE, PROTECT
from django.dispatch import receiver
from django.utils import timezone
from django.utils.text import slugify
import debug # pyflakes:ignore
from ietf.name.models import (GroupStateName, GroupTypeName, DocTagName, GroupMilestoneStateName, RoleName,
AgendaTypeName, AgendaFilterTypeName, ExtResourceName, SessionPurposeName)
AgendaTypeName, AgendaFilterTypeName, ExtResourceName, SessionPurposeName,
AppealArtifactTypeName )
from ietf.person.models import Email, Person
from ietf.utils.db import IETFJSONField
from ietf.utils.mail import formataddr, send_mail_text
from ietf.utils import log
from ietf.utils.models import ForeignKey, OneToOneField
from ietf.utils.timezone import date_today
from ietf.utils.validators import JSONForeignKeyListValidator
@ -409,6 +412,46 @@ class RoleHistory(models.Model):
class Meta:
verbose_name_plural = "role histories"
class Appeal(models.Model):
name = models.CharField(max_length=512)
group = models.ForeignKey(Group, on_delete=models.PROTECT)
date = models.DateField(default=date_today)
class Meta:
ordering = ['-date', '-id']
def __str__(self):
return f"{self.date} - {self.name}"
class AppealArtifact(models.Model):
appeal = ForeignKey(Appeal)
artifact_type = ForeignKey(AppealArtifactTypeName)
date = models.DateField(default=date_today)
title = models.CharField(max_length=256, blank=True, help_text="The artifact_type.name will be used if this field is blank")
order = models.IntegerField(default=0)
content_type = models.CharField(max_length=32)
# "Abusing" BinaryField (see the django docs) for the small number of
# these things we have on purpose. Later, any non-markdown content may
# move off into statics instead.
bits = models.BinaryField(editable=True)
class Meta:
ordering = ['date', 'order', 'artifact_type__order']
def display_title(self):
if self.title != "":
return self.title
else:
return self.artifact_type.name
def is_markdown(self):
return self.content_type == "text/markdown;charset=utf-8"
def download_name(self):
return f"{self.date}-{slugify(self.display_title())}.{'md' if self.is_markdown() else 'pdf'}"
def __str__(self):
return f"{self.date} {self.display_title()} : {self.appeal.name}"
# --- Signal hooks for group models ---

View file

@ -13,7 +13,7 @@ from ietf import api
from ietf.group.models import (Group, GroupStateTransitions, GroupMilestone, GroupHistory, # type: ignore
GroupURL, Role, GroupEvent, RoleHistory, GroupMilestoneHistory, MilestoneGroupEvent,
ChangeStateGroupEvent, GroupFeatures, GroupExtResource)
ChangeStateGroupEvent, GroupFeatures, GroupExtResource, Appeal, AppealArtifact)
from ietf.person.resources import PersonResource
@ -333,3 +333,42 @@ class GroupExtResourceResource(ModelResource):
"name": ALL_WITH_RELATIONS,
}
api.group.register(GroupExtResourceResource())
class AppealResource(ModelResource):
group = ToOneField(GroupResource, 'group')
class Meta:
queryset = Appeal.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'appeal'
ordering = ['id', ]
filtering = {
"id": ALL,
"name": ALL,
"date": ALL,
"group": ALL_WITH_RELATIONS,
}
api.group.register(AppealResource())
from ietf.name.resources import AppealArtifactTypeNameResource
class AppealArtifactResource(ModelResource):
appeal = ToOneField(AppealResource, 'appeal')
artifact_type = ToOneField(AppealArtifactTypeNameResource, 'artifact_type')
class Meta:
excludes= ("bits",)
queryset = AppealArtifact.objects.all()
serializer = api.Serializer()
cache = SimpleCache()
#resource_name = 'appealartifact'
ordering = [ "id", ]
filtering = {
"id": ALL,
"date": ALL,
"title": ALL,
"order": ALL,
"content_type": ALL,
"appeal": ALL_WITH_RELATIONS,
"artifact_type": ALL_WITH_RELATIONS,
}
api.group.register(AppealArtifactResource())

View file

@ -0,0 +1,71 @@
# Copyright The IETF Trust 2023, All Rights Reserved
import debug # pyflakes: ignore
import datetime
from pyquery import PyQuery
from django.urls import reverse as urlreverse
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
from ietf.group.factories import (
AppealFactory,
AppealArtifactFactory,
)
class AppealTests(TestCase):
def test_download_name(self):
artifact = AppealArtifactFactory()
self.assertEqual(artifact.download_name(),f"{artifact.date}-appeal.md")
artifact = AppealArtifactFactory(content_type="application/pdf",artifact_type__slug="response")
self.assertEqual(artifact.download_name(),f"{artifact.date}-response.pdf")
def test_appeal_list_view(self):
appeal_date = datetime.date.today()-datetime.timedelta(days=14)
response_date = appeal_date+datetime.timedelta(days=8)
appeal = AppealFactory(name="A name to look for", date=appeal_date)
appeal_artifact = AppealArtifactFactory(appeal=appeal, artifact_type__slug="appeal", date=appeal_date)
response_artifact = AppealArtifactFactory(appeal=appeal, artifact_type__slug="response", content_type="application/pdf", date=response_date)
url = urlreverse("ietf.group.views.appeals", kwargs=dict(acronym="iab"))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(len(q("#appeals > tbody > tr")), 1)
self.assertEqual(q("#appeal-1-date").text(), f"{appeal_date}")
self.assertEqual(f"{appeal_artifact.display_title()} - {appeal_date}", q("#artifact-1-1").text())
self.assertEqual(f"{response_artifact.display_title()} - {response_date}", q("#artifact-1-2").text())
self.assertIsNone(q("#artifact-1-1").attr("download"))
self.assertEqual(q("#artifact-1-2").attr("download"), response_artifact.download_name())
def test_markdown_view(self):
artifact = AppealArtifactFactory()
url = urlreverse("ietf.group.views.appeal_artifact", kwargs=dict(acronym="iab", artifact_id=artifact.pk))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(q("#content>p>strong").text(),"Markdown")
self.assertIsNone(q("#content a").attr("download"))
self.client.login(username='secretary', password='secretary+password')
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
q = PyQuery(r.content)
self.assertEqual(q("#content a").attr("download"), artifact.download_name())
def test_markdown_download(self):
artifact = AppealArtifactFactory()
url = urlreverse("ietf.group.views.appeal_artifact_markdown", kwargs=dict(acronym="iab", artifact_id=artifact.pk))
login_testing_unauthorized(self, "secretary", url)
r = self.client.get(url)
self.assertContains(r, "**Markdown**", status_code=200)
def test_pdf_download(self):
artifact = AppealArtifactFactory(content_type="application/pdf") # The bits won't _really_ be pdf
url = urlreverse("ietf.group.views.appeal_artifact", kwargs=dict(acronym="iab", artifact_id=artifact.pk))
r = self.client.get(url)
self.assertEqual(r.status_code, 200)
self.assertEqual(r.get("Content-Disposition"), f'attachment; filename="{artifact.download_name()}"')
self.assertEqual(r.get("Content-Type"), artifact.content_type)
self.assertEqual(r.content, artifact.bits.tobytes())

View file

@ -48,6 +48,11 @@ info_detail_urls = [
url(r'^reset_next_reviewer/$', views.reset_next_reviewer),
url(r'^email-aliases/$', RedirectView.as_view(pattern_name=views.email,permanent=False),name='ietf.group.urls_info_details.redirect.email'),
url(r'^statements/$', views.statements),
url(r'^appeals/$', views.appeals),
url(r'^appeals/artifact/(?P<artifact_id>\d+)$', views.appeal_artifact),
url(r'^appeals/artifact/(?P<artifact_id>\d+)/markdown$', views.appeal_artifact_markdown),
]

View file

@ -235,6 +235,7 @@ def construct_group_menu_context(request, group, selected, group_type, others):
entries.append(("Meetings", urlreverse("ietf.group.views.meetings", kwargs=kwargs)))
if group.acronym in ["iab", "iesg"]:
entries.append(("Statements", urlreverse("ietf.group.views.statements", kwargs=kwargs)))
entries.append(("Appeals", urlreverse("ietf.group.views.appeals", kwargs=kwargs)))
entries.append(("History", urlreverse("ietf.group.views.history", kwargs=kwargs)))
entries.append(("Photos", urlreverse("ietf.group.views.group_photos", kwargs=kwargs)))
entries.append(("Email expansions", urlreverse("ietf.group.views.email", kwargs=kwargs)))

View file

@ -72,7 +72,7 @@ from ietf.group.forms import (GroupForm, StatusUpdateForm, ConcludeGroupForm, St
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 )
ChangeStateGroupEvent, GroupFeatures, AppealArtifact )
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, group_attribute_change_desc,
@ -111,7 +111,7 @@ 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.ietfauth.utils import user_is_person, role_required
from ietf.dbtemplate.models import DBTemplate
from ietf.mailtrigger.utils import gather_address_lists
from ietf.mailtrigger.models import Recipient
@ -2119,5 +2119,48 @@ def statements(request, acronym, group_type=None):
),
)
def appeals(request, acronym, group_type=None):
if not acronym in ["iab", "iesg"]:
raise Http404
group = get_group_or_404(acronym, group_type)
appeals = group.appeal_set.all()
return render(
request,
"group/appeals.html",
construct_group_menu_context(
request,
group,
"appeals",
group_type,
{
"group": group,
"appeals": appeals,
},
),
)
def appeal_artifact(request, acronym, artifact_id, group_type=None):
artifact = get_object_or_404(AppealArtifact, pk=artifact_id)
if artifact.is_markdown():
artifact_html = markdown.markdown(artifact.bits.tobytes().decode("utf-8"))
return render(
request,
"group/appeal_artifact.html",
dict(artifact=artifact, artifact_html=artifact_html)
)
else:
return HttpResponse(
artifact.bits,
headers = {
"Content-Type": artifact.content_type,
"Content-Disposition": f'attachment; filename="{artifact.download_name()}"'
}
)
@role_required("Secretariat")
def appeal_artifact_markdown(request, acronym, artifact_id, group_type=None):
artifact = get_object_or_404(AppealArtifact, pk=artifact_id)
if artifact.is_markdown():
return HttpResponse(artifact.bits, content_type=artifact.content_type)
else:
raise Http404

View file

@ -2,63 +2,141 @@
from django.contrib import admin
from ietf.name.models import (
AgendaTypeName, BallotPositionName, ConstraintName, ContinentName, CountryName, DBTemplateTypeName,
DocRelationshipName, DocReminderTypeName, DocTagName, DocTypeName, DraftSubmissionStateName,
FeedbackTypeName, FormalLanguageName, GroupMilestoneStateName, GroupStateName, GroupTypeName,
ImportantDateName, IntendedStdLevelName, IprDisclosureStateName, IprEventTypeName,
IprLicenseTypeName, LiaisonStatementEventTypeName, LiaisonStatementPurposeName,
LiaisonStatementState, LiaisonStatementTagName, MeetingTypeName, NomineePositionStateName,
ReviewRequestStateName, ReviewResultName, ReviewTypeName, RoleName, RoomResourceName,
SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName, TopicAudienceName,
DocUrlTagName, ReviewAssignmentStateName, ReviewerQueuePolicyName, TimerangeName,
ExtResourceName, ExtResourceTypeName, SlideSubmissionStatusName, ProceedingsMaterialTypeName,
AgendaFilterTypeName, SessionPurposeName, TelechatAgendaSectionName )
AgendaTypeName,
BallotPositionName,
ConstraintName,
ContinentName,
CountryName,
DBTemplateTypeName,
DocRelationshipName,
DocReminderTypeName,
DocTagName,
DocTypeName,
DraftSubmissionStateName,
FeedbackTypeName,
FormalLanguageName,
GroupMilestoneStateName,
GroupStateName,
GroupTypeName,
ImportantDateName,
IntendedStdLevelName,
IprDisclosureStateName,
IprEventTypeName,
IprLicenseTypeName,
LiaisonStatementEventTypeName,
LiaisonStatementPurposeName,
LiaisonStatementState,
LiaisonStatementTagName,
MeetingTypeName,
NomineePositionStateName,
ReviewRequestStateName,
ReviewResultName,
ReviewTypeName,
RoleName,
RoomResourceName,
SessionStatusName,
StdLevelName,
StreamName,
TimeSlotTypeName,
TopicAudienceName,
DocUrlTagName,
ReviewAssignmentStateName,
ReviewerQueuePolicyName,
TimerangeName,
ExtResourceName,
ExtResourceTypeName,
SlideSubmissionStatusName,
ProceedingsMaterialTypeName,
AgendaFilterTypeName,
SessionPurposeName,
TelechatAgendaSectionName,
AppealArtifactTypeName,
)
from ietf.stats.models import CountryAlias
class NameAdmin(admin.ModelAdmin):
list_display = ["slug", "name", "desc", "used", "order"]
search_fields = ["slug", "name"]
prepopulate_from = { "slug": ("name",) }
prepopulate_from = {"slug": ("name",)}
class DocRelationshipNameAdmin(NameAdmin):
list_display = ["slug", "name", "revname", "desc", "used"]
admin.site.register(DocRelationshipName, DocRelationshipNameAdmin)
class DocTypeNameAdmin(NameAdmin):
list_display = ["slug", "name", "prefix", "desc", "used"]
admin.site.register(DocTypeName, DocTypeNameAdmin)
class GroupTypeNameAdmin(NameAdmin):
list_display = ["slug", "name", "verbose_name", "desc", "used"]
admin.site.register(GroupTypeName, GroupTypeNameAdmin)
class CountryAliasInline(admin.TabularInline):
model = CountryAlias
extra = 1
class CountryNameAdmin(NameAdmin):
list_display = ["slug", "name", "continent", "in_eu"]
list_filter = ["continent", "in_eu"]
inlines = [CountryAliasInline]
admin.site.register(CountryName, CountryNameAdmin)
class ImportantDateNameAdmin(NameAdmin):
list_display = ["slug", "name", "desc", "used", "default_offset_days"]
ordering = ('-used','default_offset_days',)
admin.site.register(ImportantDateName,ImportantDateNameAdmin)
ordering = (
"-used",
"default_offset_days",
)
admin.site.register(ImportantDateName, ImportantDateNameAdmin)
class ExtResourceNameAdmin(NameAdmin):
list_display = ["slug", "name", "type", "desc", "used",]
admin.site.register(ExtResourceName,ExtResourceNameAdmin)
list_display = [
"slug",
"name",
"type",
"desc",
"used",
]
admin.site.register(ExtResourceName, ExtResourceNameAdmin)
class ProceedingsMaterialTypeNameAdmin(NameAdmin):
list_display = ["slug", "name", "desc", "used", "order",]
list_display = [
"slug",
"name",
"desc",
"used",
"order",
]
admin.site.register(ProceedingsMaterialTypeName, ProceedingsMaterialTypeNameAdmin)
admin.site.register(AgendaFilterTypeName, NameAdmin)
admin.site.register(AgendaTypeName, NameAdmin)
admin.site.register(AppealArtifactTypeName, NameAdmin)
admin.site.register(BallotPositionName, NameAdmin)
admin.site.register(ConstraintName, NameAdmin)
admin.site.register(ContinentName, NameAdmin)

13
ietf/name/factories.py Normal file
View file

@ -0,0 +1,13 @@
# Copyright The IETF Trust 2023, All Rights Reserved
# -*- coding: utf-8 -*-
import factory
from .models import (
AppealArtifactTypeName,
)
class AppealArtifactTypeNameFactory(factory.django.DjangoModelFactory):
class Meta:
model = AppealArtifactTypeName
django_get_or_create = ("slug",)

View file

@ -6615,6 +6615,56 @@
"model": "name.agendatypename",
"pk": "workshop"
},
{
"fields": {
"desc": "The content of an appeal",
"name": "Appeal",
"order": 1,
"used": true
},
"model": "name.appealartifacttypename",
"pk": "appeal"
},
{
"fields": {
"desc": "The content of an appeal combined with the content of a response",
"name": "Response (with appeal included)",
"order": 2,
"used": true
},
"model": "name.appealartifacttypename",
"pk": "appeal_with_response"
},
{
"fields": {
"desc": "Other content related to an appeal",
"name": "Other content",
"order": 5,
"used": true
},
"model": "name.appealartifacttypename",
"pk": "other_content"
},
{
"fields": {
"desc": "The content of a reply to an appeal response",
"name": "Reply to response",
"order": 4,
"used": true
},
"model": "name.appealartifacttypename",
"pk": "reply_to_response"
},
{
"fields": {
"desc": "The content of a response to an appeal",
"name": "Response",
"order": 3,
"used": true
},
"model": "name.appealartifacttypename",
"pk": "response"
},
{
"fields": {
"blocking": false,
@ -11773,13 +11823,24 @@
"model": "name.importantdatename",
"pk": "draftwgagenda"
},
{
"fields": {
"default_offset_days": -12,
"desc": "Early registration and payment cut-off at UTC 23:59",
"name": "Early cutoff",
"order": 0,
"used": true
},
"model": "name.importantdatename",
"pk": "early"
},
{
"fields": {
"default_offset_days": -47,
"desc": "Early Bird registration and payment cut-off at UTC 23:59",
"name": "Earlybird cutoff",
"order": 0,
"used": true
"used": false
},
"model": "name.importantdatename",
"pk": "earlybird"
@ -11900,11 +11961,22 @@
"desc": "Standard rate registration and payment cut-off at UTC 23:59.",
"name": "Standard rate registration ends",
"order": 18,
"used": true
"used": false
},
"model": "name.importantdatename",
"pk": "stdratecutoff"
},
{
"fields": {
"default_offset_days": -47,
"desc": "Super Early registration cutoff at UTC 23:59",
"name": "Super Early cutoff",
"order": 0,
"used": true
},
"model": "name.importantdatename",
"pk": "superearly"
},
{
"fields": {
"desc": "",
@ -16455,7 +16527,7 @@
"fields": {
"command": "xym",
"switch": "--version",
"time": "2023-07-17T07:09:47.664Z",
"time": "2023-08-22T07:09:39.542Z",
"used": true,
"version": "xym 0.7.0"
},
@ -16466,7 +16538,7 @@
"fields": {
"command": "pyang",
"switch": "--version",
"time": "2023-07-17T07:09:48.075Z",
"time": "2023-08-22T07:09:39.881Z",
"used": true,
"version": "pyang 2.5.3"
},
@ -16477,7 +16549,7 @@
"fields": {
"command": "yanglint",
"switch": "--version",
"time": "2023-07-17T07:09:48.104Z",
"time": "2023-08-22T07:09:39.899Z",
"used": true,
"version": "yanglint SO 1.9.2"
},
@ -16488,9 +16560,9 @@
"fields": {
"command": "xml2rfc",
"switch": "--version",
"time": "2023-07-17T07:09:49.075Z",
"time": "2023-08-22T07:09:40.791Z",
"used": true,
"version": "xml2rfc 3.17.4"
"version": "xml2rfc 3.18.0"
},
"model": "utils.versioninfo",
"pk": 4

View file

@ -0,0 +1,59 @@
# Copyright The IETF Trust 2023, All Rights Reserved
from django.db import migrations, models
def forward(apps, schema_editor):
AppealArtifactTypeName = apps.get_model("name", "AppealArtifactTypeName")
for slug, name, desc, order in [
("appeal", "Appeal", "The content of an appeal", 1),
(
"appeal_with_response",
"Response (with appeal included)",
"The content of an appeal combined with the content of a response",
2,
),
("response", "Response", "The content of a response to an appeal", 3),
(
"reply_to_response",
"Reply to response",
"The content of a reply to an appeal response",
4,
),
("other_content", "Other content", "Other content related to an appeal", 5),
]:
AppealArtifactTypeName.objects.create(
slug=slug, name=name, desc=desc, order=order
)
def reverse(apps, schema_editor):
AppealArtifactTypeName = apps.get_model("name", "AppealArtifactTypeName")
AppealArtifactTypeName.objects.delete()
class Migration(migrations.Migration):
dependencies = [
("name", "0006_feedbacktypename_data"),
]
operations = [
migrations.CreateModel(
name="AppealArtifactTypeName",
fields=[
(
"slug",
models.CharField(max_length=32, primary_key=True, serialize=False),
),
("name", models.CharField(max_length=255)),
("desc", models.TextField(blank=True)),
("used", models.BooleanField(default=True)),
("order", models.IntegerField(default=0)),
],
options={
"ordering": ["order", "name"],
"abstract": False,
},
),
migrations.RunPython(forward, reverse),
]

View file

@ -151,3 +151,6 @@ class SlideSubmissionStatusName(NameModel):
"Pending, Accepted, Rejected"
class TelechatAgendaSectionName(NameModel):
"""roll_call, minutes, action_items"""
class AppealArtifactTypeName(NameModel):
pass

View file

@ -18,7 +18,8 @@ from ietf.name.models import ( AgendaFilterTypeName, AgendaTypeName, BallotPosit
ReviewAssignmentStateName, ReviewRequestStateName, ReviewResultName, ReviewTypeName,
RoleName, RoomResourceName, SessionStatusName, StdLevelName, StreamName, TimeSlotTypeName,
TopicAudienceName, ReviewerQueuePolicyName, TimerangeName, ExtResourceTypeName, ExtResourceName,
SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName, TelechatAgendaSectionName )
SlideSubmissionStatusName, ProceedingsMaterialTypeName, SessionPurposeName, TelechatAgendaSectionName,
AppealArtifactTypeName )
class TimeSlotTypeNameResource(ModelResource):
class Meta:
@ -737,3 +738,17 @@ class TelechatAgendaSectionNameResource(ModelResource):
"order": ALL,
}
api.name.register(TelechatAgendaSectionNameResource())
class AppealArtifactTypeNameResource(ModelResource):
class Meta:
cache = SimpleCache()
queryset = AppealArtifactTypeName.objects.all()
serializer = api.Serializer()
filtering = {
"slug": ALL,
"name": ALL,
"desc": ALL,
"used": ALL,
"order": ALL,
}
api.name.register(AppealArtifactTypeNameResource())

View file

@ -0,0 +1,24 @@
{% extends "base.html" %}
{% load ietf_filters %}
{# Copyright The IETF Trust 2023. All Rights Reserved. #}
{% load origin %}
{% block title %}{{artifact.display_title}} - {{artifact.appeal.name}}{% endblock %}
{% block pagehead %}
<meta name="description"
content="{{artifact.date}} = {{artifact.display_title}} - {{artifact.appeal.name}}">
{% endblock %}
{% block content %}
{% origin %}
<h1>
{{artifact.appeal.name}} - {{artifact.appeal.date}}
<br>
<small class="text-body-secondary">{{ artifact.display_title }} - {{ artifact.date }}</small>
</h1>
{{ artifact_html }}
{% if request.user|has_role:"Secretariat" %}
<hr>
<div>
<a class="btn btn-primary btn-sm" download="{{artifact.download_name}}" href="{% url 'ietf.group.views.appeal_artifact_markdown' acronym=artifact.appeal.group.acronym artifact_id=artifact.pk %}">Download markdown source</a>
</div>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,38 @@
{% extends "group/group_base.html" %}
{# Copyright The IETF Trust 2023, All Rights Reserved #}
{% load origin %}
{% load ietf_filters person_filters textfilters %}
{% load static %}
{% block pagehead %}
<link rel="stylesheet" href="{% static "ietf/css/list.css" %}">
{% endblock %}
{% block group_content %}
{% origin %}
<h2 class="my-3">{{group.acronym|upper}} Appeals</h2>
<table id="appeals" class="my-3 table table-sm table-striped tablesorter">
<thead>
<tr>
<th class="col-1" scope="col" data-sort="date">Date</th>
<th scope="col" data-sort="appeal">Appeal</th>
</tr>
</thead>
<tbody>
{% for appeal in appeals %}
<tr id="appeal-{{forloop.counter}}">
<td id="appeal-{{forloop.counter}}-date">{{ appeal.date|date:"Y-m-d" }}</td>
<td>{{appeal.name}}
<div class="buttonlist">
{% for part in appeal.appealartifact_set.all %}
<a id="artifact-{{forloop.parentloop.counter}}-{{forloop.counter}}" class="btn btn-primary btn-sm" href="{% url 'ietf.group.views.appeal_artifact' acronym=group.acronym artifact_id=part.pk %}"{% if not part.is_markdown %} download="{{part.download_name}}"{%endif%}>{{part.display_title}} - {{part.date|date:"Y-m-d"}}</a>
{% endfor %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block js %}
<script src="{% static "ietf/js/list.js" %}"></script>
{% endblock %}