From fc41447483ec301df33a0881101fb8303cab0bb5 Mon Sep 17 00:00:00 2001 From: Richard Terry Date: Tue, 27 Aug 2024 10:17:06 +0100 Subject: [PATCH] Add support for global defaults resolves #108 --- docs/changelog.rst | 6 ++++++ docs/installation.rst | 13 +++++++++++-- tagulous/models/fields.py | 3 ++- tagulous/models/options.py | 8 ++++++-- tagulous/settings.py | 16 ++++++++++++++++ tests/lib.py | 21 ++++++++++++++++++++- tests/test_options.py | 31 +++++++++++++++++++++++++++++++ 7 files changed, 92 insertions(+), 6 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7d2dc15..cb0ecc9 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,6 +16,10 @@ are available by installing the develop branch from github. 2.1.0, TBC ---------- +Feature: + +* Global defaults can be set with ``settings.TAGULOUS_DEFAULT_TAG_OPTIONS`` (#108) + Bugfix: * Form field ``has_changed`` now correctly detects if a TagField has changed (#185) @@ -38,6 +42,8 @@ Features: Changes: * Drop Django 2.2 support +* Drop Python 3.7 support - from now on Tagulous will only guarantee compatibility with + the latest versions of Python for supported Django versions. * Documentation fixes (#154) * Simplify steps for contributors (#166) * Remove ``AppConfig.default_app_config``, deprecated in Django 3.2 (#169) diff --git a/docs/installation.rst b/docs/installation.rst index 4eac6b9..36a7e3c 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -56,8 +56,17 @@ Settings Most projects won't need these settings, but they allow for finer control of how Tagulous behaves. -.. note:: - Model and form field options are managed separately by :doc:`tag_options`. +``TAGULOUS_DEFAULT_TAG_OPTIONS`` + Global defaults for tag field options. + + This will override the default tag options for every tag field on models and forms. + + See :doc:`tag_options` for the list of tag field options and their defaults, and how + to override them on individual tag models and tag fields. + + Default: + + TAGULOUS_DEFAULT_TAG_OPTIONS = {} ``TAGULOUS_NAME_MAX_LENGTH``, ``TAGULOUS_SLUG_MAX_LENGTH``, ``TAGULOUS_LABEL_MAX_LENGTH`` diff --git a/tagulous/models/fields.py b/tagulous/models/fields.py index c015944..eeacf5c 100644 --- a/tagulous/models/fields.py +++ b/tagulous/models/fields.py @@ -13,6 +13,7 @@ from django.utils.text import capfirst from .. import constants +from .. import settings as tag_settings from .descriptors import SingleTagDescriptor, TagDescriptor from .models import BaseTagModel, TagModel, TagTreeModel from .options import TagOptions @@ -48,7 +49,7 @@ def __init__(self, to=None, to_base=None, **kwargs): # Extract options from kwargs options = {} - for key, default in constants.OPTION_DEFAULTS.items(): + for key, default in tag_settings.DEFAULT_TAG_OPTIONS.items(): # Look in kwargs, then in tag_meta if key in kwargs: options[key] = kwargs.pop(key) diff --git a/tagulous/models/options.py b/tagulous/models/options.py index 4bb2ef6..d2b3281 100644 --- a/tagulous/models/options.py +++ b/tagulous/models/options.py @@ -3,6 +3,7 @@ """ from .. import constants +from .. import settings as tag_settings from ..utils import parse_tags, render_tags @@ -90,7 +91,7 @@ def __getattr__(self, name): return "" if name not in constants.OPTION_DEFAULTS: raise AttributeError(name) - return self.__dict__.get(name, constants.OPTION_DEFAULTS[name]) + return self.__dict__.get(name, tag_settings.DEFAULT_TAG_OPTIONS[name]) def _get_items(self, with_defaults, keys): """ @@ -99,7 +100,10 @@ def _get_items(self, with_defaults, keys): if with_defaults: return dict( [ - (name, self.__dict__.get(name, constants.OPTION_DEFAULTS[name])) + ( + name, + self.__dict__.get(name, tag_settings.DEFAULT_TAG_OPTIONS[name]), + ) for name in keys ] ) diff --git a/tagulous/settings.py b/tagulous/settings.py index 259799f..30dc2ce 100644 --- a/tagulous/settings.py +++ b/tagulous/settings.py @@ -6,6 +6,8 @@ from django.conf import settings +from .constants import OPTION_DEFAULTS + # # Database control settings # @@ -22,6 +24,20 @@ SLUG_ALLOW_UNICODE = getattr(settings, "TAGULOUS_SLUG_ALLOW_UNICODE", False) +# +# Field settings +# + +# Collect dict of default values, and use them to override the internal defaults +# Validate them against the internal defaults, as TagOptions would +DEFAULT_TAG_OPTIONS = { + **OPTION_DEFAULTS, + **getattr(settings, "TAGULOUS_DEFAULT_TAG_OPTIONS", {}), +} +if _unknown := set(DEFAULT_TAG_OPTIONS) - set(OPTION_DEFAULTS): + raise ValueError(f"Unexpected TAGULOUS_DEFAULT_TAG_OPTIONS: {', '.join(_unknown)}") + + # # Autocomplete settings # diff --git a/tests/lib.py b/tests/lib.py index ead4e7c..57515cb 100644 --- a/tests/lib.py +++ b/tests/lib.py @@ -1,14 +1,17 @@ +import importlib import os import re import sys import unittest +from contextlib import contextmanager from io import StringIO import django from django.db import connection, models -from django.test import testcases +from django.test import override_settings, testcases from tagulous import models as tag_models +from tagulous import settings as tag_settings # Detect test environment # This is used when creating files (migrations and fixtures) to ensure that @@ -275,3 +278,19 @@ def tagfield_html(html: str) -> str: f' aria-describedby="{match.group(1)}_helptext">', ) return html + + +@contextmanager +def override_tag_settings(**options): + # Reload settings + context = override_settings(TAGULOUS_DEFAULT_TAG_OPTIONS=options) + context.__enter__() + importlib.reload(tag_settings) + + try: + yield + finally: + context.__exit__(None, None, None) + + # Reload settings + importlib.reload(tag_settings) diff --git a/tests/test_options.py b/tests/test_options.py index a3d3661..149fe1e 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -11,6 +11,8 @@ from tagulous import constants as tag_constants from tagulous import models as tag_models +from .lib import override_tag_settings + class TagOptionsTest(TestCase): """ @@ -33,6 +35,35 @@ def test_defaults(self): ), ) + def test_global_defaults(self): + # Set some overrides + overrides = { + "initial": "override", + "protect_initial": False, + "force_lowercase": True, + "autocomplete_view": "/override/", + } + for key, value in overrides.items(): + self.assertNotEqual(value, tag_constants.OPTION_DEFAULTS[key]) + + expected = {**tag_constants.OPTION_DEFAULTS, **overrides} + + with override_tag_settings(**overrides): + opt = tag_models.TagOptions() + self.assertEqual(opt.items(with_defaults=False), {}) + self.assertEqual(opt.items(), expected) + self.assertEqual(opt.form_items(with_defaults=False), {}) + self.assertEqual( + opt.form_items(), + dict( + [ + (k, v) + for k, v in expected.items() + if k in tag_constants.FORM_OPTIONS + ] + ), + ) + def test_override_defaults(self): opt = tag_models.TagOptions(force_lowercase=True, case_sensitive=True) local_args = {"force_lowercase": True, "case_sensitive": True}