feat: move IAB appeals into the datatracker (#6229)
* 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:
parent
79e7145363
commit
852f9d90b9
ietf
doc
group
admin.pyfactories.py
management/commands
migrations
models.pyresources.pytests_appeals.pyurls.pyutils.pyviews.pyname
templates/group
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
205
ietf/group/management/commands/import_iab_appeals.py
Normal file
205
ietf/group/management/commands/import_iab_appeals.py
Normal 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)
|
83
ietf/group/migrations/0002_appeal.py
Normal file
83
ietf/group/migrations/0002_appeal.py
Normal 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"],
|
||||
},
|
||||
),
|
||||
]
|
|
@ -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 ---
|
||||
|
||||
|
|
|
@ -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())
|
||||
|
|
71
ietf/group/tests_appeals.py
Normal file
71
ietf/group/tests_appeals.py
Normal 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())
|
||||
|
|
@ -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),
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
13
ietf/name/factories.py
Normal 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",)
|
|
@ -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
|
||||
|
|
59
ietf/name/migrations/0007_appeal_artifact_typename.py
Normal file
59
ietf/name/migrations/0007_appeal_artifact_typename.py
Normal 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),
|
||||
]
|
|
@ -151,3 +151,6 @@ class SlideSubmissionStatusName(NameModel):
|
|||
"Pending, Accepted, Rejected"
|
||||
class TelechatAgendaSectionName(NameModel):
|
||||
"""roll_call, minutes, action_items"""
|
||||
|
||||
class AppealArtifactTypeName(NameModel):
|
||||
pass
|
||||
|
|
|
@ -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())
|
||||
|
|
24
ietf/templates/group/appeal_artifact.html
Normal file
24
ietf/templates/group/appeal_artifact.html
Normal 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 %}
|
38
ietf/templates/group/appeals.html
Normal file
38
ietf/templates/group/appeals.html
Normal 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 %}
|
Loading…
Reference in a new issue