from django.db import models from django.utils.functional import cached_property from django.utils.translation import gettext as _ from wagtail.coreutils import InvokeViaAttributeShortcut from wagtail.models import Site from .registry import register_setting __all__ = [ "BaseGenericSetting", "BaseSiteSetting", "register_setting", ] class AbstractSetting(models.Model): """ The abstract base model for settings. Subclasses must be registered using :func:`~wagtail.contrib.settings.registry.register_setting` """ class Meta: abstract = True # Override to fetch ForeignKey values in the same query when # retrieving settings (e.g. via `for_request()`) select_related = None @classmethod def base_queryset(cls): """ Returns a queryset of objects of this type to use as a base. You can use the `select_related` attribute on your class to specify a list of foreign key field names, which the method will attempt to select additional related-object data for when the query is executed. If your needs are more complex than this, you can override this method on your custom class. """ queryset = cls.objects.all() if cls.select_related is not None: queryset = queryset.select_related(*cls.select_related) return queryset @classmethod def get_cache_attr_name(cls): """ Returns the name of the attribute that should be used to store a reference to the fetched/created object on a request. """ return f"_{cls._meta.app_label}.{cls._meta.model_name}".lower() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Per-instance page URL cache self._page_url_cache = {} @cached_property def page_url(self): # Allows get_page_url() to be invoked using # `obj.page_url.foreign_key_name` syntax return InvokeViaAttributeShortcut(self, "get_page_url") def get_page_url(self, attribute_name, request=None): """ Returns the URL of a page referenced by a foreign key (or other attribute) matching the name ``attribute_name``. If the field value is null, or links to something other than a ``Page`` object, an empty string is returned. The result is also cached per-object to facilitate fast repeat access. Raises an ``AttributeError`` if the object has no such field or attribute. """ if attribute_name in self._page_url_cache: return self._page_url_cache[attribute_name] if not hasattr(self, attribute_name): raise AttributeError( "'{}' object has no attribute '{}'".format( self.__class__.__name__, attribute_name ) ) page = getattr(self, attribute_name) if hasattr(page, "specific"): url = page.specific.get_url(getattr(self, "_request", None)) else: url = "" self._page_url_cache[attribute_name] = url return url def __getstate__(self): # Ignore 'page_url' when pickling state = super().__getstate__() state.pop("page_url", None) return state class BaseSiteSetting(AbstractSetting): site = models.OneToOneField( Site, unique=True, db_index=True, editable=False, on_delete=models.CASCADE, ) class Meta: abstract = True @classmethod def for_request(cls, request): """ Get or create an instance of this model for the request, and cache the result on the request for faster repeat access. """ attr_name = cls.get_cache_attr_name() if hasattr(request, attr_name): return getattr(request, attr_name) site = Site.find_for_request(request) site_settings = cls.for_site(site) # to allow more efficient page url generation site_settings._request = request setattr(request, attr_name, site_settings) return site_settings def __getstate__(self): # Leave out _request from the pickled state state = super().__getstate__() state.pop("_request", None) return state @classmethod def for_site(cls, site): """ Get or create an instance of this setting for the site. """ if site is None: raise cls.DoesNotExist("%s does not exist for site None." % cls) queryset = cls.base_queryset() instance, created = queryset.get_or_create(site=site) return instance def __str__(self): return _("%(site_setting)s for %(site)s") % { "site_setting": self._meta.verbose_name, "site": self.site, } class BaseGenericSetting(AbstractSetting): """ Generic settings are singleton models - only one instance of each model can be created. """ class Meta: abstract = True @classmethod def _get_or_create(cls): """ Internal convenience method to get or create the first instance. We cannot hardcode `pk=1`, for example, as not all database backends use sequential IDs (e.g. Postgres). """ first_obj = cls.base_queryset().first() if first_obj is None: return cls.objects.create() return first_obj @classmethod def load(cls, request_or_site=None): """ Get or create an instance of this model. There is only ever one instance of models inheriting from `AbstractSetting` so we can use `pk=1`. If `request_or_site` is present and is a request object, then we cache the result on the request for faster repeat access. """ # We can only cache on the request, so if there is no request then # we know there's nothing in the cache. if request_or_site is None or isinstance(request_or_site, Site): return cls._get_or_create() # Check if we already have this in the cache and return it if so. attr_name = cls.get_cache_attr_name() if hasattr(request_or_site, attr_name): return getattr(request_or_site, attr_name) obj = cls._get_or_create() # Cache for next time. setattr(request_or_site, attr_name, obj) return obj def __str__(self): return str(self._meta.verbose_name)