Remove deprecated table.UpdateAction class

UpdateAction is deprecated in Newton and is used in unit-tests only

Change-Id: I7952cb77b168cc39f8fc37c3b7ffe0c830d4e0d4
This commit is contained in:
Ivan Kolodyazhny 2018-04-12 09:33:45 +03:00
parent 07237c1fc6
commit f6f39d86da
10 changed files with 9 additions and 622 deletions

View File

@ -289,53 +289,6 @@ Example::
admin=True)
return project_info
Updating changed cell data (DEPRECATED)
---------------------------------------
Define an ``update_cell`` method in the class inherited from
``tables.UpdateAction``. This method takes care of saving the data of the
table cell. There can be one class for every cell thanks to the
``cell_name`` parameter. This class is then defined in tables column as
``update_action=UpdateCell``, so each column can have its own updating
method.
Example::
class UpdateCell(tables.UpdateAction):
def allowed(self, request, project, cell):
# Determines whether given cell or row will be inline editable
# for signed in user.
return api.keystone.keystone_can_edit_project()
def update_cell(self, request, project_id, cell_name, new_cell_value):
# in-line update project info
try:
project_obj = datum
# updating changed value by new value
setattr(project_obj, cell_name, new_cell_value)
# sending new attributes back to API
api.keystone.tenant_update(
request,
project_id,
name=project_obj.name,
description=project_obj.description,
enabled=project_obj.enabled)
except Conflict:
# Validation error for naming conflict, raised when user
# choose the existing name. We will raise a
# ValidationError, that will be sent back to the client
# browser and shown inside of the table cell.
message = _("This name is already taken.")
raise ValidationError(message)
except:
# Other exception of the API just goes through standard
# channel
exceptions.handle(request, ignore=True)
return False
return True
Defining a form_field for each Column that we want to be in-line edited.
------------------------------------------------------------------------

View File

@ -285,7 +285,6 @@ classes:
- :class:`~horizon.tables.BatchAction`
- :class:`~horizon.tables.DeleteAction`
- :class:`~horizon.tables.UpdateAction` **DEPRECATED**
- :class:`~horizon.tables.FixedFilterAction`

View File

