Skip to content

Commit

Permalink
Refactoring; utility functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Nigel2392 committed Apr 9, 2024
1 parent f56085e commit 934c051
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 67 deletions.
65 changes: 14 additions & 51 deletions wagtail_fedit/templatetags/fedit.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from django.db import models

from wagtail.blocks import BoundBlock
from wagtail.models import Page
from wagtail import hooks
from urllib.parse import urlencode

Expand All @@ -18,7 +17,12 @@
FeditBlockEditButton,
FeditFieldEditButton,
)
from ..utils import FEDIT_PREVIEW_VAR, get_field_content
from ..utils import (
_can_edit,
edit_url,
get_field_content,
_resolve_expressions,
)
from ..hooks import (
CONSTRUCT_BLOCK_TOOLBAR,
CONSTRUCT_FIELD_TOOLBAR,
Expand Down Expand Up @@ -68,14 +72,8 @@ def render(self, context):
if not field_name and "wagtail_fedit_field_name" in context:
field_name = context["wagtail_fedit_field_name"]

if isinstance(block, FilterExpression):
block = block.resolve(context)
if isinstance(block_id, FilterExpression):
block_id = block_id.resolve(context)
if isinstance(field_name, FilterExpression):
field_name = field_name.resolve(context)
if isinstance(model, FilterExpression):
model = model.resolve(context)
block, block_id, field_name, model =\
_resolve_expressions(block, block_id, field_name, model)

if not block_id and "block_id" not in context and not block:
raise ValueError("Block ID is required")
Expand Down Expand Up @@ -133,19 +131,11 @@ def render(self, context):
if not _can_edit(request, model):
return rendered

# If the model is a page, we can redirect the user to the page editor.
# This will act as a shortcut; jumping to the block inside of the admin.
if isinstance(model, Page):
admin_edit_url = _get_from_context_or_set(
context, "page_base_edit_url",
lambda: reverse(
"wagtailadmin_pages:edit",
args=[model.id],
),
)
admin_edit_url = f"{admin_edit_url}#block-{block_id}-section"
else:
admin_edit_url = None
admin_edit_url = edit_url(
model,
request,
hash=f"block-{block_id}-section",
)

extra["has_block"] = self.has_block

Expand Down Expand Up @@ -301,11 +291,7 @@ def render(self, context):
model = self.model
inline = self.inline

if isinstance(model, FilterExpression):
model = model.resolve(context)

if isinstance(inline, FilterExpression):
inline = inline.resolve(context)
model, inline = _resolve_expressions(model, inline)

obj = model
for i in range(len(getters) - 1):
Expand Down Expand Up @@ -450,26 +436,3 @@ def get_kwargs(parser: Parser, kwarg_list: list[str], tokens: list[str]) -> dict

return kwargs


def _can_edit(request, obj: models.Model):
if not request or not obj:
return False

return not (
not request.user.is_authenticated\
or not request.user.has_perm("wagtailadmin.access_admin")\
or not request.user.has_perm(f"{obj._meta.app_label}.change_{obj._meta.model_name}")\
or not getattr(request, FEDIT_PREVIEW_VAR, False)
)


def _get_from_context_or_set(context, key, value, *args, **kwargs):
if key in context:
return context[key]

if callable(value):
value = value(*args, **kwargs)

context[key] = value
return value

133 changes: 128 additions & 5 deletions wagtail_fedit/utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Any
from collections import namedtuple
from urllib.parse import urlencode
from django.db import models
from django.http import HttpRequest
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.template.base import FilterExpression
from django.conf import settings
from django.urls import reverse

Expand All @@ -12,6 +14,7 @@
DraftStateMixin,
WorkflowMixin,
LockableMixin,
PreviewableMixin
)
from wagtail.admin.admin_url_finder import AdminURLFinder
from wagtail.blocks.stream_block import StreamValue
Expand Down Expand Up @@ -85,24 +88,39 @@ def get_help_text(self) -> str:


