From 604d6edef06f1e1913a3f2eaa557df2aba04f14f Mon Sep 17 00:00:00 2001 From: "Mark J. Donnelly" Date: Thu, 7 Oct 2021 19:30:51 +0000 Subject: [PATCH] Add a new Django field, IETFJSONField This field is needed because the plain JSONField does not permit empty arrays - [] - or empty objects - {} - when the field is marked as required. Those values explicitly evaluate to a null value, and are rejected. Instead, the IETFJSONField accepts two new arguments to control this: - empty_values: An array of values that should evaluate to null/empty, and be rejected. - accepted_empty_values: An array of values that should *not* evaluate to null/empty, and be accepted. This allows the programmer to specify either a positive or negative statement of what values to accept. Fixes issue #3331. Commit ready for merge. - Legacy-Id: 19401 --- ietf/group/models.py | 17 +++++++++-------- ietf/utils/db.py | 28 ++++++++++++++++++++++++++++ ietf/utils/fields.py | 15 +++++++++++++++ 3 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 ietf/utils/db.py diff --git a/ietf/group/models.py b/ietf/group/models.py index aae5c4807d..72c4c86ecf 100644 --- a/ietf/group/models.py +++ b/ietf/group/models.py @@ -23,6 +23,7 @@ from ietf.group.colors import fg_group_colors, bg_group_colors from ietf.name.models import GroupStateName, GroupTypeName, DocTagName, GroupMilestoneStateName, RoleName, AgendaTypeName, ExtResourceName 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 @@ -282,14 +283,14 @@ class GroupFeatures(models.Model): agenda_type = models.ForeignKey(AgendaTypeName, null=True, default="ietf", on_delete=CASCADE) about_page = models.CharField(max_length=64, blank=False, default="ietf.group.views.group_about" ) default_tab = models.CharField(max_length=64, blank=False, default="ietf.group.views.group_about" ) - material_types = jsonfield.JSONField(max_length=64, blank=False, default=["slides"]) - default_used_roles = jsonfield.JSONField(max_length=256, blank=False, default=[]) - admin_roles = jsonfield.JSONField(max_length=64, blank=False, default=["chair"]) # Trac Admin - docman_roles = jsonfield.JSONField(max_length=128, blank=False, default=["ad","chair","delegate","secr"]) - groupman_roles = jsonfield.JSONField(max_length=128, blank=False, default=["ad","chair",]) - groupman_authroles = jsonfield.JSONField(max_length=128, blank=False, default=["Secretariat",]) - matman_roles = jsonfield.JSONField(max_length=128, blank=False, default=["ad","chair","delegate","secr"]) - role_order = jsonfield.JSONField(max_length=128, blank=False, default=["chair","secr","member"], + material_types = IETFJSONField(max_length=64, accepted_empty_values=[[], {}], blank=False, default=["slides"]) + default_used_roles = IETFJSONField(max_length=256, accepted_empty_values=[[], {}], blank=False, default=[]) + admin_roles = IETFJSONField(max_length=64, accepted_empty_values=[[], {}], blank=False, default=["chair"]) # Trac Admin + docman_roles = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], blank=False, default=["ad","chair","delegate","secr"]) + groupman_roles = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], blank=False, default=["ad","chair",]) + groupman_authroles = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], blank=False, default=["Secretariat",]) + matman_roles = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], blank=False, default=["ad","chair","delegate","secr"]) + role_order = IETFJSONField(max_length=128, accepted_empty_values=[[], {}], blank=False, default=["chair","secr","member"], help_text="The order in which roles are shown, for instance on photo pages. Enter valid JSON.") diff --git a/ietf/utils/db.py b/ietf/utils/db.py new file mode 100644 index 0000000000..67f1237b66 --- /dev/null +++ b/ietf/utils/db.py @@ -0,0 +1,28 @@ +# Copyright The IETF Trust 2021, All Rights Reserved +# -*- coding: utf-8 -*- + +# Taken from/inspired by +# https://stackoverflow.com/questions/55147169/django-admin-jsonfield-default-empty-dict-wont-save-in-admin +# +# JSONField should recognize {}, (), and [] as valid, non-empty JSON +# values. However, the base Field class excludes them +import jsonfield + +from ietf.utils.fields import IETFJSONField as FormIETFJSONField + + +class IETFJSONField(jsonfield.JSONField): + form_class = FormIETFJSONField + + def __init__(self, *args, empty_values=FormIETFJSONField.empty_values, accepted_empty_values=None, **kwargs): + if accepted_empty_values is None: + accepted_empty_values = [] + self.empty_values = [x + for x in empty_values + if x not in accepted_empty_values] + super().__init__(*args, **kwargs) + + def formfield(self, **kwargs): + if issubclass(kwargs['form_class'], FormIETFJSONField): + kwargs.setdefault('empty_values', self.empty_values) + return super().formfield(**{**kwargs}) diff --git a/ietf/utils/fields.py b/ietf/utils/fields.py index 647ea0722c..4e470e8152 100644 --- a/ietf/utils/fields.py +++ b/ietf/utils/fields.py @@ -6,6 +6,8 @@ import json import re +import jsonfield + import debug # pyflakes:ignore from typing import Optional, Type # pyflakes:ignore @@ -265,6 +267,19 @@ def clean(self, value): return objs.first() if self.max_entries == 1 else objs + +class IETFJSONField(jsonfield.fields.forms.JSONField): + def __init__(self, *args, empty_values=jsonfield.fields.forms.JSONField.empty_values, + accepted_empty_values=None, **kwargs): + if accepted_empty_values is None: + accepted_empty_values = [] + self.empty_values = [x + for x in empty_values + if x not in accepted_empty_values] + + super().__init__(*args, **kwargs) + + class MissingOkImageField(models.ImageField): """Image field that can validate successfully if file goes missing