@ -1,277 +0,0 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
//TODO(lcastell):Inline edit is deprecated and will be removed in Horizon 12.0
horizon.inline_edit = {
get_cell_id: function (td_element) {
return [
td_element.parents("tr").first().data("object-id"),
"__",
td_element.data("cell-name")
].join('');
},
get_object_container: function () {
// global cell object container
if (!window.cell_object_container) {
window.cell_object_container = [];
}
return window.cell_object_container;
},
get_cell_object: function (td_element) {
var cell_id = horizon.inline_edit.get_cell_id(td_element);
var id = "cell__" + cell_id;
var container = horizon.inline_edit.get_object_container(td_element);
var cell_object;
if (container && container[id]){
// if cell object exists, I will reuse it
cell_object = container[id];
cell_object.reset_with(td_element);
return cell_object;
} else {
// or I will create new cell object
cell_object = new horizon.inline_edit.Cell(td_element);
// saving cell object to global container
container[id] = cell_object;
return cell_object;
}
},
Cell: function (td_element){
var self = this;
// setting initial attributes
self.reset_with = function(td_element){
self.td_element = td_element;
self.form_element = td_element.find("input, textarea");
self.url = td_element.data('update-url');
self.inline_edit_mod = false;
self.successful_update = false;
};
self.reset_with(td_element);
self.refresh = function () {
horizon.ajax.queue({
url: self.url,
data: {'inline_edit_mod': self.inline_edit_mod},
beforeSend: function () {
self.start_loading();
},
complete: function () {
// Bug in jQuery tool-tip, if I hover tool-tip, then confirm the field with
// enter and the cell is reloaded, tool-tip stays. So just to be sure, I am
// removing tool-tips manually
$(".tooltip.fade.top.in").remove();
self.stop_loading();
if (self.successful_update) {
// if cell was updated successfully, I will show fading check sign
var success = $('<div class="success"></div>');
self.td_element.find('.inline-edit-status').append(success);
// edit pencil will disappear and appear again once the check sign has faded
// also green background will disappear
self.td_element.addClass("no-transition");
self.td_element.addClass("success");
self.td_element.removeClass("no-transition");
self.td_element.removeClass("inline_edit_available");
success.fadeOut(1300, function () {
self.td_element.addClass("inline_edit_available");
self.td_element.removeClass("success");
});
}
},
error: function(jqXHR) {
if (jqXHR.status === 401){
var redir_url = jqXHR.getResponseHeader("X-Horizon-Location");
if (redir_url){
location.href = redir_url;
} else {
horizon.toast.add("error", gettext("Not authorized to do this operation."));
}
}
else {
if (!horizon.ajax.get_messages(jqXHR)) {
// Generic error handler. Really generic.
horizon.toast.add("error", gettext("An error occurred. Please try again later."));
}
}
},
success: function (data) {
var $td_element = $(data);
var $tr, $cell_wrapper, td_element_text;
self.form_element = self.get_form_element($td_element);
if (self.inline_edit_mod) {
var cellWidth = self.td_element.outerWidth(true);
$td_element.width(cellWidth);
$td_element.addClass("has-form");
}
// saving old td_element for cancel and loading purposes
self.cached_presentation_view = self.td_element;
// replacing old td with the new td element returned from the server
self.rewrite_cell($td_element);
// keeping parent tr's data-display attr up to date
$tr = $td_element.closest('tr');
if ($td_element.attr('data-cell-name') === $tr.attr('data-display-key')) {
$cell_wrapper= $td_element.find('.table_cell_data_wrapper');
if ($cell_wrapper.length) {
td_element_text = $cell_wrapper.find('a').text();
if ($tr.attr('data-display') !== td_element_text) {
$tr.attr('data-display', td_element_text);
}
}
}
// focusing the form element inside the cell
if (self.inline_edit_mod) {
self.form_element.focus();
}
}
});
};
self.update = function(post_data){
// make the update request
horizon.ajax.queue({
type: 'POST',
url: self.url,
data: post_data,
beforeSend: function () {
self.start_loading();
},
complete: function () {
if (!self.successful_update){
self.stop_loading();
}
},
error: function(jqXHR) {
if (jqXHR.status === 400){
// make place for error icon, only if the error icon is not already present
if (self.td_element.find(".inline-edit-error .error").length <= 0) {
self.form_element.css('padding-left', '20px');
self.form_element.width(self.form_element.width() - 20);
}
// obtain the error message from response body
var error_message = $.parseJSON(jqXHR.responseText).message;
// insert the error icon
var error = $('<div title="' + error_message + '" class="error"></div>');
self.td_element.find(".inline-edit-error").html(error);
error.tooltip({'placement':'top'});
}
else if (jqXHR.status === 401){
var redir_url = jqXHR.getResponseHeader("X-Horizon-Location");
if (redir_url){
location.href = redir_url;
} else {
horizon.toast.add("error", gettext("Not authorized to do this operation."));
}
}
else {
if (!horizon.ajax.get_messages(jqXHR)) {
// Generic error handler. Really generic.
horizon.toast.add("error", gettext("An error occurred. Please try again later."));
}
}
},
success: function () {
// if update was successful
self.successful_update = true;
self.refresh();
}
});
};
self.cancel = function() {
self.rewrite_cell(self.cached_presentation_view);
self.stop_loading();
};
self.get_form_element = function(td_element){
return td_element.find("input, textarea");
};
self.rewrite_cell = function(td_element){
self.td_element.replaceWith(td_element);
self.td_element = td_element;
};
self.start_loading = function() {
self.td_element.addClass("no-transition");
var spinner = $('<div class="loading"></div>');
self.td_element.find('.inline-edit-status').append(spinner);
self.td_element.addClass("loading");
self.td_element.removeClass("inline_edit_available");
self.get_form_element(self.td_element).attr("disabled", "disabled");
};
self.stop_loading = function() {
self.td_element.find('div.inline-edit-status div.loading').remove();
self.td_element.removeClass("loading");
self.td_element.addClass("inline_edit_available");
self.get_form_element(self.td_element).removeAttr("disabled");
};
}
};
horizon.addInitFunction(horizon.inline_edit.init = function(parent) {
parent = parent || document;
var $table = $(parent).find('table');
$table.on('click', '.ajax-inline-edit', function (evt) {
var $this = $(this);
var td_element = $this.parents('td').first();
var cell = horizon.inline_edit.get_cell_object(td_element);
cell.inline_edit_mod = true;
cell.refresh();
evt.preventDefault();
});
var submit_form = function(evt, el){
var $submit = $(el);
var td_element = $submit.parents('td').first();
var post_data = $submit.parents('form').first().serialize();
var cell = horizon.inline_edit.get_cell_object(td_element);
cell.update(post_data);
evt.preventDefault();
};
$table.on('click', '.inline-edit-submit', function (evt) {
submit_form(evt, this);
});
$table.on('keypress', '.inline-edit-form', function (evt) {
if (evt.which === 13 && !evt.shiftKey) {
submit_form(evt, this);
}
});
$table.on('click', '.inline-edit-cancel', function (evt) {
var $cancel = $(this);
var td_element = $cancel.parents('td').first();
var cell = horizon.inline_edit.get_cell_object(td_element);
cell.cancel();
evt.preventDefault();
});
$table.on('mouseenter', '.inline_edit_available', function () {
$(this).find(".table_cell_action").fadeIn(100);
});
$table.on('mouseleave', '.inline_edit_available', function () {
$(this).find(".table_cell_action").fadeOut(200);
});
});

