Skip to content

Commit

Permalink
Changes for compatibility with Wagtail>=3.0.3
Browse files Browse the repository at this point in the history
- Refactored `helpers.thirdparty.wagtail` module

- `mentions_wagtail_path`, `mentions_wagtail_re_path` parameter
  `model_class` can now accept a model name if the class is not
  available. e.g. model_class=MyModel, model_class="myapp.MyModel",
  model_class="MyModel" should all work. App name should be included
  where possible.
  • Loading branch information
beatonma committed Nov 30, 2022
1 parent b53dca1 commit 4cc30c3
Show file tree
Hide file tree
Showing 17 changed files with 449 additions and 276 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
build/
dist/
env/
env-*/
*.egg-info/
__pycache__/
.coverage
Expand Down
12 changes: 9 additions & 3 deletions manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ def parse_args() -> Tuple[argparse.Namespace, List[str]]:
parser = argparse.ArgumentParser()

subs = parser.add_subparsers(dest="command")
subs.add_parser("makemigrations")
makemigrations = subs.add_parser("makemigrations")
makemigrations.add_argument("migration_apps", nargs="*")

return parser.parse_known_args()
known_, remaining_ = parser.parse_known_args()

MIGRATION_SETTINGS["INSTALLED_APPS"] += known_.migration_apps
known_.migration_apps = [x.split(".")[-1] for x in known_.migration_apps]

return known_, remaining_


if __name__ == "__main__":
Expand All @@ -43,6 +49,6 @@ def parse_args() -> Tuple[argparse.Namespace, List[str]]:
settings.configure(**MIGRATION_SETTINGS)
django.setup()

args = sys.argv + [args_.command]
args = sys.argv + [args_.command] + args_.migration_apps

execute_from_command_line(args)
6 changes: 6 additions & 0 deletions mentions/helpers/thirdparty/wagtail/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .resolution import get_model_for_url_by_wagtail
from .urls import (
mentions_wagtail_path,
mentions_wagtail_re_path,
mentions_wagtail_route,
)
60 changes: 60 additions & 0 deletions mentions/helpers/thirdparty/wagtail/proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Modules should import wagtail components from here.
The library should work with (and without)wWagtail==3.0.3 and above. The @path
decorator was only introduced in wagtail==4.0.0 so we need to handle it being
unavailable."""

from mentions.exceptions import OptionalDependency

try:
from wagtail.contrib.routable_page.models import RoutablePageMixin
from wagtail.contrib.routable_page.models import path as wagtail_path
from wagtail.contrib.routable_page.models import re_path as wagtail_re_path

except ImportError:

def config_error(function_name: str, min_wagtail_version: str, django_func):
"""When wagtail is not available or"""

def fake_decorator(pattern, name):
def raise_error(*args, **kwargs):
raise OptionalDependency(
f"Tried to use decorator '{function_name}' but "
f"wagtail>={min_wagtail_version} is not installed. ("
f"pattern={pattern}, name={name})"
)

def fake_wrapper(view_func):
if not hasattr(view_func, "_routablepage_routes"):
view_func._routablepage_routes = []

view_func._routablepage_routes.append(
(
django_func(
pattern,
raise_error,
name=name or view_func.__name__,
),
1000,
)
)
return view_func

return fake_wrapper

return fake_decorator

try:
from django.urls import re_path
from wagtail.contrib.routable_page.models import RoutablePageMixin
from wagtail.contrib.routable_page.models import route as wagtail_re_path

except ImportError:
wagtail_re_path = config_error("mentions_wagtail_re_path", "3.0.3", re_path)

class RoutablePageMixin:
pass

from django.urls import path

wagtail_path = config_error("mentions_wagtail_path", "4.0", path)
106 changes: 106 additions & 0 deletions mentions/helpers/thirdparty/wagtail/resolution.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from typing import Callable, Optional, Type

from django.apps import apps
from django.urls import ResolverMatch

from mentions import config, contract, options
from mentions.exceptions import BadUrlConfig, NoModelForUrlPath, OptionalDependency
from mentions.helpers.resolution import get_model_for_url_by_helper
from mentions.helpers.thirdparty.wagtail.proxy import RoutablePageMixin
from mentions.helpers.thirdparty.wagtail.util import get_annotation_from_viewfunc
from mentions.helpers.types import MentionableImpl, ModelClass
from mentions.models.mixins import MentionableMixin

__all__ = [
"get_model_for_url_by_wagtail",
"autopage_page_resolver",
]


def get_model_for_url_by_wagtail(match: ResolverMatch) -> MentionableMixin:
"""Try to resolve a Wagtail Page instance, if Wagtail is installed.
If using RoutablePageMixin you must replace the Wagtail @path/@re_path
decorators with @mentions_wagtail_path/@mentions_wagtail_re_path to add
the metadata required to resolve the correct target Page.
"""

if not config.is_wagtail_installed():
raise OptionalDependency("wagtail")

import wagtail.views

if match.func != wagtail.views.serve:
raise OptionalDependency("wagtail")

from wagtail.models.sites import get_site_for_hostname

site = get_site_for_hostname(options.domain_name(), None)
path = match.args[0]
path_components = [component for component in path.split("/") if component]

page, args, kwargs = site.root_page.localized.specific.route(None, path_components)

if isinstance(page, MentionableMixin):
return page

if not isinstance(page, RoutablePageMixin):
raise NoModelForUrlPath()

view_func, view_args, view_kwargs = args

kwarg_mapping = get_annotation_from_viewfunc(view_func)
view_kwargs.update(kwarg_mapping)

model_name = view_kwargs.get(contract.URLPATTERNS_MODEL_NAME)

try:
model_class: Type[MentionableMixin] = resolve_model(model_name, page)

except LookupError:
raise BadUrlConfig(f"Cannot find model `{model_name}`!")

return get_model_for_url_by_helper(model_class, view_args, view_kwargs)


def autopage_page_resolver(
model_class: ModelClass,
lookup: dict,
view_func: Callable,
) -> Callable:
def wrapped_view_func(self, request, *args, **kwargs):
resolved_model = resolve_model(model_class, view_func)
lookup_kwargs = {**lookup, **kwargs}
page = get_model_for_url_by_helper(resolved_model, args, lookup_kwargs)

return view_func(self, request, page)

return wrapped_view_func


def resolve_app_name(module_name: str) -> Optional[str]:
app_configs = apps.get_app_configs()
for conf in app_configs:
if module_name.startswith(conf.name):
return conf.label

raise LookupError(f"Cannot find app for module {module_name}")


def resolve_model(model_class: ModelClass, context: object) -> Type[MentionableImpl]:
"""Resolve a model type from an identifier.
model_class may be:
- a class already.
- a dotted string name for a class.
- a simple class name. In this case, use context.__module__ to try and
construct a valid dotted name for the class.
"""
if not isinstance(model_class, str):
return model_class

if "." in model_class:
return apps.get_model(model_class)

app_name = resolve_app_name(context.__module__)
return apps.get_model(f"{app_name}.{model_class}")
Loading

0 comments on commit 4cc30c3

Please sign in to comment.