Initial commit

This commit is contained in:
2024-08-27 20:33:44 +02:00
commit 1f1832267d
14794 changed files with 1599592 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
from importlib import import_module
from django.apps import apps
from django.utils.module_loading import module_has_submodule
def get_app_modules():
"""
Generator function that yields a module object for each installed app
yields tuples of (app_name, module)
"""
for app in apps.get_app_configs():
yield app.name, app.module
def get_app_submodules(submodule_name):
"""
Searches each app module for the specified submodule
yields tuples of (app_name, module)
"""
for name, module in get_app_modules():
if module_has_submodule(module, submodule_name):
yield name, import_module(f"{name}.{submodule_name}")

View File

@@ -0,0 +1,82 @@
import functools
from django.utils.functional import cached_property
# Need to inherit from object explicitly, to turn ``cached_classmethod`` in to
# a new-style class. WeakKeyDictionary is an old-style class, which do not
# support descriptors.
class cached_classmethod(dict):
"""
Cache the result of a no-arg class method.
.. code-block:: python
class Foo:
@cached_classmethod
def bar(cls):
# Some expensive computation
return 'baz'
Similar to ``@lru_cache``, but the cache is per-class, stores a single
value, and thus doesn't fill up; where as ``@lru_cache`` is global across
all classes, and could fill up if too many classes were used.
"""
def __init__(self, fn):
self.fn = fn
functools.update_wrapper(self, fn)
def __get__(self, instance, owner):
"""Get the class_cache for this type when accessed"""
return self[owner]
def __missing__(self, cls):
"""Make a new class_cache on cache misses"""
value = _cache(self, cls, self.fn)
self[cls] = value
return value
class _cache:
"""Calls the real class method behind when called, caching the result"""
def __init__(self, cache, cls, fn):
self.cache = cache
self.cls = cls
self.fn = fn
functools.update_wrapper(self, fn)
@cached_property
def value(self):
"""Generate the cached value"""
return self.fn(self.cls)
def __call__(self):
"""Get the cached value"""
return self.value
def cache_clear(self):
"""Clear the cached value."""
# Named after lru_cache.cache_clear
self.cache.pop(self.cls, None)
def xframe_options_sameorigin_override(view_func):
"""
Modify a view function so its response has the X-Frame-Options HTTP header
set to 'SAMEORIGIN'.
Adapted from Django's xframe_options_sameorigin so that it's always applied
even if the response already has that header set:
https://github.com/django/django/blob/3.2/django/views/decorators/clickjacking.py#L22-L37
Usage:
@xframe_options_sameorigin_override
def some_view(request):
...
"""
def wrapped_view(*args, **kwargs):
resp = view_func(*args, **kwargs)
resp["X-Frame-Options"] = "SAMEORIGIN"
return resp
return functools.wraps(view_func)(wrapped_view)

View File

@@ -0,0 +1,87 @@
import warnings
from importlib import import_module
class RemovedInWagtail70Warning(DeprecationWarning):
pass
removed_in_next_version_warning = RemovedInWagtail70Warning
class RemovedInWagtail80Warning(PendingDeprecationWarning):
pass
class MovedDefinitionHandler:
"""
A wrapper for module objects to enable definitions to be moved to a new module, with a
deprecation path for the old location. Importing the name from the old location will
raise a deprecation warning (but will still complete successfully).
To use, place the following code in the old module:
import sys
from wagtail.utils.deprecation import MovedDefinitionHandler, RemovedInWagtailXWarning
MOVED_DEFINITIONS = {
'SomeClassOrVariableName': 'path.to.new.module',
}
sys.modules[__name__] = MovedDefinitionHandler(sys.modules[__name__], MOVED_DEFINITIONS, RemovedInWagtailXWarning)
If the name of the definition has also changed, you can specify its new name along with
the path to its new module using a tuple. For example:
MOVED_DEFINITIONS = {
'SomeClassOrVariableName': ('path.to.new.module', 'NewClassOrVariableName'),
}
"""
def __init__(self, real_module, moved_definitions, warning_class):
self.real_module = real_module
self.moved_definitions = moved_definitions
self.warning_class = warning_class
def __getattr__(self, name):
try:
return getattr(self.real_module, name)
except AttributeError as e:
try:
# is the missing name one of our moved definitions?
new_module_name = self.moved_definitions[name]
new_name = name
if isinstance(new_module_name, tuple):
new_module_name, new_name = new_module_name
except KeyError:
# raise the original AttributeError without including the inner try/catch
# in the stack trace
raise e from None
if new_name != name:
warnings.warn(
"%s has been moved from %s to %s and renamed to %s"
% (name, self.real_module.__name__, new_module_name, new_name),
category=self.warning_class,
stacklevel=2,
)
else:
warnings.warn(
"%s has been moved from %s to %s"
% (name, self.real_module.__name__, new_module_name),
category=self.warning_class,
stacklevel=2,
)
# load the requested definition from the module named in moved_definitions
new_module = import_module(new_module_name)
definition = getattr(new_module, new_name)
# stash that definition into the current module so that we don't have to
# redo this import next time we access it
setattr(self.real_module, name, definition)
return definition