def use_related_form(field: models.Field) -> bool:
"""
Check if a field should be included in the related forms.
Internally used to make sure we use widgets instead
of rendering a form for a Page, Image or Document model.
"""
for hook in hooks.get_hooks(EXCLUDE_FROM_RELATED_FORMS):
if hook(field):
return False
return True


def access_userbar_model(request: HttpRequest) -> models.Model:

"""
Retrieve the model set for the userbar in the request.
"""
if not hasattr(request, USERBAR_MODEL_VAR):
return None

return getattr(request, USERBAR_MODEL_VAR)

def with_userbar_model(request: HttpRequest, model: models.Model) -> HttpRequest:
"""
Set the model to be available in the userbar.
The request is shared easily between these contexts - might as well use it.
"""
setattr(request, USERBAR_MODEL_VAR, model)
return request

def get_block_name(block):
"""
Return the block's type name for a block in a StreamField or ListBlock.
eg. "heading", "paragraph", "image", "item", "column", etc.
"""
if isinstance(block, StreamValue.StreamChild):
return block.block_type
elif isinstance(block, ListValue.ListChild):
Expand All @@ -115,6 +133,9 @@ def get_block_name(block):
raise ValueError("Unknown block type: %s" % type(block))

def get_block_path(block):
"""
Get the current path part of a block in a StreamField or ListBlock.
"""
if isinstance(block, StreamValue.StreamChild):
return block.id
elif isinstance(block, ListValue.ListChild):
Expand All @@ -127,6 +148,9 @@ def get_block_path(block):
raise ValueError("Unknown block type: %s" % type(block))

def find_block(block_id, field, contentpath=None):
"""
Find a block in a StreamField or ListBlock by its ID.
"""
if contentpath is None:
contentpath = []

Expand Down Expand Up @@ -181,6 +205,12 @@ def _look_for_renderers():


def get_field_content(request, instance, meta_field: models.Field, context, content=None):
"""
Return the content for a field on a model.
Also checks the model for a rendering method.
The method should be named `render_fedit_{field_name}`.
We wil also check for any hooks which may convert the content.
"""
_look_for_renderers()

