mirror of
https://github.com/Hopiu/wagtail.git
synced 2026-05-17 11:41:11 +00:00
This implements a new "find" view for all endpoints which can be used for finding an individual object based on the URL parameters passed to it. If an object is found, the view will return a ``302`` redirect to detail page of that object. If not, the view will return a ``404`` response. For the pages endpoint, I've added a ``html_path`` parameter to this view, this allows finding a page by its path on the site. For example a GET request to ``/api/v2/pages/find/?html_path=/`` will always generate a 302 response to the detail view of the homepage. This uses Wagtail's internal routing mechanism so routable pages are supported as well. Fixes #4154
241 lines
7.6 KiB
Python
241 lines
7.6 KiB
Python
from urllib.parse import urlparse
|
|
|
|
from django.conf import settings
|
|
|
|
from wagtail.core.models import Page
|
|
from wagtail.core.utils import resolve_model_string
|
|
|
|
|
|
class BadRequestError(Exception):
|
|
pass
|
|
|
|
|
|
def get_base_url(request=None):
|
|
base_url = getattr(settings, 'WAGTAILAPI_BASE_URL', request.site.root_url if request and request.site else None)
|
|
|
|
if base_url:
|
|
# We only want the scheme and netloc
|
|
base_url_parsed = urlparse(base_url)
|
|
|
|
return base_url_parsed.scheme + '://' + base_url_parsed.netloc
|
|
|
|
|
|
def get_full_url(request, path):
|
|
base_url = get_base_url(request) or ''
|
|
return base_url + path
|
|
|
|
|
|
def get_object_detail_url(router, request, model, pk):
|
|
url_path = router.get_object_detail_urlpath(model, pk)
|
|
|
|
if url_path:
|
|
return get_full_url(request, url_path)
|
|
|
|
|
|
def pages_for_site(site):
|
|
pages = Page.objects.public().live()
|
|
pages = pages.descendant_of(site.root_page, inclusive=True)
|
|
return pages
|
|
|
|
|
|
def page_models_from_string(string):
|
|
page_models = []
|
|
|
|
for sub_string in string.split(','):
|
|
page_model = resolve_model_string(sub_string)
|
|
|
|
if not issubclass(page_model, Page):
|
|
raise ValueError("Model is not a page")
|
|
|
|
page_models.append(page_model)
|
|
|
|
return tuple(page_models)
|
|
|
|
|
|
def filter_page_type(queryset, page_models):
|
|
qs = queryset.none()
|
|
|
|
for model in page_models:
|
|
qs |= queryset.type(model)
|
|
|
|
return qs
|
|
|
|
|
|
class FieldsParameterParseError(ValueError):
|
|
pass
|
|
|
|
|
|
def parse_fields_parameter(fields_str):
|
|
"""
|
|
Parses the ?fields= GET parameter. As this parameter is supposed to be used
|
|
by developers, the syntax is quite tight (eg, not allowing any whitespace).
|
|
Having a strict syntax allows us to extend the it at a later date with less
|
|
chance of breaking anyone's code.
|
|
|
|
This function takes a string and returns a list of tuples representing each
|
|
top-level field. Each tuple contains three items:
|
|
- The name of the field (string)
|
|
- Whether the field has been negated (boolean)
|
|
- A list of nested fields if there are any, None otherwise
|
|
|
|
Some examples of how this function works:
|
|
|
|
>>> parse_fields_parameter("foo")
|
|
[
|
|
('foo', False, None),
|
|
]
|
|
|
|
>>> parse_fields_parameter("foo,bar")
|
|
[
|
|
('foo', False, None),
|
|
('bar', False, None),
|
|
]
|
|
|
|
>>> parse_fields_parameter("-foo")
|
|
[
|
|
('foo', True, None),
|
|
]
|
|
|
|
>>> parse_fields_parameter("foo(bar,baz)")
|
|
[
|
|
('foo', False, [
|
|
('bar', False, None),
|
|
('baz', False, None),
|
|
]),
|
|
]
|
|
|
|
It raises a FieldsParameterParseError (subclass of ValueError) if it
|
|
encounters a syntax error
|
|
"""
|
|
|
|
def get_position(current_str):
|
|
return len(fields_str) - len(current_str)
|
|
|
|
def parse_field_identifier(fields_str):
|
|
first_char = True
|
|
negated = False
|
|
ident = ""
|
|
|
|
while fields_str:
|
|
char = fields_str[0]
|
|
|
|
if char in ['(', ')', ',']:
|
|
if not ident:
|
|
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
|
|
|
|
if ident in ['*', '_'] and char == '(':
|
|
# * and _ cannot have nested fields
|
|
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
|
|
|
|
return ident, negated, fields_str
|
|
|
|
elif char == '-':
|
|
if not first_char:
|
|
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
|
|
|
|
negated = True
|
|
|
|
elif char in ['*', '_']:
|
|
if ident and char == '*':
|
|
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
|
|
|
|
ident += char
|
|
|
|
elif char.isalnum() or char == '_':
|
|
if ident == '*':
|
|
# * can only be on its own
|
|
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
|
|
|
|
ident += char
|
|
|
|
elif char.isspace():
|
|
raise FieldsParameterParseError("unexpected whitespace at position %d" % get_position(fields_str))
|
|
else:
|
|
raise FieldsParameterParseError("unexpected char '%s' at position %d" % (char, get_position(fields_str)))
|
|
|
|
first_char = False
|
|
fields_str = fields_str[1:]
|
|
|
|
return ident, negated, fields_str
|
|
|
|
def parse_fields(fields_str, expect_close_bracket=False):
|
|
first_ident = None
|
|
is_first = True
|
|
fields = []
|
|
|
|
while fields_str:
|
|
sub_fields = None
|
|
ident, negated, fields_str = parse_field_identifier(fields_str)
|
|
|
|
# Some checks specific to '*' and '_'
|
|
if ident in ['*', '_']:
|
|
if not is_first:
|
|
raise FieldsParameterParseError("'%s' must be in the first position" % ident)
|
|
|
|
if negated:
|
|
raise FieldsParameterParseError("'%s' cannot be negated" % ident)
|
|
|
|
if fields_str and fields_str[0] == '(':
|
|
if negated:
|
|
# Negated fields cannot contain subfields
|
|
raise FieldsParameterParseError("unexpected char '(' at position %d" % get_position(fields_str))
|
|
|
|
sub_fields, fields_str = parse_fields(fields_str[1:], expect_close_bracket=True)
|
|
|
|
if is_first:
|
|
first_ident = ident
|
|
else:
|
|
# Negated fields can't be used with '_'
|
|
if first_ident == '_' and negated:
|
|
# _,foo is allowed but _,-foo is not
|
|
raise FieldsParameterParseError("negated fields with '_' doesn't make sense")
|
|
|
|
# Additional fields without sub fields can't be used with '*'
|
|
if first_ident == '*' and not negated and not sub_fields:
|
|
# *,foo(bar) and *,-foo are allowed but *,foo is not
|
|
raise FieldsParameterParseError("additional fields with '*' doesn't make sense")
|
|
|
|
fields.append((ident, negated, sub_fields))
|
|
|
|
if fields_str and fields_str[0] == ')':
|
|
if not expect_close_bracket:
|
|
raise FieldsParameterParseError("unexpected char ')' at position %d" % get_position(fields_str))
|
|
|
|
return fields, fields_str[1:]
|
|
|
|
if fields_str and fields_str[0] == ',':
|
|
fields_str = fields_str[1:]
|
|
|
|
# A comma can not exist immediately before another comma or the end of the string
|
|
if not fields_str or fields_str[0] == ',':
|
|
raise FieldsParameterParseError("unexpected char ',' at position %d" % get_position(fields_str))
|
|
|
|
is_first = False
|
|
|
|
if expect_close_bracket:
|
|
# This parser should've exited with a close bracket but instead we
|
|
# hit the end of the input. Raise an error
|
|
raise FieldsParameterParseError("unexpected end of input (did you miss out a close bracket?)")
|
|
|
|
return fields, fields_str
|
|
|
|
fields, _ = parse_fields(fields_str)
|
|
|
|
return fields
|
|
|
|
|
|
def parse_boolean(value):
|
|
"""
|
|
Parses strings into booleans using the following mapping (case-sensitive):
|
|
|
|
'true' => True
|
|
'false' => False
|
|
'1' => True
|
|
'0' => False
|
|
"""
|
|
if value in ['true', '1']:
|
|
return True
|
|
elif value in ['false', '0']:
|
|
return False
|
|
else:
|
|
raise ValueError("expected 'true' or 'false', got '%s'" % value)
|