View File

@@ -0,0 +1,35 @@
import hashlib
from io import UnsupportedOperation
HASH_READ_SIZE = 2**18 # 256k - matches `hashlib.file_digest`
def hash_filelike(filelike):
"""
Compute the hash of a file-like object, without loading it all into memory.
"""
file_pos = 0
if hasattr(filelike, "tell"):
file_pos = filelike.tell()
try:
# Reset file handler to the start of the file so we hash it all
filelike.seek(0)
except (AttributeError, UnsupportedOperation):
pass
if hasattr(hashlib, "file_digest"):
hasher = hashlib.file_digest(filelike, hashlib.sha1)
else:
hasher = hashlib.sha1()
while True:
data = filelike.read(HASH_READ_SIZE)
if not data:
break
hasher.update(data)
if hasattr(filelike, "seek"):
# Reset the file handler to where it was before
filelike.seek(file_pos)
return hasher.hexdigest()

View File

@@ -0,0 +1,14 @@
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
def get_custom_form(form_setting):
"""Return custom form class if defined and available"""
try:
return import_string(getattr(settings, form_setting))
except ImportError:
raise ImproperlyConfigured(
"%s refers to a form '%s' that is not available"
% (form_setting, getattr(settings, form_setting))
)

View File

@@ -0,0 +1,91 @@
from django.core.exceptions import ImproperlyConfigured
from django.db import models
from wagtail.coreutils import resolve_model_string
class ObjectTypeRegistry:
"""
Implements a lookup table for mapping objects to values according to the object type.
The most specific type according to the object's inheritance chain is selected.
"""
def __init__(self):
# values in this dict will be returned if the field type exactly matches an item here
self.values_by_exact_class = {}
# values in this dict will be returned if any class in the field's inheritance chain
# matches, preferring more specific subclasses
self.values_by_class = {}
def register(self, cls, value=None, exact_class=False):
if exact_class:
self.values_by_exact_class[cls] = value
else:
self.values_by_class[cls] = value
def get_by_type(self, cls):
try:
return self.values_by_exact_class[cls]
except KeyError:
for ancestor in cls.mro():
try:
return self.values_by_class[ancestor]
except KeyError:
pass
def get(self, obj):
value = self.get_by_type(obj.__class__)
if callable(value) and not isinstance(value, type):
value = value(obj)
return value
class ModelFieldRegistry(ObjectTypeRegistry):
"""
Handles the recurring pattern where we need to register different values for different
model field types, and retrieve the one that most closely matches a given model field,
according to its type (taking inheritance into account), and in the case of foreign keys,
the type of the related model (again, taking inheritance into account).
For example, this is used by wagtail.admin.forms.models when constructing model forms:
we use such a registry to retrieve the appropriate dict of arguments to pass to the
form field constructor. A lookup for a models.TextField will return a dict specifying a
text area widget, and a lookup for a foreign key to Image will return a dict specifying
an image chooser widget.
"""
def __init__(self):
super().__init__()
self.values_by_class[models.ForeignKey] = self.foreign_key_lookup
# values in this dict will be returned if the field is a foreign key to a related
# model in here, matching most specific subclass first
self.values_by_fk_related_model = {}
def register(self, field_class, to=None, value=None, exact_class=False):
if to:
if field_class == models.ForeignKey:
self.values_by_fk_related_model[resolve_model_string(to)] = value
else:
raise ImproperlyConfigured(
"The 'to' argument on ModelFieldRegistry.register is only valid for ForeignKey fields"
)
else:
super().register(field_class, value=value, exact_class=exact_class)
def foreign_key_lookup(self, field):
value = None
target_model = field.remote_field.model
for model in target_model.mro():
if model in self.values_by_fk_related_model:
value = self.values_by_fk_related_model[model]
break
if callable(value) and not isinstance(value, type):
value = value(field)
return value