if isinstance(meta_field, str):
Expand Down Expand Up @@ -214,19 +244,65 @@ def get_field_content(request, instance, meta_field: models.Field, context, cont
return content

def is_draft_capable(model):
"""
Check if a model is capable of drafts.
"""
return isinstance(model, DraftStateMixin)\
or type(model) == type\
and issubclass(model, DraftStateMixin)

def saving_relation(m1, m2):
"""
Check if two model instances are different.
This is used to determine if a relation is being saved.
"""
return not (
m1._meta.app_label == m2._meta.app_label
and m1._meta.model_name == m2._meta.model_name\
and m1.pk == m2.pk
)


def get_model_string(instance: models.Model, publish_url: bool = False, request: HttpRequest = None, target = "_blank") -> str:
def edit_url(instance: models.Model, request: HttpRequest, hash = None, **params) -> str:
"""
Return the edit URL for a given object and user (or request instead of user.)
If none exists and the model is an instance of PreviewableMixin;
return the wagtail_fedit:editable url; else an empty string.
"""

user = request.user
finder = AdminURLFinder(user)
admin_url = finder.get_edit_url(instance)

if not admin_url:

# Check if the instance is a PreviewableMixin
# and the user has permission to edit it.
if isinstance(instance, PreviewableMixin)\
and _can_edit(request, instance):

admin_url = reverse(
"wagtail_fedit:editable",
args=[
instance.pk,
instance._meta.app_label,
instance._meta.model_name
],
)
else:
return ""

data = urlencode(params)
if params:
admin_url = f"{admin_url}?{data}"

if hash:
admin_url = f"{admin_url}#{hash}"

return admin_url


def get_model_string(instance: models.Model, publish_url: bool = False, request: HttpRequest = None, target = "_blank", **params) -> str:
"""
Get a string representation of a model instance. If the instance has a
`get_admin_display_title` method, it will be used to get the string
Expand Down Expand Up @@ -256,9 +332,12 @@ def get_model_string(instance: models.Model, publish_url: bool = False, request:
],
)

data = urlencode(params)
if params:
admin_url = f"{admin_url}?{data}"

else:
finder = AdminURLFinder(request)
admin_url = finder.get_edit_url(instance)
admin_url = edit_url(instance, request, **params)

if admin_url:
model_string = mark_safe(
Expand All @@ -267,7 +346,26 @@ def get_model_string(instance: models.Model, publish_url: bool = False, request:

return model_string

def _can_edit(request, obj: models.Model):
"""
Check if the user has appropriate permissions to edit an object.
Also requires the current request be on the `wagtail_fedit:editable` url.
"""
if not request or not obj:
return False

return not (
not request.user.is_authenticated\
or not request.user.has_perm("wagtailadmin.access_admin")\
or not request.user.has_perm(f"{obj._meta.app_label}.change_{obj._meta.model_name}")\
or not getattr(request, FEDIT_PREVIEW_VAR, False)
)

def user_can_publish(instance, user, check_for_changes: bool = True):
"""
Check if a user can publish an object.
Mostly comes from PagePermissionTester.can_publish
"""
if not isinstance(instance, DraftStateMixin):
return False

Expand All @@ -284,6 +382,10 @@ def user_can_publish(instance, user, check_for_changes: bool = True):
return instance.permissions_for_user(user).can_publish()

def user_can_unpublish(instance, user):
"""
Check if a user can unpublish an object.
Mostly comes from PagePermissionTester.can_unpublish
"""
if not isinstance(instance, DraftStateMixin):
return False

Expand All @@ -293,6 +395,10 @@ def user_can_unpublish(instance, user):
return instance.permissions_for_user(user).can_unpublish()

def user_can_submit_for_moderation(instance, user, check_for_changes: bool = True):
"""
Check if a user can submit an object for moderation.
Mostly comes from PagePermissionTester.can_submit_for_moderation
"""
if not getattr(settings, "WAGTAIL_WORKFLOW_ENABLED", True):
return False

Expand All @@ -312,6 +418,9 @@ def user_can_submit_for_moderation(instance, user, check_for_changes: bool = Tru
_lock_info = namedtuple("lock_info", ["lock", "locked_for_user"])

def lock_info(object, user) -> _lock_info:
"""
Returns the Lock instance (if any) and whether it is locked for the given user.
"""
if isinstance(object, LockableMixin):
lock = object.get_lock()
locked_for_user = lock is not None and lock.for_user(
Expand All @@ -325,6 +434,20 @@ def lock_info(object, user) -> _lock_info:


def get_hooks(hook_name):
"""
Return the hooks for a given hook name in the wagtail_fedit namespace.
"""
for hook in hooks.get_hooks(f"wagtail_fedit.{hook_name}"):
yield hook



def _resolve_expressions(context, *expressions):
"""
Resolve a list of possible templatetag filterexpressions.
"""
def _map(expression):
if isinstance(expression, FilterExpression):
return expression.resolve(context)
return expression

return tuple(map(_map, expressions))
16 changes: 5 additions & 11 deletions wagtail_fedit/views/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,11 @@ def get_context_data(self, **kwargs):
"form_class": self.form_class,
}

if isinstance(self.instance, Page):
admin_edit_url = reverse(
"wagtailadmin_pages:edit",
args=[self.instance.pk],
)
admin_edit_url = f"{admin_edit_url}#block-{self.block_id}-section"
else:
admin_edit_url = None


must["admin_edit_url"] = admin_edit_url
must["admin_edit_url"] = utils.edit_url(
self.instance,
self.request,
hash=f"block-{self.block_id}-section",
)

context.update(must)
for key, value in can.items():
Expand Down

0 comments on commit 934c051

Please sign in to comment.