View File

@ -124,5 +124,3 @@ horizon.addInitFunction(horizon.tabs.init = function () {
});
});
//TODO(lcastell):Inline edit is deprecated and will be removed in Horizon 12.0
horizon.tabs.addTabLoadFunction(horizon.inline_edit.init);

View File

@ -22,7 +22,6 @@ from horizon.tables.actions import FilterAction
from horizon.tables.actions import FixedFilterAction
from horizon.tables.actions import LinkAction
from horizon.tables.actions import NameFilterAction
from horizon.tables.actions import UpdateAction
from horizon.tables.base import Column
from horizon.tables.base import DataTable
from horizon.tables.base import Row
@ -42,7 +41,6 @@ __all__ = [
'FixedFilterAction',
'LinkAction',
'NameFilterAction',
'UpdateAction',
'Column',
'DataTable',
'Row',

View File

@ -26,7 +26,6 @@ from django import urls
from django.utils.functional import Promise
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
import six
from horizon import exceptions
@ -964,61 +963,3 @@ class handle_exception_with_detail_message(object):
# just to re-raise the exception.
raise
return decorated
class Deprecated(type):
# TODO(lcastell) Replace class with similar functionality from
# oslo_log.versionutils when it's finally added in 11.0
def __new__(meta, name, bases, kwargs):
cls = super(Deprecated, meta).__new__(meta, name, bases, kwargs)
if name != 'UpdateAction':
LOG.warning(
"WARNING:The UpdateAction class defined in module '%(mod)s' "
"is deprecated as of Newton and may be removed in "
"Horizon P (12.0). Class '%(name)s' defined at "
"module '%(module)s' shall no longer subclass it.",
{'mod': UpdateAction.__module__,
'name': name,
'module': kwargs['__module__']})
return cls
@six.add_metaclass(Deprecated)
class UpdateAction(object):
"""**DEPRECATED**: A table action for cell updates by inline editing."""
name = "update"
def action(self, request, datum, obj_id, cell_name, new_cell_value):
self.update_cell(request, datum, obj_id, cell_name, new_cell_value)
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Update Item",
u"Update Items",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Updated Item",
u"Updated Items",
count
)
def update_cell(self, request, datum, obj_id, cell_name, new_cell_value):
"""Method for saving data of the cell.
This method must implements saving logic of the inline edited table
cell.
"""
def allowed(self, request, datum, cell):
"""Determine whether updating is allowed for the current request.
This method is meant to be overridden with more specific checks.
Data of the row and of the cell are passed to the method.
"""
return True

View File