View File

@@ -0,0 +1,106 @@
# Copied from django-sendfile 0.3.6 and tweaked to allow a backend to be passed
# to sendfile()
# See: https://github.com/johnsensible/django-sendfile/pull/33
import os.path
from mimetypes import guess_type
VERSION = (0, 3, 6)
__version__ = ".".join(map(str, VERSION))
def _lazy_load(fn):
_cached = []
def _decorated():
if not _cached:
_cached.append(fn())
return _cached[0]
def clear():
while _cached:
_cached.pop()
_decorated.clear = clear
return _decorated
@_lazy_load
def _get_sendfile():
from importlib import import_module
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
backend = getattr(settings, "SENDFILE_BACKEND", None)
if not backend:
raise ImproperlyConfigured("You must specify a value for SENDFILE_BACKEND")
module = import_module(backend)
return module.sendfile
def sendfile(
request,
filename,
attachment=False,
attachment_filename=None,
mimetype=None,
encoding=None,
backend=None,
):
"""
create a response to send file using backend configured in SENDFILE_BACKEND
If attachment is True the content-disposition header will be set.
This will typically prompt the user to download the file, rather
than view it. The content-disposition filename depends on the
value of attachment_filename:
None (default): Same as filename
False: No content-disposition filename
String: Value used as filename
If no mimetype or encoding are specified, then they will be guessed via the
filename (using the standard python mimetypes module)
"""
_sendfile = backend or _get_sendfile()
if not os.path.exists(filename):
from django.http import Http404
raise Http404('"%s" does not exist' % filename)
guessed_mimetype, guessed_encoding = guess_type(filename)
if mimetype is None:
if guessed_mimetype:
mimetype = guessed_mimetype
else:
mimetype = "application/octet-stream"
response = _sendfile(request, filename, mimetype=mimetype)
if attachment:
parts = ["attachment"]
else:
parts = ["inline"]
if attachment_filename is None:
attachment_filename = os.path.basename(filename)
if attachment_filename:
from django.utils.encoding import force_str
from wagtail.coreutils import string_to_ascii
attachment_filename = force_str(attachment_filename)
ascii_filename = string_to_ascii(attachment_filename)
parts.append('filename="%s"' % ascii_filename)
if ascii_filename != attachment_filename:
from urllib.parse import quote
quoted_filename = quote(attachment_filename)
parts.append("filename*=UTF-8''%s" % quoted_filename)
response["Content-Disposition"] = "; ".join(parts)
response["Content-length"] = os.path.getsize(filename)
response["Content-Type"] = mimetype
response["Content-Encoding"] = encoding or guessed_encoding
return response

View File

@@ -0,0 +1,50 @@
# Sendfile "streaming" backend
# This is based on sendfiles builtin "simple" backend but uses a StreamingHttpResponse
import os
import stat
from email.utils import mktime_tz, parsedate_tz
from django.http import FileResponse, HttpResponseNotModified
from django.utils.http import http_date
def sendfile(request, filename, **kwargs):
# Respect the If-Modified-Since header.
statobj = os.stat(filename)
if not was_modified_since(
request.headers.get("if-modified-since"),
statobj[stat.ST_MTIME],
):
return HttpResponseNotModified()
response = FileResponse(open(filename, "rb"))
response["Last-Modified"] = http_date(statobj[stat.ST_MTIME])
return response
def was_modified_since(header=None, mtime=0):
"""
Was something modified since the user last downloaded it?
header
This is the value of the If-Modified-Since header. If this is None,
I'll just return True.
mtime
This is the modification time of the item we're talking about.
"""
try:
if header is None:
raise ValueError
header_date = parsedate_tz(header)
if header_date is None:
raise ValueError
header_mtime = mktime_tz(header_date)
if mtime > header_mtime:
raise ValueError
except (ValueError, OverflowError):
return True
return False

View File

