Merge "Removed inline_edit functionality for tables"

This commit is contained in:
Jenkins 2016-08-19 03:15:34 +00:00 committed by Gerrit Code Review
commit de29bbce85
12 changed files with 41 additions and 316 deletions

View File

@ -90,7 +90,7 @@ Actions
.. autoclass:: DeleteAction .. autoclass:: DeleteAction
:members: :members:
.. autoclass:: UpdateAction .. autoclass:: UpdateAction **DEPRECATED**
:members: :members:
Class-Based Views Class-Based Views

View File

@ -288,8 +288,8 @@ Example::
admin=True) admin=True)
return project_info return project_info
Updating changed cell data Updating changed cell data (DEPRECATED)
-------------------------- ---------------------------------------
Define an ``update_cell`` method in the class inherited from Define an ``update_cell`` method in the class inherited from
``tables.UpdateAction``. This method takes care of saving the data of the ``tables.UpdateAction``. This method takes care of saving the data of the

View File

@ -287,7 +287,7 @@ There are also additional actions which are extensions of the basic Action class
- :class:`~horizon.tables.BatchAction` - :class:`~horizon.tables.BatchAction`
- :class:`~horizon.tables.DeleteAction` - :class:`~horizon.tables.DeleteAction`
- :class:`~horizon.tables.UpdateAction` - :class:`~horizon.tables.UpdateAction` **DEPRECATED**
- :class:`~horizon.tables.FixedFilterAction` - :class:`~horizon.tables.FixedFilterAction`

View File