@ -65,7 +65,6 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.modals.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.tables.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.tables_inline_edit.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.tabs.js'></script>
<script>

View File

@ -253,19 +253,6 @@ class MyServerFilterAction(tables.FilterAction):
return items
class MyUpdateAction(tables.UpdateAction):
def allowed(self, *args):
return True
def update_cell(self, *args):
pass
class MyUpdateActionNotAllowed(MyUpdateAction):
def allowed(self, *args):
return False
def get_name(obj):
return "custom %s" % obj.name
@ -284,8 +271,7 @@ class MyTable(tables.DataTable):
verbose_name="Verbose Name",
sortable=True,
form_field=forms.CharField(required=True),
form_field_attributes={'class': 'test'},
update_action=MyUpdateAction)
form_field_attributes={'class': 'test'})
value = tables.Column('value',
sortable=True,
link='http://example.com/',
@ -345,8 +331,7 @@ class MyTableNotAllowedInlineEdit(MyTable):
verbose_name="Verbose Name",
sortable=True,
form_field=forms.CharField(required=True),
form_field_attributes={'class': 'test'},
update_action=MyUpdateActionNotAllowed)
form_field_attributes={'class': 'test'})
class Meta(object):
name = "my_table"
@ -358,7 +343,6 @@ class MyTableWrapList(MyTable):
name = tables.Column('name',
form_field=forms.CharField(required=True),
form_field_attributes={'class': 'test'},
update_action=MyUpdateActionNotAllowed,
wrap_list=True)
value = tables.Column('value',
@ -443,7 +427,6 @@ class DataTableTests(test.TestCase):
actions.get_final_attrs().get('class', ""))
# In-line edit action on column.
name_column = self.table.columns['name']
self.assertEqual(MyUpdateAction, name_column.update_action)
self.assertEqual(forms.CharField, name_column.form_field.__class__)
self.assertEqual({'class': 'test'}, name_column.form_field_attributes)
@ -728,96 +711,6 @@ class DataTableTests(test.TestCase):
self.assertNotContains(resp_optional, '<ul>')
self.assertNotContains(resp_optional, '</ul>')
def test_inline_edit_available_cell_rendering(self):
self.table = MyTable(self.request, TEST_DATA_2)
row = self.table.get_rows()[0]
name_cell = row.cells['name']
# Check if in-line edit is available in the cell,
# but is not in inline_edit_mod.
self.assertTrue(name_cell.inline_edit_available)
self.assertFalse(name_cell.inline_edit_mod)
# Check if is cell is rendered correctly.
name_cell_rendered = name_cell.render()
resp = http.HttpResponse(name_cell_rendered)
self.assertContains(resp, '<td', 1)
self.assertContains(resp, 'inline_edit_available', 1)
self.assertContains(resp,
'data-update-url="?action=cell_update&amp;'
'table=my_table&amp;cell_name=name&amp;obj_id=1"',
1)
self.assertContains(resp, 'table_cell_wrapper', 1)
self.assertContains(resp, 'table_cell_data_wrapper', 1)
self.assertContains(resp, 'table_cell_action', 1)
self.assertContains(resp, 'ajax-inline-edit', 1)
def test_inline_edit_available_not_allowed_cell_rendering(self):
self.table = MyTableNotAllowedInlineEdit(self.request, TEST_DATA_2)
row = self.table.get_rows()[0]
name_cell = row.cells['name']
# Check if in-line edit is available in the cell,
# but is not in inline_edit_mod.
self.assertTrue(name_cell.inline_edit_available)
self.assertFalse(name_cell.inline_edit_mod)
# Check if is cell is rendered correctly.
name_cell_rendered = name_cell.render()
resp = http.HttpResponse(name_cell_rendered)
self.assertContains(resp, '<td', 1)
self.assertContains(resp, 'inline_edit_available', 1)
self.assertContains(resp,
'data-update-url="?action=cell_update&amp;'
'table=my_table&amp;cell_name=name&amp;obj_id=1"',
1)
self.assertContains(resp, 'table_cell_wrapper', 0)
self.assertContains(resp, 'table_cell_data_wrapper', 0)
self.assertContains(resp, 'table_cell_action', 0)
self.assertContains(resp, 'ajax-inline-edit', 0)
def test_inline_edit_mod_cell_rendering(self):
self.table = MyTable(self.request, TEST_DATA_2)
name_col = self.table.columns['name']
name_col.auto = "form_field"
row = self.table.get_rows()[0]
name_cell = row.cells['name']
name_cell.inline_edit_mod = True
# Check if in-line edit is available in the cell,
# and is in inline_edit_mod, also column auto must be
# set as form_field.
self.assertTrue(name_cell.inline_edit_available)
self.assertTrue(name_cell.inline_edit_mod)
self.assertEqual('form_field',
name_col.auto)
# Check if is cell is rendered correctly.
name_cell_rendered = name_cell.render()
resp = http.HttpResponse(name_cell_rendered)
self.assertContains(resp,
'<input class="test" id="name__1" name="name__1"'
' type="text" value="custom object_1" />',
count=1, html=True)
self.assertContains(resp, '<td', 1)
self.assertContains(resp, 'inline_edit_available', 1)
self.assertContains(resp,
'data-update-url="?action=cell_update&amp;'
'table=my_table&amp;cell_name=name&amp;obj_id=1"',
1)
self.assertContains(resp, 'table_cell_wrapper', 1)
self.assertContains(resp, 'inline-edit-error', 1)
self.assertContains(resp, 'inline-edit-form', 1)
self.assertContains(resp, 'inline-edit-actions', 1)
self.assertContains(resp, 'inline-edit-submit', 1)
self.assertContains(resp, 'inline-edit-cancel', 1)
def test_inline_edit_mod_checkbox_with_label(self):
class TempTable(MyTable):
name = tables.Column(get_name,
@ -826,8 +719,7 @@ class DataTableTests(test.TestCase):
form_field=forms.BooleanField(
required=True,
label="Verbose Name"),
form_field_attributes={'class': 'test'},
update_action=MyUpdateAction)
form_field_attributes={'class': 'test'})
class Meta(object):
name = "my_table"
@ -850,10 +742,6 @@ class DataTableTests(test.TestCase):
'id="name__1" name="name__1" type="checkbox" '
'value="custom object_1" />',
count=1, html=True)
self.assertContains(resp,
'<label class="inline-edit-label" for="name__1">'
'Verbose Name</label>',
count=1, html=True)
def test_table_search_action(self):
class TempTable(MyTable):
@ -900,8 +788,7 @@ class DataTableTests(test.TestCase):
form_field=forms.CharField(
widget=forms.Textarea(),
required=False),
form_field_attributes={'class': 'test'},
update_action=MyUpdateAction)
form_field_attributes={'class': 'test'})
class Meta(object):
name = "my_table"
@ -1137,121 +1024,6 @@ class DataTableTests(test.TestCase):
u'FakeObject: öbject_4'],
transform=six.text_type)
def test_inline_edit_update_action_get_non_ajax(self):
# Non ajax inline edit request should return None.
url = ('/my_url/?action=cell_update'
'&table=my_table&cell_name=name&obj_id=1')
req = self.factory.get(url, {})
self.table = MyTable(req, TEST_DATA_2)
handled = self.table.maybe_preempt()
# Checking the response header.
self.assertIsNone(handled)
def test_inline_edit_update_action_get(self):
# Get request should return td field with data.
url = ('/my_url/?action=cell_update'
'&table=my_table&cell_name=name&obj_id=1')
req = self.factory.get(url, {},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.table = MyTable(req, TEST_DATA_2)
handled = self.table.maybe_preempt()
# Checking the response header.
self.assertEqual(200, handled.status_code)
# Checking the response content.
resp = handled
self.assertContains(resp, '<td', 1)
self.assertContains(resp, 'inline_edit_available', 1)
self.assertContains(
resp,
'data-update-url="/my_url/?action=cell_update&amp;'
'table=my_table&amp;cell_name=name&amp;obj_id=1"',
1)
self.assertContains(resp, 'table_cell_wrapper', 1)
self.assertContains(resp, 'table_cell_data_wrapper', 1)
self.assertContains(resp, 'table_cell_action', 1)
self.assertContains(resp, 'ajax-inline-edit', 1)
def test_inline_edit_update_action_get_not_allowed(self):
# Name column has required validation, sending blank
# will return error.
url = ('/my_url/?action=cell_update'
'&table=my_table&cell_name=name&obj_id=1')
req = self.factory.post(url, {})
self.table = MyTableNotAllowedInlineEdit(req, TEST_DATA_2)
handled = self.table.maybe_preempt()
# Checking the response header.
self.assertEqual(401, handled.status_code)
def test_inline_edit_update_action_get_inline_edit_mod(self):
# Get request in inline_edit_mode should return td with form field.
url = ('/my_url/?inline_edit_mod=true&action=cell_update'
'&table=my_table&cell_name=name&obj_id=1')
req = self.factory.get(url, {},
HTTP_X_REQUESTED_WITH='XMLHttpRequest')
self.table = MyTable(req, TEST_DATA_2)
handled = self.table.maybe_preempt()
# Checking the response header.
self.assertEqual(200, handled.status_code)
# Checking the response content.
resp = handled
self.assertContains(resp,
'<input class="test" id="name__1" name="name__1"'
' type="text" value="custom object_1" />',
count=1, html=True)
self.assertContains(resp, '<td', 1)
self.assertContains(resp, 'inline_edit_available', 1)
self.assertContains(
resp,
'data-update-url="/my_url/?action=cell_update&amp;'
'table=my_table&amp;cell_name=name&amp;obj_id=1"',
1)
self.assertContains(resp, 'table_cell_wrapper', 1)
self.assertContains(resp, 'inline-edit-error', 1)
self.assertContains(resp, 'inline-edit-form', 1)
self.assertContains(resp, 'inline-edit-actions', 1)
self.assertContains(resp, '<button', 2)
self.assertContains(resp, 'inline-edit-submit', 1)
self.assertContains(resp, 'inline-edit-cancel', 1)
def test_inline_edit_update_action_post(self):
# Post request should invoke the cell update table action.
url = ('/my_url/?action=cell_update'
'&table=my_table&cell_name=name&obj_id=1')
req = self.factory.post(url, {'name__1': 'test_name'})
self.table = MyTable(req, TEST_DATA_2)
# checking the response header
handled = self.table.maybe_preempt()
self.assertEqual(200, handled.status_code)
def test_inline_edit_update_action_post_not_allowed(self):
# Post request should invoke the cell update table action.
url = ('/my_url/?action=cell_update'
'&table=my_table&cell_name=name&obj_id=1')
req = self.factory.post(url, {'name__1': 'test_name'})
self.table = MyTableNotAllowedInlineEdit(req, TEST_DATA_2)
# checking the response header
handled = self.table.maybe_preempt()
self.assertEqual(401, handled.status_code)
def test_inline_edit_update_action_post_validation_error(self):
# Name column has required validation, sending blank
# will return error.
url = ('/my_url/?action=cell_update'
'&table=my_table&cell_name=name&obj_id=1')
req = self.factory.post(url, {})
self.table = MyTable(req, TEST_DATA_2)
handled = self.table.maybe_preempt()
# Checking the response header.
self.assertEqual(400, handled.status_code)
self.assertEqual(('Content-Type', 'application/json'),
handled._headers['content-type'])
# Checking the response content.
resp = handled
self.assertContains(resp,
'"message": "This field is required."',
count=1, status_code=400)
def test_column_uniqueness(self):
table1 = MyTable(self.request)
table2 = MyTable(self.request)

View File

@ -35,7 +35,6 @@
<script src='{{ STATIC_URL }}horizon/js/horizon.selenium.js'></script>
{% endif %}
<script src='{{ STATIC_URL }}horizon/js/horizon.tables.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.tables_inline_edit.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.tabs.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.templates.js'></script>
<script src='{{ STATIC_URL }}horizon/js/horizon.users.js'></script>

View File

@ -0,0 +1,5 @@
---
other:
- |
UpdateAction is deprecated in Newton and removed now. You should not use
inline edit functionality in your plugins anymore.