@@ -0,0 +1,90 @@
import json
import os
import subprocess
from setuptools import Command
from setuptools.command.bdist_egg import bdist_egg
from setuptools.command.sdist import sdist as base_sdist
from wagtail import __semver__
class assets_mixin:
def compile_assets(self):
try:
subprocess.check_call(["npm", "run", "build"])
except (OSError, subprocess.CalledProcessError) as e:
print("Error compiling assets: " + str(e)) # noqa: T201
raise SystemExit(1)
def publish_assets(self):
try:
subprocess.check_call(["npm", "publish", "client"])
except (OSError, subprocess.CalledProcessError) as e:
print("Error publishing front-end assets: " + str(e)) # noqa: T201
raise SystemExit(1)
def bump_client_version(self):
"""
Writes the current Wagtail version number into package.json
"""
path = os.path.join(".", "client", "package.json")
input_file = open(path)
try:
package = json.loads(input_file.read().decode("utf-8"))
except ValueError as e:
print("Unable to read " + path + " " + e) # noqa: T201
raise SystemExit(1)
package["version"] = __semver__
try:
with open(path, "w", encoding="utf-8") as f:
f.write(str(json.dumps(package, indent=2, ensure_ascii=False)))
except OSError as e:
print( # noqa: T201
"Error setting the version for front-end assets: " + str(e)
)
raise SystemExit(1)
class assets(Command, assets_mixin):
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
self.bump_client_version()
self.compile_assets()
self.publish_assets()
class sdist(base_sdist, assets_mixin):
def run(self):
self.compile_assets()
base_sdist.run(self)
class check_bdist_egg(bdist_egg):
# If this file does not exist, warn the user to compile the assets
sentinel_dir = "wagtail/wagtailadmin/static/"
def run(self):
bdist_egg.run(self)
if not os.path.isdir(self.sentinel_dir):
print( # noqa: T201
"\n".join(
[
"************************************************************",
"The front end assets for Wagtail are missing.",
"To generate the assets, please refer to the documentation in",
"docs/contributing/developing.md",
"************************************************************",
]
)
)

View File

@@ -0,0 +1,7 @@
from bs4 import BeautifulSoup
from django.utils.encoding import force_str
def text_from_html(val):
# Return the unescaped text content of an HTML string
return BeautifulSoup(force_str(val), "html.parser").getText().strip()

View File

@@ -0,0 +1,43 @@
import datetime
from django.conf import settings
from django.utils import formats, timezone
from django.utils.dateparse import parse_datetime
def ensure_utc(value):
"""
Similar to how django-modelcluster stores the revision's data and similar to how
django stores dates in the database, this converts the date to UTC if required.
"""
# https://github.com/wagtail/django-modelcluster/blob/8666f16eaf23ca98afc160b0a4729864411c0563/modelcluster/models.py#L21-L28
if settings.USE_TZ:
if timezone.is_naive(value):
default_timezone = timezone.get_default_timezone()
value = timezone.make_aware(value, default_timezone).astimezone(
datetime.timezone.utc
)
else:
# convert to UTC
value = timezone.localtime(value, datetime.timezone.utc)
return value
def parse_datetime_localized(date_string):
"""
Uses Django's parse_datetime(), but ensures to return an aware datetime.
"""
dt = parse_datetime(date_string)
if settings.USE_TZ and timezone.is_naive(dt):
dt = timezone.make_aware(dt, timezone=timezone.get_default_timezone())
return dt
def render_timestamp(timestamp):
"""
Helper function to format a possibly-timezone-aware datetime into the format
used by Django (e.g. in templates).
"""
if timezone.is_aware(timestamp):
timestamp = timezone.localtime(timestamp)
return formats.date_format(timestamp, "DATETIME_FORMAT")

View File

@@ -0,0 +1,17 @@
from functools import update_wrapper
def decorate_urlpatterns(urlpatterns, decorator):
"""Decorate all the views in the passed urlpatterns list with the given decorator"""
for pattern in urlpatterns:
if hasattr(pattern, "url_patterns"):
# this is an included RegexURLResolver; recursively decorate the views
# contained in it
decorate_urlpatterns(pattern.url_patterns, decorator)
if getattr(pattern, "callback", None):
pattern.callback = update_wrapper(
decorator(pattern.callback), pattern.callback
)
return urlpatterns

View File

@@ -0,0 +1,42 @@
from collections.abc import Mapping
def deep_update(source, overrides):
"""Update a nested dictionary or similar mapping.
Modify ``source`` in place.
"""
for key, value in overrides.items():
if isinstance(value, Mapping) and value:
returned = deep_update(source.get(key, {}), value)
source[key] = returned
else:
source[key] = overrides[key]
return source
def flatten_choices(choices):
"""
Convert potentially grouped choices into a flat dict of choices.
flatten_choices([(1, '1st'), (2, '2nd')]) -> {1: '1st', 2: '2nd'}
flatten_choices([('Group', [(1, '1st'), (2, '2nd')])]) -> {1: '1st', 2: '2nd'}
flatten_choices({'Group': {'1': '1st', '2': '2nd'}}) -> {'1': '1st', '2': '2nd'}
"""
ret = {}
to_unpack = choices.items() if isinstance(choices, dict) else choices
for key, value in to_unpack:
if isinstance(value, (list, tuple)):
# grouped choices (category, sub choices)
for sub_key, sub_value in value:
ret[str(sub_key)] = sub_value
elif isinstance(value, (dict)):
# grouped choices using dict (category, sub choices)
for sub_key, sub_value in value.items():
ret[str(sub_key)] = sub_value
else:
# choice (key, display value)
ret[str(key)] = value
return ret

View File

@@ -0,0 +1,54 @@
# This file is heavily inspired by django.utils.version
def get_version(version):
"""Return a PEP 440-compliant version number from VERSION."""
version = get_complete_version(version)
# Now build the two parts of the version number:
# main = X.Y[.Z]
# sub = .devN - for pre-alpha releases
# | {a|b|rc}N - for alpha, beta, and rc releases
main = get_main_version(version)
sub = ""
if version[3] != "final":
mapping = {"alpha": "a", "beta": "b", "rc": "rc", "dev": ".dev"}
sub = mapping[version[3]] + str(version[4])
return main + sub
def get_main_version(version=None, include_patch=True):
"""Return main version (X.Y[.Z]) from VERSION."""
version = get_complete_version(version)
if include_patch:
parts = 2 if version[2] == 0 else 3
else:
parts = 2
return ".".join(str(x) for x in version[:parts])
def get_complete_version(version=None):
"""
Return a tuple of the Wagtail version. If version argument is non-empty,
check for correctness of the tuple provided.
"""
if version is None:
from wagtail import VERSION as version
else:
assert len(version) == 5
assert version[3] in ("dev", "alpha", "beta", "rc", "final")
return version
def get_semver_version(version):
"Returns the semver version (X.Y.Z[-(alpha|beta)]) from VERSION"
main = ".".join(str(x) for x in version[:3])
sub = ""
if version[3] != "final":
sub = "-{}.{}".format(*version[3:])
return main + sub

View File

@@ -0,0 +1,45 @@
from warnings import warn
from django.forms.widgets import Widget
from django.utils.safestring import mark_safe
from wagtail.utils.deprecation import RemovedInWagtail70Warning
class WidgetWithScript(Widget):
warn(
"The usage of `WidgetWithScript` hook is deprecated. Use external scripts instead.",
category=RemovedInWagtail70Warning,
)
def render_html(self, name, value, attrs):
"""Render the HTML (non-JS) portion of the field markup"""
return super().render(name, value, attrs)
def get_value_data(self, value):
# Perform any necessary preprocessing on the value passed to render() before it is passed
# on to render_html / render_js_init. This is a good place to perform database lookups
# that are needed by both render_html and render_js_init. Return value is arbitrary
# (we only care that render_html / render_js_init can accept it), but will typically be
# a dict of data needed for rendering: id, title etc.
return value
def render(self, name, value, attrs=None, renderer=None):
# no point trying to come up with sensible semantics for when 'id' is missing from attrs,
# so let's make sure it fails early in the process
try:
id_ = attrs["id"]
except (KeyError, TypeError):
raise TypeError(
"WidgetWithScript cannot be rendered without an 'id' attribute"
)
value_data = self.get_value_data(value)
widget_html = self.render_html(name, value_data, attrs)
js = self.render_js_init(id_, name, value_data)
out = f"{widget_html}<script>{js}</script>"
return mark_safe(out)
def render_js_init(self, id_, name, value):
return ""