@ -1,3 +1,4 @@
//TODO(lcastell):Inline edit is deprecated and will be removed in Horizon 12.0
horizon.inline_edit = { horizon.inline_edit = {
get_cell_id: function (td_element) { get_cell_id: function (td_element) {
return [ return [

View File

@ -108,4 +108,5 @@ 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); horizon.tabs.addTabLoadFunction(horizon.inline_edit.init);

View File

@ -961,8 +961,26 @@ class DeleteAction(BatchAction):
""" """
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)
message = ("WARNING:The UpdateAction class defined in module '%s'"
" is deprecated as of Newton and may be removed in "
"Horizon P (12.0). Class '%s' defined at module '%s' "
"shall no longer subclass it.")
if name != 'UpdateAction':
LOG.warning(message % (UpdateAction.__module__,
name,
kwargs['__module__']))
return cls
@six.add_metaclass(Deprecated)
class UpdateAction(object): class UpdateAction(object):
"""A table action for cell updates by inline editing.""" """A table action for cell updates by inline editing."""
name = "update" name = "update"
def action(self, request, datum, obj_id, cell_name, new_cell_value): def action(self, request, datum, obj_id, cell_name, new_cell_value):

View File

@ -16,7 +16,6 @@ from django.template import defaultfilters as filters
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import forms from horizon import forms
from horizon import tables from horizon import tables
@ -95,33 +94,6 @@ class UpdateRow(tables.Row):
wrap=True) wrap=True)
class UpdateCell(tables.UpdateAction):
policy_rules = (("image", "modify_metadef_namespace"),)
def update_cell(self, request, datum, namespace_name,
cell_name, new_cell_value):
# inline update namespace info
try:
namespace_obj = datum
# updating changed value by new value
if cell_name == 'public':
cell_name = 'visibility'
if new_cell_value:
new_cell_value = 'public'
else:
new_cell_value = 'private'
setattr(namespace_obj, cell_name, new_cell_value)
properties = {cell_name: new_cell_value}
glance.metadefs_namespace_update(
request,
namespace_name,
**properties)
except Exception:
exceptions.handle(request, ignore=True)
return False
return True
class AdminNamespacesTable(tables.DataTable): class AdminNamespacesTable(tables.DataTable):
display_name = tables.Column( display_name = tables.Column(
"display_name", "display_name",
@ -143,15 +115,13 @@ class AdminNamespacesTable(tables.DataTable):
verbose_name=_("Public"), verbose_name=_("Public"),
empty_value=False, empty_value=False,
form_field=forms.BooleanField(required=False), form_field=forms.BooleanField(required=False),
filters=(filters.yesno, filters.capfirst), filters=(filters.yesno, filters.capfirst))
update_action=UpdateCell)
protected = tables.Column( protected = tables.Column(
"protected", "protected",
verbose_name=_("Protected"), verbose_name=_("Protected"),
empty_value=False, empty_value=False,
form_field=forms.BooleanField(required=False), form_field=forms.BooleanField(required=False),
filters=(filters.yesno, filters.capfirst), filters=(filters.yesno, filters.capfirst))
update_action=UpdateCell)
def get_object_id(self, datum): def get_object_id(self, datum):
return datum.namespace return datum.namespace

View File

@ -20,7 +20,6 @@ from horizon import forms
from horizon import tables from horizon import tables
from openstack_dashboard.api import cinder from openstack_dashboard.api import cinder
from openstack_dashboard import policy
class CreateVolumeType(tables.LinkAction): class CreateVolumeType(tables.LinkAction):
@ -196,51 +195,14 @@ class UpdateRow(tables.Row):
return volume_type return volume_type
class UpdateCell(tables.UpdateAction):
def allowed(self, request, volume_type, cell):
return policy.check(
("volume_extension", "volume_extension:types_manage"), request)
def update_cell(self, request, data, volume_type_id,
cell_name, new_cell_value):
# inline update volume type name and/or description
try:
vol_type_obj = data
# updating changed value by new value
setattr(vol_type_obj, cell_name, new_cell_value)
name_value = getattr(vol_type_obj, 'name', None)
desc_value = getattr(vol_type_obj, 'description', None)
public_value = getattr(vol_type_obj, 'public', None)
cinder.volume_type_update(
request,
volume_type_id,
name=name_value,
description=desc_value,
is_public=public_value)
except Exception as ex:
if ex.code and ex.code == 409:
error_message = _('New name conflicts with another '
'volume type.')
else:
error_message = _('Unable to update the volume type.')
exceptions.handle(request, error_message)
return False
return True
class VolumeTypesTable(tables.DataTable): class VolumeTypesTable(tables.DataTable):
name = tables.Column("name", verbose_name=_("Name"), name = tables.Column("name", verbose_name=_("Name"),
form_field=forms.CharField( form_field=forms.CharField(max_length=64))
max_length=64),
update_action=UpdateCell)
description = tables.Column(lambda obj: getattr(obj, 'description', None), description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'), verbose_name=_('Description'),
form_field=forms.CharField( form_field=forms.CharField(
widget=forms.Textarea(attrs={'rows': 4}), widget=forms.Textarea(attrs={'rows': 4}),
required=False), required=False))
update_action=UpdateCell)
assoc_qos_spec = tables.Column("associated_qos_spec", assoc_qos_spec = tables.Column("associated_qos_spec",
verbose_name=_("Associated QoS Spec")) verbose_name=_("Associated QoS Spec"))
@ -250,7 +212,6 @@ class VolumeTypesTable(tables.DataTable):
public = tables.Column("is_public", public = tables.Column("is_public",
verbose_name=_("Public"), verbose_name=_("Public"),
filters=(filters.yesno, filters.capfirst), filters=(filters.yesno, filters.capfirst),
update_action=UpdateCell,
form_field=forms.BooleanField( form_field=forms.BooleanField(
label=_('Public'), required=False)) label=_('Public'), required=False))

View File

@ -10,17 +10,14 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.core.exceptions import ValidationError # noqa
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.template import defaultfilters as filters from django.template import defaultfilters as filters
from django.utils.http import urlencode from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy from django.utils.translation import ungettext_lazy
from horizon import exceptions
from horizon import forms from horizon import forms
from horizon import tables from horizon import tables
from keystoneclient.exceptions import Conflict # noqa
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard import policy from openstack_dashboard import policy
@ -223,58 +220,21 @@ class UpdateRow(tables.Row):
return project_info return project_info
class UpdateCell(tables.UpdateAction):
def allowed(self, request, project, cell):
policy_rule = (("identity", "identity:update_project"),)
return (
(cell.column.name != 'enabled' or
request.user.project_id != cell.datum.id) and
api.keystone.keystone_can_edit_project() and
policy.check(policy_rule, request))
def update_cell(self, request, datum, project_id,
cell_name, new_cell_value):
# inline update project info
try:
project_obj = datum
# updating changed value by new value
setattr(project_obj, cell_name, new_cell_value)
api.keystone.tenant_update(
request,
project_id,
name=project_obj.name,
description=project_obj.description,
enabled=project_obj.enabled)
except Conflict:
# Returning a nice error message about name conflict. The message
# from exception is not that clear for the users.
message = _("This name is already taken.")
raise ValidationError(message)
except Exception:
exceptions.handle(request, ignore=True)
return False
return True
class TenantsTable(tables.DataTable): class TenantsTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Name'), name = tables.Column('name', verbose_name=_('Name'),
link=("horizon:identity:projects:detail"), link=("horizon:identity:projects:detail"),
form_field=forms.CharField(max_length=64), form_field=forms.CharField(max_length=64))
update_action=UpdateCell)
description = tables.Column(lambda obj: getattr(obj, 'description', None), description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'), verbose_name=_('Description'),
form_field=forms.CharField( form_field=forms.CharField(
widget=forms.Textarea(attrs={'rows': 4}), widget=forms.Textarea(attrs={'rows': 4}),
required=False), required=False))
update_action=UpdateCell)
id = tables.Column('id', verbose_name=_('Project ID')) id = tables.Column('id', verbose_name=_('Project ID'))
enabled = tables.Column('enabled', verbose_name=_('Enabled'), status=True, enabled = tables.Column('enabled', verbose_name=_('Enabled'), status=True,
filters=(filters.yesno, filters.capfirst), filters=(filters.yesno, filters.capfirst),
form_field=forms.BooleanField( form_field=forms.BooleanField(
label=_('Enabled'), label=_('Enabled'),
required=False), required=False))
update_action=UpdateCell)
if api.keystone.VERSIONS.active >= 3: if api.keystone.VERSIONS.active >= 3:
domain_name = tables.Column( domain_name = tables.Column(
@ -284,8 +244,7 @@ class TenantsTable(tables.DataTable):
filters=(filters.yesno, filters.capfirst), filters=(filters.yesno, filters.capfirst),
form_field=forms.BooleanField( form_field=forms.BooleanField(
label=_('Enabled'), label=_('Enabled'),
required=False), required=False))
update_action=UpdateCell)
def get_project_detail_link(self, project): def get_project_detail_link(self, project):
# this method is an ugly monkey patch, needed because # this method is an ugly monkey patch, needed because

View File

@ -12,7 +12,6 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import copy
import datetime import datetime
import logging import logging
import os import os
@ -36,13 +35,6 @@ from openstack_dashboard.test import helpers as test
from openstack_dashboard import usage from openstack_dashboard import usage
from openstack_dashboard.usage import quotas from openstack_dashboard.usage import quotas
with_sel = os.environ.get('WITH_SELENIUM', False)
if with_sel:
from selenium.webdriver import ActionChains # noqa
from selenium.webdriver.common import keys
from socket import timeout as socket_timeout # noqa
INDEX_URL = reverse('horizon:identity:projects:index') INDEX_URL = reverse('horizon:identity:projects:index')
USER_ROLE_PREFIX = workflows.PROJECT_USER_MEMBER_SLUG + "_role_" USER_ROLE_PREFIX = workflows.PROJECT_USER_MEMBER_SLUG + "_role_"
@ -1646,143 +1638,6 @@ class DetailProjectViewTests(test.BaseAdminViewTests):
@unittest.skipUnless(os.environ.get('WITH_SELENIUM', False), @unittest.skipUnless(os.environ.get('WITH_SELENIUM', False),
"The WITH_SELENIUM env variable is not set.") "The WITH_SELENIUM env variable is not set.")
class SeleniumTests(test.SeleniumAdminTestCase): class SeleniumTests(test.SeleniumAdminTestCase):
@test.create_stubs(
{api.keystone: ('tenant_list', 'tenant_get', 'tenant_update',
'domain_lookup')})
def test_inline_editing_update(self):
# Tenant List
api.keystone.tenant_list(IgnoreArg(),
domain=None,
marker=None,
paginate=True) \
.AndReturn([self.tenants.list(), False])
api.keystone.domain_lookup(IgnoreArg()).AndReturn({None: None})
# Edit mod
api.keystone.tenant_get(IgnoreArg(),
u'1',
admin=True) \
.AndReturn(self.tenants.list()[0])
# Update - requires get and update
api.keystone.tenant_get(IgnoreArg(),
u'1',
admin=True) \
.AndReturn(self.tenants.list()[0])
api.keystone.tenant_update(
IgnoreArg(),
u'1',
description='a test tenant.',
enabled=True,
name=u'Changed test_tenant')
# Refreshing cell with changed name
changed_tenant = copy.copy(self.tenants.list()[0])
changed_tenant.name = u'Changed test_tenant'
api.keystone.tenant_get(IgnoreArg(),
u'1',
admin=True) \
.AndReturn(changed_tenant)
self.mox.ReplayAll()
self.selenium.get("%s%s" % (self.live_server_url, INDEX_URL))
# Check the presence of the important elements
td_element = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']")
cell_wrapper = td_element.find_element_by_class_name(
'table_cell_wrapper')
edit_button_wrapper = td_element.find_element_by_class_name(
'table_cell_action')
edit_button = edit_button_wrapper.find_element_by_tag_name('button')
# Hovering over td and clicking on edit button
action_chains = ActionChains(self.selenium)
action_chains.move_to_element(cell_wrapper).click(edit_button)
action_chains.perform()
# Waiting for the AJAX response for switching to editing mod
wait = self.ui.WebDriverWait(self.selenium, 10,
ignored_exceptions=[socket_timeout])
wait.until(lambda x: self.selenium.find_element_by_name("name__1"))
# Changing project name in cell form
td_element = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']")
name_input = td_element.find_element_by_tag_name('input')
name_input.send_keys(keys.Keys.HOME)
name_input.send_keys("Changed ")
# Saving new project name by AJAX
td_element.find_element_by_class_name('inline-edit-submit').click()
# Waiting for the AJAX response of cell refresh
wait = self.ui.WebDriverWait(self.selenium, 10,
ignored_exceptions=[socket_timeout])
wait.until(lambda x: self.selenium.find_element_by_xpath(
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']"
"/div[@class='table_cell_wrapper']"
"/div[@class='table_cell_data_wrapper']"))
# Checking new project name after cell refresh
data_wrapper = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']"
"/div[@class='table_cell_wrapper']"
"/div[@class='table_cell_data_wrapper']")
self.assertTrue(data_wrapper.text == u'Changed test_tenant',
"Error: saved tenant name is expected to be "
"'Changed test_tenant'")
@test.create_stubs(
{api.keystone: ('tenant_list', 'tenant_get', 'domain_lookup')})
def test_inline_editing_cancel(self):
# Tenant List
api.keystone.tenant_list(IgnoreArg(),
domain=None,
marker=None,
paginate=True) \
.AndReturn([self.tenants.list(), False])
api.keystone.domain_lookup(IgnoreArg()).AndReturn({None: None})
# Edit mod
api.keystone.tenant_get(IgnoreArg(),
u'1',
admin=True) \
.AndReturn(self.tenants.list()[0])
# Cancel edit mod is without the request
self.mox.ReplayAll()
self.selenium.get("%s%s" % (self.live_server_url, INDEX_URL))
# Check the presence of the important elements
td_element = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']")
cell_wrapper = td_element.find_element_by_class_name(
'table_cell_wrapper')
edit_button_wrapper = td_element.find_element_by_class_name(
'table_cell_action')
edit_button = edit_button_wrapper.find_element_by_tag_name('button')
# Hovering over td and clicking on edit
action_chains = ActionChains(self.selenium)
action_chains.move_to_element(cell_wrapper).click(edit_button)
action_chains.perform()
# Waiting for the AJAX response for switching to editing mod
wait = self.ui.WebDriverWait(self.selenium, 10,
ignored_exceptions=[socket_timeout])
wait.until(lambda x: self.selenium.find_element_by_name("name__1"))
# Click on cancel button
td_element = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']")
td_element.find_element_by_class_name('inline-edit-cancel').click()
# Cancel is via javascript, so it should be immediate
# Checking that tenant name is not changed
data_wrapper = self.selenium.find_element_by_xpath(
"//td[@data-update-url='/identity/?action=cell_update"
"&table=tenants&cell_name=name&obj_id=1']"
"/div[@class='table_cell_wrapper']"
"/div[@class='table_cell_data_wrapper']")
self.assertTrue(data_wrapper.text == u'test_tenant',
"Error: saved tenant name is expected to be "
"'test_tenant'")
@test.create_stubs({api.keystone: ('get_default_domain', @test.create_stubs({api.keystone: ('get_default_domain',
'get_default_role', 'get_default_role',
'user_list', 'user_list',

View File

@ -10,14 +10,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.core import exceptions as django_exceptions
from django.template import defaultfilters from django.template import defaultfilters
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy from django.utils.translation import ungettext_lazy
from horizon import exceptions as horizon_exceptions
from horizon import forms from horizon import forms
from horizon import messages
from horizon import tables from horizon import tables
from openstack_dashboard import api from openstack_dashboard import api
from openstack_dashboard import policy from openstack_dashboard import policy
@ -176,47 +173,6 @@ class UpdateRow(tables.Row):
return user_info return user_info
class UpdateCell(tables.UpdateAction):
def allowed(self, request, user, cell):
return api.keystone.keystone_can_edit_user() and \
policy.check((("identity", "identity:update_user"),),
request)
def update_cell(self, request, datum, user_id,
cell_name, new_cell_value):
try:
user_obj = datum
setattr(user_obj, cell_name, new_cell_value)
if ((not new_cell_value) or new_cell_value.isspace()) and \
(cell_name == 'name'):
message = _("The User Name field cannot be empty.")
messages.warning(request, message)
raise django_exceptions.ValidationError(message)
kwargs = {}
attr_to_keyword_map = {
'name': 'name',
'description': 'description',
'email': 'email',
'enabled': 'enabled',
'project_id': 'project'
}
for key in attr_to_keyword_map:
value = getattr(user_obj, key, None)
keyword_name = attr_to_keyword_map[key]
if value is not None:
kwargs[keyword_name] = value
api.keystone.user_update(request, user_obj, **kwargs)
except horizon_exceptions.Conflict:
message = _("This name is already taken.")
messages.warning(request, message)
raise django_exceptions.ValidationError(message)
except Exception:
horizon_exceptions.handle(request, ignore=True)
return False
return True
class UsersTable(tables.DataTable): class UsersTable(tables.DataTable):
STATUS_CHOICES = ( STATUS_CHOICES = (
("true", True), ("true", True),
@ -225,19 +181,16 @@ class UsersTable(tables.DataTable):
name = tables.Column('name', name = tables.Column('name',
link="horizon:identity:users:detail", link="horizon:identity:users:detail",
verbose_name=_('User Name'), verbose_name=_('User Name'),
form_field=forms.CharField(required=False), form_field=forms.CharField(required=False))
update_action=UpdateCell)
description = tables.Column(lambda obj: getattr(obj, 'description', None), description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'), verbose_name=_('Description'),
hidden=KEYSTONE_V2_ENABLED, hidden=KEYSTONE_V2_ENABLED,
form_field=forms.CharField( form_field=forms.CharField(
widget=forms.Textarea(attrs={'rows': 4}), widget=forms.Textarea(attrs={'rows': 4}),
required=False), required=False))
update_action=UpdateCell)
email = tables.Column(lambda obj: getattr(obj, 'email', None), email = tables.Column(lambda obj: getattr(obj, 'email', None),
verbose_name=_('Email'), verbose_name=_('Email'),
form_field=forms.EmailField(required=False), form_field=forms.EmailField(required=False),
update_action=UpdateCell,
filters=(lambda v: defaultfilters filters=(lambda v: defaultfilters
.default_if_none(v, ""), .default_if_none(v, ""),
defaultfilters.escape, defaultfilters.escape,

View File

@ -0,0 +1,7 @@
---
deprecations:
- Inline Edit functionality for Horizon tables is now deprecated and will be
removed in Horizon P (12.0)
The functionality was removed from the following tables.
Admin Volume Types table, Admin Metadata Definitions table, Identity
Projects table and Identity Users table