Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
erp5
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
alecs_myu
erp5
Commits
4c273cae
Commit
4c273cae
authored
Apr 20, 2018
by
Tomáš Peterka
Committed by
Tomáš Peterka
Apr 25, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
[hal_json_style] Refactor for clarity and bug fixes
parent
14ebe535
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
192 additions
and
227 deletions
+192
-227
bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
...rtal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
+192
-227
No files found.
bt5/erp5_hal_json_style/SkinTemplateItem/portal_skins/erp5_hal_json_style/ERP5Document_getHateoas.py
View file @
4c273cae
"""Hello. This will be long because this godly script does almost everything.
I
n general, it always returns a JSON res
ponse in HATEOAS format specification.
I
t **always** return a JSON re
ponse in HATEOAS format specification.
:param REQUEST: HttpRequest holding GET and/or POST data
:param response:
:param view: either "view" or absolute URL of an ERP5 Action
:param mode: {str} help to decide what user wants from us "form" | "search" ...
:param relative_url: an URL of `traversed_document` to operate on (it must have an object_view)
:param portal_status_message: {str} message to be displayed on the user
:param portal_status_level: {str|int} severity of the message using ERP5Type.Log levels or their names like 'info', 'warn', 'error'
Only in
mode == 'search'
Parameters for
mode == 'search'
:param query: string-serialized Query
:param select_list: list of strings to select from search result object
:param limit: tuple(start_index, num_records) which is further passed to list_method BUT not every list_method takes it into account
:param form_relative_url: {str} relative URL of a form FIELD issuing the search (listbox/relation field...)
it can be None in case of special listboxes like List of Modules
or relative path like "portal_skins/erp5_ui_test/FooModule_viewFooList/listbox"
:param default_param_json: {str} BASE64 encoded JSON with parameters intended for the list_method
:param .form_id: In case of page_template = "form" it will be similar to form_relative_url with the exception that it contains
only the form name (e.g. FooModule_viewFooList). In case of dialogs it points to the previous form which is
often more important than the dialog form.
Only in
mode == 'form'
:param form:
Parameters for
mode == 'form'
:param form:
{instace} of a form - obviously this call can be only internal (Script-to-Script)
Only in
mode == 'traverse'
Parameters for
mode == 'traverse'
Traverse renders arbitrary View. It can be a Form or a Script.
:param relative_url: string, MANDATORY for obtaining the traversed_document. Calling this script directly on an object should be
forbidden in code (but it is not now).
:param view: {str} mandatory. the view reference as defined on a Portal Type (e.g. "view" or "publish_view")
# Form
When handling form, we can expect field values to be stored in REQUEST.form in two forms
...
...
@@ -56,11 +62,13 @@ if REQUEST is None:
if
response
is
None
:
response
=
REQUEST
.
RESPONSE
def
isFieldType
(
field
,
type_name
):
if
field
.
meta_type
==
'ProxyField'
:
field
=
field
.
getRecursiveTemplateField
()
return
field
.
meta_type
==
type_name
def
toBasicTypes
(
obj
):
"""Ensure that obj contains only basic types."""
if
obj
is
None
:
...
...
@@ -79,7 +87,8 @@ def toBasicTypes(obj):
log
(
'Cannot convert {!s} to basic types {!s}'
.
format
(
type
(
obj
),
obj
),
level
=
100
)
return
obj
def
addHiddenFieldToForm
(
form
,
name
,
value
):
def
renderHiddenField
(
form
,
name
,
value
):
if
form
==
{}:
form
[
'_embedded'
]
=
{}
form
[
'_embedded'
][
'_view'
]
=
{}
...
...
@@ -90,7 +99,7 @@ def addHiddenFieldToForm(form, name, value):
field_dict
=
form
field_dict
[
name
]
=
{
"type"
:
"StringField"
,
"type"
:
"StringField"
,
# must be string field because only this gets send when non-editable
"key"
:
name
,
"default"
:
value
,
"editable"
:
0
,
...
...
@@ -101,6 +110,7 @@ def addHiddenFieldToForm(form, name, value):
"required"
:
1
,
}
# http://stackoverflow.com/a/13105359
def
byteify
(
string
):
if
isinstance
(
string
,
dict
):
...
...
@@ -440,12 +450,12 @@ url_template_dict = {
"form_action"
:
"%(traversed_document_url)s/%(action_id)s"
,
"traverse_generator"
:
"%(root_url)s/%(script_id)s?mode=traverse"
+
\
"&relative_url=%(relative_url)s&view=%(view)s"
,
"traverse_generator_non_view"
:
"%(root_url)s/%(script_id)s?mode=traverse"
+
\
"&relative_url=%(relative_url)s&view=%(view)s&form_id=%(form_id)s"
,
"traverse_generator_with_parameter"
:
"%(root_url)s/%(script_id)s?mode=traverse"
+
\
"traverse_generator_action"
:
"%(root_url)s/%(script_id)s?mode=traverse"
+
\
"&relative_url=%(relative_url)s&view=%(view)s&extra_param_json=%(extra_param_json)s"
,
"traverse_template"
:
"%(root_url)s/%(script_id)s?mode=traverse"
+
\
"{&relative_url,view}"
,
# Search template will call standard "searchValues" on a document described by `root_url`
"search_template"
:
"%(root_url)s/%(script_id)s?mode=search"
+
\
"{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}"
,
"worklist_template"
:
"%(root_url)s/%(script_id)s?mode=worklist"
,
...
...
@@ -455,6 +465,9 @@ url_template_dict = {
"&list_method=%(list_method)s"
\
"&default_param_json=%(default_param_json)s"
\
"{&query,select_list*,limit*,sort_on*,local_roles*,selection_domain*}"
,
# Non-editable searches suppose the search results will be rendered as-is and no template
# fields will get involved. Unfortunately, fields need to be resolved because of formatting
# all the time so we abandoned this no_editable version
"custom_search_template_no_editable"
:
"%(root_url)s/%(script_id)s?mode=search"
+
\
"&relative_url=%(relative_url)s"
\
"&list_method=%(list_method)s"
\
...
...
@@ -480,6 +493,35 @@ def getRealRelativeUrl(document):
return
'/'
.
join
(
portal
.
portal_url
.
getRelativeContentPath
(
document
))
def
parseActionUrl
(
url
):
"""Parse usual ERP5 Action URL into components: ~root, context~, view_id, param_dict, url.
:param url: {str} is expected to be in form https://<site_root>/context/view_id?optional=params
"""
param_dict
=
{}
url_and_params
=
url
.
split
(
site_root
.
absolute_url
())[
-
1
].
split
(
'?'
)
_
,
script
=
url_and_params
[
0
].
strip
(
"/ "
).
rsplit
(
'/'
,
1
)
if
len
(
url_and_params
)
>
1
:
for
param
in
url_and_params
[
1
].
split
(
'&'
):
param_name
,
param_value
=
param
.
split
(
'='
)
if
"+"
in
param_value
:
param_value
=
param_value
.
replace
(
"+"
,
" "
)
if
":"
in
param_name
:
param_name
,
param_type
=
param_name
.
split
(
":"
)
if
param_type
==
"int"
:
param_value
=
int
(
param_value
)
elif
param_type
==
"bool"
:
param_value
=
True
if
param_value
.
lower
()
in
(
"true"
,
"1"
)
else
False
else
:
raise
ValueError
(
"Cannot convert param {}={} to type {}. Feel free to add implemetation at the position of this exception."
.
format
(
param_name
,
param_value
,
param_type
))
param_dict
[
param_name
]
=
param_value
return
{
'view_id'
:
script
,
'params'
:
param_dict
,
'url'
:
url
}
def
getFormRelativeUrl
(
form
):
return
portal
.
portal_catalog
(
portal_type
=
(
"ERP5 Form"
,
"ERP5 Report"
),
...
...
@@ -491,10 +533,15 @@ def getFormRelativeUrl(form):
def
getFieldDefault
(
form
,
field
,
key
,
value
=
None
):
"""Get available value for `field` preferably in python-object from REQUEST or from field's default."""
"""Get available value for `field` preferably in python-object from REQUEST or from field's default.
Previously we used Formulator.Field._get_default which is (for no reason) private.
"""
if
value
is
None
:
value
=
(
REQUEST
.
form
.
get
(
field
.
id
,
REQUEST
.
form
.
get
(
key
,
None
))
or
field
.
get_value
(
'default'
,
request
=
REQUEST
,
REQUEST
=
REQUEST
))
value
=
REQUEST
.
get
(
key
,
MARKER
)
# use marker because default value can be intentionally empty string
if
value
is
MARKER
:
value
=
field
.
get_value
(
'default'
,
request
=
REQUEST
,
REQUEST
=
REQUEST
)
if
field
.
has_value
(
"unicode"
)
and
field
.
get_value
(
"unicode"
)
and
isinstance
(
value
,
unicode
):
value
=
unicode
(
value
,
form
.
get_form_encoding
())
if
getattr
(
value
,
'translate'
,
None
)
is
not
None
:
...
...
@@ -770,7 +817,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# portal_type list can be overriden by selection too
# since it can be intentionally empty we don't override with non-empty field value
portal_type_list
=
selection_params
.
get
(
"portal_type"
,
field
.
get_value
(
'portal_types'
))
# requirement: get only sortable/searchable columns which are already displayed in listbox
# see https://lab.nexedi.com/nexedi/erp5/blob/HEAD/product/ERP5Form/ListBox.py#L1004
# implemented in javascript in the end
...
...
@@ -905,12 +951,13 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
formbox_context
=
traversed_document
if
field
.
get_value
(
'context_method_id'
):
# harness acquisition and call the method right away
formbox_context
=
getattr
(
traversed_document
,
field
.
get_value
(
'context_method_id'
))()
formbox_context
=
getattr
(
traversed_document
,
field
.
get_value
(
'context_method_id'
))(
field
=
field
,
REQUEST
=
REQUEST
)
embedded_document
[
'_debug'
]
=
"Different context"
embeded_form
=
getattr
(
formbox_context
,
field
.
get_value
(
'formbox_target_id'
))
# get embedded form definition
embed
d
ed_form
=
getattr
(
formbox_context
,
field
.
get_value
(
'formbox_target_id'
))
# renderForm mutates `embedded_document` therefor no return/assignment
renderForm
(
formbox_context
,
embeded_form
,
embedded_document
,
key_prefix
=
key
)
renderForm
(
formbox_context
,
embed
d
ed_form
,
embedded_document
,
key_prefix
=
key
)
# fix editability which is hard-coded to 0 in `renderForm` implementation
embedded_document
[
'form_id'
][
'editable'
]
=
field
.
get_value
(
"editable"
)
...
...
@@ -944,7 +991,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
"""
Render a `form` in plain python dict.
This function sets varibles 'here' and 'form_id' resp. 'dialog_id' for forms resp. form dialogs to REQUEST.
This function sets vari
a
bles 'here' and 'form_id' resp. 'dialog_id' for forms resp. form dialogs to REQUEST.
Any other REQUEST mingling are at the responsability of the callee.
:param selection_params: holds parameters to construct ERP5Form.Selection instance
...
...
@@ -957,7 +1004,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
# Following pop/push of form_id resp. dialog_id is here because of FormBox - an embedded form in a form
# Fields of forms use form_id in their TALES expressions and obviously FormBox's form_id is different
# from its parent's form
# from its parent's form
. It is very important that we do not remove form_id in case of a Dialog Form.
if
form
.
pt
==
"form_dialog"
:
previous_request_other
[
'dialog_id'
]
=
REQUEST
.
other
.
pop
(
'dialog_id'
,
None
)
REQUEST
.
set
(
'dialog_id'
,
form
.
id
)
...
...
@@ -967,7 +1014,6 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
field_errors
=
REQUEST
.
get
(
'field_errors'
,
{})
#hardcoded
include_action
=
True
if
form
.
pt
==
'form_dialog'
:
action_to_call
=
"Base_callDialogMethod"
...
...
@@ -989,6 +1035,14 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
"method"
:
form
.
method
,
}
}
if
form
.
pt
==
"form_dialog"
:
# If there is a "form_id" in the REQUEST then it means that last view was actually a form
# and we are most likely in a dialog. We save previous form into `last_form_id` ...
last_form_id
=
extra_param_json
.
pop
(
"form_id"
,
""
)
or
REQUEST
.
get
(
"form_id"
,
""
)
except
AttributeError
:
pass
# Form traversed_document
response_dict
[
'_links'
][
'traversed_document'
]
=
{
"href"
:
default_document_uri_template
%
{
...
...
@@ -1026,25 +1080,25 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
response_dict
[
field
.
id
]
=
renderField
(
traversed_document
,
field
,
form
,
key_prefix
=
key_prefix
,
selection_params
=
selection_params
,
extra_param_json
=
extra_param_json
)
if
field_errors
.
has_key
(
field
.
id
):
response_dict
[
field
.
id
][
"error_text"
]
=
field_errors
[
field
.
id
].
error_text
except
AttributeError
:
except
AttributeError
as
error
:
# Do not crash if field configuration is wrong.
pass
log
(
"Field {} rendering failed because of {!s}"
.
format
(
field
.
id
,
error
),
level
=
800
)
# Form Edit handler uses form_id to recover the submitted form.
# Form Dialog handler uses 'dialog_id' instead and 'form_id'
# - Some dialog actions (e.g. Print) uses form_id to obtain previous view form
if
(
form
.
pt
==
'form_dialog'
):
addHiddenFieldToForm
(
response_dict
,
'dialog_id'
,
form
.
id
)
# Form Edit handler uses form_id to recover the submitted form and to control its
# properties like editability
if
form
.
pt
==
'form_dialog'
:
# overwrite "form_id" field's value because old UI does that by passing
# the form_id in query string and hidden fields
if
REQUEST
.
get
(
'form_id'
,
None
):
addHiddenFieldToForm
(
response_dict
,
"form_id"
,
REQUEST
.
get
(
'form_id'
))
# some dialog actions (Print Module) use previous selection name
if
REQUEST
.
get
(
'selection_name'
,
None
):
addHiddenFieldToForm
(
response_dict
,
"selection_name"
,
REQUEST
.
get
(
'selection_name'
))
renderHiddenField
(
response_dict
,
"form_id"
,
REQUEST
.
get
(
'form_id'
)
or
form
.
id
)
# dialog_id is a mandatory field in any form_dialog
renderHiddenField
(
response_dict
,
'dialog_id'
,
form
.
id
)
# some dialog actions use custom cancel_url
if
REQUEST
.
get
(
'cancel_url'
,
None
):
renderHiddenField
(
response_dict
,
"cancel_url"
,
REQUEST
.
get
(
'cancel_url'
))
else
:
# In form_view we place only form_id in the request form
addHiddenFieldToForm
(
response_dict
,
'form_id'
,
form
.
id
)
renderHiddenField
(
response_dict
,
'form_id'
,
form
.
id
)
if
(
form
.
pt
==
'report_view'
):
# reports are expected to return list of ReportSection which is a wrapper
...
...
@@ -1135,52 +1189,27 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
response_dict
[
'report_section_list'
]
=
report_result_list
# end-if report_section
if
form
.
pt
==
"form_dialog"
:
# Insert hash of current values into the form so scripts can see whether data has
# changed if they provide multi-step process
if
form_data
:
extra_param_json
[
"form_hash"
]
=
form
.
hash_validated_data
(
form_data
)
# extra_param_json is a special field in forms (just like form_id). extra_param_json field holds JSON
# metadata about the form (its hash and dynamic fields)
renderHiddenField
(
response_dict
,
'extra_param_json'
,
json
.
dumps
(
extra_param_json
))
for
key
,
value
in
previous_request_other
.
items
():
if
value
is
not
None
:
REQUEST
.
set
(
key
,
value
)
# XXX form action update, etc
def
renderRawField
(
field
):
meta_type
=
field
.
meta_type
return
{
"meta_type"
:
field
.
meta_type
}
if
meta_type
==
"MethodField"
:
result
=
{
"meta_type"
:
field
.
meta_type
}
else
:
result
=
{
"meta_type"
:
field
.
meta_type
,
"_values"
:
field
.
values
,
# XXX TALES expression is not JSON serializable by default
# "_tales": field.tales
"_overrides"
:
field
.
overrides
}
if
meta_type
==
"ProxyField"
:
result
[
'_delegated_list'
]
=
field
.
delegated_list
# try:
# result['_delegated_list'].pop('list_method')
# except KeyError:
# pass
# XXX ListMethod is not JSON serialized by default
try
:
result
[
'_values'
].
pop
(
'list_method'
)
except
KeyError
:
pass
try
:
result
[
'_overrides'
].
pop
(
'list_method'
)
except
KeyError
:
pass
return
result
def
renderFormDefinition
(
form
,
response_dict
):
"""Form "definition" is configurable in Zope admin: Form -> Order."""
"""Form "definition" is configurable in Zope admin: Form -> Order.
We add some known constants inside Forms such as form_id and into
Dialog Form such as dialog_id.
"""
group_list
=
[]
for
group
in
form
.
Form_getGroupTitleAndId
():
...
...
@@ -1188,7 +1217,7 @@ def renderFormDefinition(form, response_dict):
field_list
=
[]
for
field
in
form
.
get_fields_in_group
(
group
[
'goid'
],
include_disabled
=
1
):
field_list
.
append
((
field
.
id
,
renderRawField
(
field
)
))
field_list
.
append
((
field
.
id
,
{
'meta_type'
:
field
.
meta_type
}
))
group_list
.
append
((
group
[
'gid'
],
field_list
))
...
...
@@ -1294,22 +1323,26 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
)
response
.
setStatus
(
401
)
return
""
elif
mime_type
!=
traversed_document
.
Base_handleAcceptHeader
([
mime_type
]):
response
.
setStatus
(
406
)
return
""
elif
(
mode
==
'root'
)
or
(
mode
==
'traverse'
):
#################################################
# Raw document
#################################################
##
# Render ERP Document with a `view` specified
# `view` contains view's name and we extract view's URL (we suppose form ${object_url}/Form_view)
# which after expansion gives https://<site-root>/context/view_id?optional=params
if
(
REQUEST
is
not
None
)
and
(
REQUEST
.
other
[
'method'
]
!=
"GET"
):
response
.
setStatus
(
405
)
return
""
# Default properties shared by all ERP5 Document and Site
action_dict
=
{}
# result_dict['_relative_url'] = traversed_document.getRelativeUrl()
current_action
=
{}
# current action parameters (context, script, URL params)
action_dict
=
{}
# actions available on current `traversed_document`
last_form_id
=
None
# will point to the previous form so we can obtain previous selection
result_dict
[
'title'
]
=
traversed_document
.
getTitle
()
# Add a link to the portal type if possible
...
...
@@ -1342,65 +1375,44 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
"name"
:
Base_translateString
(
container
.
getTitle
()),
}
# Extract embedded form in the document view
embedded_url
=
None
# Find current action URL and extract embedded view
erp5_action_dict
=
portal
.
Base_filterDuplicateActions
(
portal
.
portal_actions
.
listFilteredActionsFor
(
traversed_document
))
for
erp5_action_key
in
erp5_action_dict
.
keys
():
for
view_action
in
erp5_action_dict
[
erp5_action_key
]:
# Try to embed the form in the result
if
(
view
==
view_action
[
'id'
]):
embedded_url
=
'%s'
%
view_action
[
'url'
]
# `form_id` should be actually called `dialog_id` in case of form dialogs
# so real form_id of a previous view stays untouched.
# Here we save previous form_id to `last_form_id` so it does not get overriden by `dialog_id`
last_form_id
=
REQUEST
.
get
(
'form_id'
,
""
)
if
REQUEST
is
not
None
else
""
form_id
=
""
if
(
embedded_url
is
not
None
):
# XXX Try to fetch the form in the traversed_document of the document
# Of course, this code will completely crash in many cases (page template
# instead of form, unexpected action TALES expression). Happy debugging.
# renderer_form_relative_url = view_action['url'][len(portal.absolute_url()):]
form_id
=
embedded_url
.
split
(
'?'
,
1
)[
0
].
split
(
"/"
)[
-
1
]
# renderer_form = traversed_document.restrictedTraverse(form_id, None)
# XXX Proxy field are not correctly handled in traversed_document of web site
renderer_form
=
getattr
(
traversed_document
,
form_id
)
if
(
renderer_form
is
not
None
):
current_action
=
parseActionUrl
(
'%s'
%
view_action
[
'url'
])
# current action/view being rendered
# If we have current action definition we are able to render embedded view
# which should be a "ERP5 Form" but in reality can be anything
if
current_action
.
get
(
'view_id'
,
''
):
view_instance
=
getattr
(
traversed_document
,
current_action
[
'view_id'
])
if
(
view_instance
is
not
None
):
embedded_dict
=
{
'_links'
:
{
'self'
:
{
'href'
:
embedded_url
'href'
:
current_action
[
'url'
]
}
}
}
# Put all query parameters (?reset:int=1&workflow_action=start_action) in request to mimic usual form display
query_param_dict
=
{}
query_split
=
embedded_url
.
split
(
'?'
,
1
)
if
len
(
query_split
)
==
2
:
for
query_parameter
in
query_split
[
1
].
split
(
"&"
):
query_key
,
query_value
=
query_parameter
.
split
(
'='
)
# often + is used instead of %20 so we replace for space here
query_param_dict
[
query_key
]
=
query_value
.
replace
(
"+"
,
" "
)
# set URL params into REQUEST (just like it was sent by form)
for
query_key
,
query_value
in
query_param_dict
.
items
():
# Request is later used for method's arguments discovery so set URL params into REQUEST (just like it was sent by form)
for
query_key
,
query_value
in
current_action
[
'params'
].
items
():
REQUEST
.
set
(
query_key
,
query_value
)
# Embedded Form can be a Script or even a class method thus we mitigate here
# If our "form" is actually a Script (nothing is sure in ERP5) then execute it here
try
:
if
"Script"
in
renderer_form
.
meta_type
:
if
"Script"
in
view_instance
.
meta_type
:
# we suppose that the script takes only what is given in the URL params
return
renderer_form
(
**
query_param_dict
)
return
view_instance
(
**
current_action
[
'params'
]
)
except
AttributeError
:
# if renderer form does not have attr meta_type then it is not a document
# but most likely bound instance method. Some form_ids do actually point to methods.
returned_value
=
renderer_form
(
**
query_param_dict
)
returned_value
=
view_instance
(
**
current_action
[
'params'
]
)
# returned value is usually REQUEST.RESPONSE.redirect()
log
(
'ERP5Document_getHateoas'
,
'HAL_JSON cannot handle returned value "{!s}" from {}({!s})'
.
format
(
returned_value
,
form_id
,
query_param_dict
),
100
)
returned_value
,
current_action
[
'view_id'
],
current_action
[
'params'
]
),
100
)
status_message
=
Base_translateString
(
'Operation executed'
)
if
isinstance
(
returned_value
,
(
str
,
unicode
))
and
returned_value
.
startswith
(
'http'
):
parsed_url
=
urlparse
(
returned_value
)
...
...
@@ -1437,14 +1449,13 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# select correct URL template based on action_type and form page template
url_template_key
=
"traverse_generator"
if
erp5_action_key
not
in
(
"view"
,
"object_view"
,
"object_jio_view"
):
# previous view's form_id required almost everything but other views
url_template_key
=
"traverse_generator_non_view"
# XXX This line is only optimization for shorter URL and thus is ugly
if
not
(
form_id
or
last_form_id
):
url_template_key
=
"traverse_generator_action"
# but when we do not have the last form id we do not pass is of course
if
not
(
current_action
.
get
(
'view_id'
,
''
)
or
last_form_id
):
url_template_key
=
"traverse_generator"
erp5_action_list
[
-
1
][
'href'
]
=
url_template_dict
[
url_template_key
]
%
{
"root_url"
:
site_root
.
absolute_url
(),
"script_id"
:
script
.
id
,
"script_id"
:
script
.
id
,
# this script (ERP5Document_getHateoas)
"relative_url"
:
traversed_document
.
getRelativeUrl
().
replace
(
"/"
,
"%2F"
),
"view"
:
erp5_action_list
[
-
1
][
'name'
],
"form_id"
:
form_id
if
form_id
and
renderer_form
.
pt
==
"form_view"
else
last_form_id
...
...
@@ -1662,7 +1673,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
elif
sort_order
.
lower
().
startswith
(
"desc"
):
sort_order
=
"DESC"
else
:
# should raise a
n
ValueError instead
# should raise a ValueError instead
log
(
'Wrong sort order "{}" in {}! It must start with "asc" or "desc"'
.
format
(
sort_order
,
form_relative_url
),
level
=
200
)
# error
return
(
sort_col
,
sort_order
)
...
...
@@ -1685,7 +1696,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
#
# for k, v in catalog_kw.items():
# REQUEST.set(k, v)
search_result_iterable
=
callable_list_method
(
**
catalog_kw
)
# Cast to list if only one element is provided
...
...
@@ -1721,9 +1731,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
if
'selection'
not
in
catalog_kw
:
catalog_kw
[
'selection'
]
=
context
.
getPortalObject
().
portal_selections
.
getSelectionFor
(
selection_name
,
REQUEST
)
# field TALES expression evaluated by Base_getRelatedObjectParameter requires that
REQUEST
.
other
[
'form_id'
]
=
listbox_form
.
id
for
select
in
select_list
:
# See Listbox.py getValueList --> getEditableField & getColumnAliasList method
# In short: there are Form Field definitions which names start with
...
...
@@ -1817,7 +1824,9 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# if there is no tales expr (or is empty) we extract the value from search result
default_field_value
=
getAttrFromAnything
(
search_result
,
select
,
property_getter
,
{})
contents_item
[
select
]
=
renderField
(
# If the contents_item has field rendering in it, better is to add an
# extra layer of abstraction to not get conflicts
contents_item
[
select
][
'field_gadget_param'
]
=
renderField
(
traversed_document
,
editable_field_dict
[
select
],
listbox_form
,
...
...
@@ -1830,107 +1839,70 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# a key name to Python Script with variable number of input parameters.
contents_item
[
select
]
=
getAttrFromAnything
(
search_result
,
select
,
property_getter
,
{
'brain'
:
search_result
})
# If the contents_item has field rendering in it, better is to add an
# extra layer of abstraction to not get conflicts
if
isinstance
(
contents_item
[
select
],
dict
):
contents_item
[
select
]
=
{
'field_gadget_param'
:
contents_item
[
select
],
}
# By default, we won't be generating views in the URL
generate_view
=
False
url_parameter_dict
=
{}
url_parameter_dict
=
None
if
select
in
url_column_dict
:
# Check if we get URL parameters using listbox field `url_columns`
url_column_method
=
getattr
(
search_result
,
url_column_dict
[
select
],
None
)
# If there is empty url_column_method, do nothing and continue. This
# will create no URL in these cases
if
url_column_method
is
None
:
continue
url_parameter_dict
=
url_column_method
(
url_dict
=
True
,
brain
=
search_result
,
selection
=
catalog_kw
[
'selection'
],
selection_name
=
catalog_kw
[
'selection_name'
],
column_id
=
select
)
# Since, now we are using url_columns for both XHTML UI and renderJS UI,
# its normal to get string as a result of the `url_column_method`
# function call. In that cases, we will do nothing. Take note, the
# result of `url_column_method` function call which is usable here should
# be dictionary in the format :-
# {
# 'command': <command_name, ex: 'raw', 'push_history'>,
# 'options': {
# 'url': <Absolute URL>,
# 'jio_key': <Relative URL of object>,
# 'view': <id of the view>,
# }
# }
if
isinstance
(
url_parameter_dict
,
str
):
continue
try
:
url_column_method
=
getattr
(
search_result
,
url_column_dict
[
select
])
# Result of `url_column_method` must be a dictionary in the format
# {'command': <command_name, ex: 'raw', 'push_history'>,
# 'options': {'url': <Absolute URL>, 'jio_key': <Relative URL of object>, 'view': <id of the view>}}
url_parameter_dict
=
url_column_method
(
url_dict
=
True
,
brain
=
search_result
,
selection
=
catalog_kw
[
'selection'
],
selection_name
=
catalog_kw
[
'selection_name'
],
column_id
=
select
)
except
AttributeError
as
e
:
if
url_column_dict
[
select
]:
log
(
"Invalid URL method {!s} on column {}"
.
format
(
url_column_dict
[
select
],
select
),
level
=
800
)
elif
getattr
(
search_result
,
'getListItemUrlDict'
,
None
)
is
not
None
:
# Check if we can get URL result from the brain
try
:
url_parameter_dict
=
search_result
.
getListItemUrlDict
(
select
,
result_index
,
catalog_kw
[
'selection_name'
]
)
select
,
result_index
,
catalog_kw
[
'selection_name'
]
)
except
(
ConflictError
,
RuntimeError
):
raise
except
:
log
(
'could not evaluate the url method getListItemUrlDict with %r'
%
search_result
,
level
=
800
)
continue
else
:
# Continue in case there is no url_column_dict or brain to get URL
continue
url_result_dict
=
{
select
:
url_parameter_dict
}
# If contents item don't have `field_gadget_param` in it, then add it
# to default
if
not
isinstance
(
contents_item
[
select
],
dict
):
contents_item
[
select
]
=
{
'default'
:
contents_item
[
select
],
}
contents_item
[
select
].
update
({
'url_value'
:
url_result_dict
[
select
]})
if
contents_item
[
select
][
'url_value'
]:
if
isinstance
(
url_parameter_dict
,
dict
):
# We need to put URL into rendered field so just ensure it is a dict
if
not
isinstance
(
contents_item
[
select
],
dict
):
contents_item
[
select
]
=
{
'default'
:
contents_item
[
select
],
'editable'
:
False
}
# We should be generating view if there is extra params for view in
# view_kw. These parameters are required to create url at hateoas side
# using the URL template as necessary
if
'view_kw'
in
contents_item
[
select
][
'url_value'
]:
generate_view
=
True
if
'view_kw'
not
in
url_parameter_dict
:
contents_item
[
select
][
'url_value'
]
=
url_parameter_dict
else
:
# Get extra parameters either from url_result_dict or from brain
extra_url_param_dict
=
contents_item
[
select
][
'url_value'
][
'view_kw'
].
get
(
'extra_param_json'
,
{})
if
generate_view
:
extra_url_param_dict
=
url_parameter_dict
[
'view_kw'
].
get
(
'extra_param_json'
,
{})
url_template_id
=
'traverse_generator'
if
extra_url_param_dict
:
url_template_id
=
'traverse_generator_with_parameter'
contents_item
[
select
][
'url_value'
][
'options'
][
'view'
]
=
\
url_template_dict
[
url_template_id
]
%
{
"root_url"
:
site_root
.
absolute_url
(),
"script_id"
:
script
.
id
,
"relative_url"
:
contents_item
[
select
][
'url_value'
][
'view_kw'
][
'jio_key'
].
replace
(
"/"
,
"%2F"
),
"view"
:
contents_item
[
select
][
'url_value'
][
'view_kw'
][
'view'
],
"extra_param_json"
:
urlsafe_b64encode
(
json
.
dumps
(
ensureSerializable
(
extra_url_param_dict
)))
url_template_id
=
'traverse_generator_action'
contents_item
[
select
][
'url_value'
]
=
{
'command'
:
url_parameter_dict
[
'command'
],
'options'
:
{
'jio_key'
:
url_parameter_dict
.
get
(
'options'
,
{}).
get
(
'jio_key'
,
url_parameter_dict
[
'view_kw'
][
'jio_key'
]),
'editable'
:
url_parameter_dict
.
get
(
'options'
,
{}).
get
(
'editable'
,
None
),
'view'
:
url_template_dict
[
url_template_id
]
%
{
"root_url"
:
site_root
.
absolute_url
(),
"script_id"
:
script
.
id
,
"relative_url"
:
url_parameter_dict
[
'view_kw'
][
'jio_key'
].
replace
(
"/"
,
"%2F"
),
"view"
:
url_parameter_dict
[
'view_kw'
][
'view'
],
"extra_param_json"
:
urlsafe_b64encode
(
json
.
dumps
(
ensureSerializable
(
extra_url_param_dict
)))
}
}
# Its better to remove the 'view_kw' from the dictionary as it doesn't
# serve any purpose further in the result_dict
if
'view_kw'
in
contents_item
[
select
][
'url_value'
]:
del
contents_item
[
select
][
'url_value'
][
'view_kw'
]
}
# endfor select
REQUEST
.
other
.
pop
(
'cell'
,
None
)
contents_list
.
append
(
contents_item
)
...
...
@@ -1943,30 +1915,23 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
source_field_meta_type
=
source_field
.
getRecursiveTemplateField
().
meta_type
# Lets mingle with editability of fields here
# Original Listbox.py modifies editability during field rendering (method `render`)
# which
we cannot use here
so we overwrite result's own editability
# Original Listbox.py modifies editability during field rendering (method `render`)
,
# which
is done on frontend side,
so we overwrite result's own editability
if
source_field
is
not
None
and
source_field_meta_type
==
"ListBox"
:
# XXX TODO: should take into account "editable_columns" from listbox own selection
editable_column_set
=
set
(
name
for
name
,
_
in
source_field
.
get_value
(
"editable_columns"
))
for
line
in
result_dict
[
'_embedded'
][
'contents'
]:
for
select
in
line
:
# forbid editability only for fields not specified in editable_columns
if
select
in
editable_column_set
:
continue
if
(
isinstance
(
line
[
select
],
dict
)
and
line
[
select
].
get
(
'field_gadget_param'
,
None
)
is
not
None
):
if
line
[
select
][
'field_gadget_param'
].
get
(
'editable'
):
line
[
select
][
'field_gadget_param'
][
'editable'
]
=
False
if
isinstance
(
line
[
select
],
dict
)
and
'field_gadget_param'
in
line
[
select
]:
line
[
select
][
'field_gadget_param'
][
'editable'
]
=
False
if
source_field
is
not
None
and
source_field_meta_type
==
"ListBox"
:
# Trigger count method if exist
# XXX No need to count if no pagination
count_method
=
source_field
.
get_value
(
'count_method'
)
or
None
count_method_name
=
count_method
.
getMethodName
()
if
count_method
is
not
None
else
""
# Only try to get count method results in case method name exists, in all
# other cases, just pass.
if
count_method_name
!=
""
and
count_method_name
!=
list_method
:
count_method
=
source_field
.
get_value
(
'count_method'
)
if
count_method
!=
""
and
count_method
.
getMethodName
()
!=
list_method
:
count_kw
=
dict
(
catalog_kw
)
# Drop not needed parameters
count_kw
.
pop
(
'selection'
,
None
)
...
...
@@ -1974,7 +1939,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
count_kw
.
pop
(
"sort_on"
,
None
)
count_kw
.
pop
(
"limit"
,
None
)
try
:
count_method
=
getattr
(
traversed_document
,
count_method
_name
)
count_method
=
getattr
(
traversed_document
,
count_method
.
getMethodName
()
)
count_method_result
=
count_method
(
REQUEST
=
REQUEST
,
**
count_kw
)
result_dict
[
'_embedded'
][
'count'
]
=
ensureSerializable
(
count_method_result
[
0
][
0
])
except
AttributeError
as
error
:
...
...
@@ -1983,7 +1948,6 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# and just pass. This also ensures we have compatibilty with how old
# UI behave in these cases.
log
(
'Invalid count method %s'
%
error
,
level
=
800
)
pass
contents_stat_list
=
[]
# in case the search was issued by listbox we can provide results of
...
...
@@ -1992,6 +1956,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
stat_method
=
source_field
.
get_value
(
'stat_method'
)
stat_columns
=
source_field
.
get_value
(
'stat_columns'
)
contents_stat
=
{}
if
len
(
stat_columns
)
>
0
:
# prefer stat per column (follow original ListBox.py implementation)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment