diff --git a/doc/source/ref/tables.rst b/doc/source/ref/tables.rst index b2f22d896..749844b48 100644 --- a/doc/source/ref/tables.rst +++ b/doc/source/ref/tables.rst @@ -90,7 +90,7 @@ Actions .. autoclass:: DeleteAction :members: -.. autoclass:: UpdateAction +.. autoclass:: UpdateAction **DEPRECATED** :members: Class-Based Views diff --git a/doc/source/topics/tables.rst b/doc/source/topics/tables.rst index 913b4dada..b521a4cd7 100644 --- a/doc/source/topics/tables.rst +++ b/doc/source/topics/tables.rst @@ -288,8 +288,8 @@ Example:: admin=True) return project_info -Updating changed cell data --------------------------- +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 diff --git a/doc/source/tutorials/dashboard.rst b/doc/source/tutorials/dashboard.rst index b7e1edb76..96071ae5f 100644 --- a/doc/source/tutorials/dashboard.rst +++ b/doc/source/tutorials/dashboard.rst @@ -287,7 +287,7 @@ There are also additional actions which are extensions of the basic Action class - :class:`~horizon.tables.BatchAction` - :class:`~horizon.tables.DeleteAction` -- :class:`~horizon.tables.UpdateAction` +- :class:`~horizon.tables.UpdateAction` **DEPRECATED** - :class:`~horizon.tables.FixedFilterAction` diff --git a/horizon/static/horizon/js/horizon.tables_inline_edit.js b/horizon/static/horizon/js/horizon.tables_inline_edit.js index 0e33e20d4..ca53b7218 100644 --- a/horizon/static/horizon/js/horizon.tables_inline_edit.js +++ b/horizon/static/horizon/js/horizon.tables_inline_edit.js @@ -1,3 +1,4 @@ +//TODO(lcastell):Inline edit is deprecated and will be removed in Horizon 12.0 horizon.inline_edit = { get_cell_id: function (td_element) { return [ diff --git a/horizon/static/horizon/js/horizon.tabs.js b/horizon/static/horizon/js/horizon.tabs.js index 4102d640c..bdbd199db 100644 --- a/horizon/static/horizon/js/horizon.tabs.js +++ b/horizon/static/horizon/js/horizon.tabs.js @@ -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); diff --git a/horizon/tables/actions.py b/horizon/tables/actions.py index 1e9da87ed..865c85fef 100644 --- a/horizon/tables/actions.py +++ b/horizon/tables/actions.py @@ -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): """A table action for cell updates by inline editing.""" + name = "update" def action(self, request, datum, obj_id, cell_name, new_cell_value): diff --git a/openstack_dashboard/dashboards/admin/metadata_defs/tables.py b/openstack_dashboard/dashboards/admin/metadata_defs/tables.py index 50e46c135..b5a0dc0f5 100644 --- a/openstack_dashboard/dashboards/admin/metadata_defs/tables.py +++ b/openstack_dashboard/dashboards/admin/metadata_defs/tables.py @@ -16,7 +16,6 @@ from django.template import defaultfilters as filters from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy -from horizon import exceptions from horizon import forms from horizon import tables @@ -95,33 +94,6 @@ class UpdateRow(tables.Row): 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): display_name = tables.Column( "display_name", @@ -143,15 +115,13 @@ class AdminNamespacesTable(tables.DataTable): verbose_name=_("Public"), empty_value=False, form_field=forms.BooleanField(required=False), - filters=(filters.yesno, filters.capfirst), - update_action=UpdateCell) + filters=(filters.yesno, filters.capfirst)) protected = tables.Column( "protected", verbose_name=_("Protected"), empty_value=False, form_field=forms.BooleanField(required=False), - filters=(filters.yesno, filters.capfirst), - update_action=UpdateCell) + filters=(filters.yesno, filters.capfirst)) def get_object_id(self, datum): return datum.namespace diff --git a/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py b/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py index d9e3d339d..3adcada8a 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py +++ b/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py @@ -20,7 +20,6 @@ from horizon import forms from horizon import tables from openstack_dashboard.api import cinder -from openstack_dashboard import policy class CreateVolumeType(tables.LinkAction): @@ -196,51 +195,14 @@ class UpdateRow(tables.Row): 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): name = tables.Column("name", verbose_name=_("Name"), - form_field=forms.CharField( - max_length=64), - update_action=UpdateCell) + form_field=forms.CharField(max_length=64)) description = tables.Column(lambda obj: getattr(obj, 'description', None), verbose_name=_('Description'), form_field=forms.CharField( widget=forms.Textarea(attrs={'rows': 4}), - required=False), - update_action=UpdateCell) + required=False)) assoc_qos_spec = tables.Column("associated_qos_spec", verbose_name=_("Associated QoS Spec")) @@ -250,7 +212,6 @@ class VolumeTypesTable(tables.DataTable): public = tables.Column("is_public", verbose_name=_("Public"), filters=(filters.yesno, filters.capfirst), - update_action=UpdateCell, form_field=forms.BooleanField( label=_('Public'), required=False)) diff --git a/openstack_dashboard/dashboards/identity/projects/tables.py b/openstack_dashboard/dashboards/identity/projects/tables.py index fc5ba5fa8..02ab84578 100644 --- a/openstack_dashboard/dashboards/identity/projects/tables.py +++ b/openstack_dashboard/dashboards/identity/projects/tables.py @@ -10,17 +10,14 @@ # License for the specific language governing permissions and limitations # under the License. -from django.core.exceptions import ValidationError # noqa from django.core.urlresolvers import reverse from django.template import defaultfilters as filters from django.utils.http import urlencode from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy -from horizon import exceptions from horizon import forms from horizon import tables -from keystoneclient.exceptions import Conflict # noqa from openstack_dashboard import api from openstack_dashboard import policy @@ -220,58 +217,21 @@ class UpdateRow(tables.Row): 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): name = tables.Column('name', verbose_name=_('Name'), link=("horizon:identity:projects:detail"), - form_field=forms.CharField(max_length=64), - update_action=UpdateCell) + form_field=forms.CharField(max_length=64)) description = tables.Column(lambda obj: getattr(obj, 'description', None), verbose_name=_('Description'), form_field=forms.CharField( widget=forms.Textarea(attrs={'rows': 4}), - required=False), - update_action=UpdateCell) + required=False)) id = tables.Column('id', verbose_name=_('Project ID')) enabled = tables.Column('enabled', verbose_name=_('Enabled'), status=True, filters=(filters.yesno, filters.capfirst), form_field=forms.BooleanField( label=_('Enabled'), - required=False), - update_action=UpdateCell) + required=False)) if api.keystone.VERSIONS.active >= 3: domain_name = tables.Column( @@ -281,8 +241,7 @@ class TenantsTable(tables.DataTable): filters=(filters.yesno, filters.capfirst), form_field=forms.BooleanField( label=_('Enabled'), - required=False), - update_action=UpdateCell) + required=False)) def get_project_detail_link(self, project): # this method is an ugly monkey patch, needed because diff --git a/openstack_dashboard/dashboards/identity/projects/tests.py b/openstack_dashboard/dashboards/identity/projects/tests.py index a20938a27..d0a27e5bb 100644 --- a/openstack_dashboard/dashboards/identity/projects/tests.py +++ b/openstack_dashboard/dashboards/identity/projects/tests.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -import copy import datetime import logging import os @@ -36,13 +35,6 @@ from openstack_dashboard.test import helpers as test from openstack_dashboard import usage 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') USER_ROLE_PREFIX = workflows.PROJECT_USER_MEMBER_SLUG + "_role_" @@ -1637,143 +1629,6 @@ class DetailProjectViewTests(test.BaseAdminViewTests): @unittest.skipUnless(os.environ.get('WITH_SELENIUM', False), "The WITH_SELENIUM env variable is not set.") 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', 'get_default_role', 'user_list', diff --git a/openstack_dashboard/dashboards/identity/users/tables.py b/openstack_dashboard/dashboards/identity/users/tables.py index 0d93704c1..66f940aa7 100644 --- a/openstack_dashboard/dashboards/identity/users/tables.py +++ b/openstack_dashboard/dashboards/identity/users/tables.py @@ -10,14 +10,11 @@ # License for the specific language governing permissions and limitations # under the License. -from django.core import exceptions as django_exceptions from django.template import defaultfilters from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy -from horizon import exceptions as horizon_exceptions from horizon import forms -from horizon import messages from horizon import tables from openstack_dashboard import api from openstack_dashboard import policy @@ -176,47 +173,6 @@ class UpdateRow(tables.Row): 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): STATUS_CHOICES = ( ("true", True), @@ -225,19 +181,16 @@ class UsersTable(tables.DataTable): name = tables.Column('name', link="horizon:identity:users:detail", verbose_name=_('User Name'), - form_field=forms.CharField(required=False), - update_action=UpdateCell) + form_field=forms.CharField(required=False)) description = tables.Column(lambda obj: getattr(obj, 'description', None), verbose_name=_('Description'), hidden=KEYSTONE_V2_ENABLED, form_field=forms.CharField( widget=forms.Textarea(attrs={'rows': 4}), - required=False), - update_action=UpdateCell) + required=False)) email = tables.Column(lambda obj: getattr(obj, 'email', None), verbose_name=_('Email'), form_field=forms.EmailField(required=False), - update_action=UpdateCell, filters=(lambda v: defaultfilters .default_if_none(v, ""), defaultfilters.escape, diff --git a/releasenotes/notes/remove-inline-edit-63f92054238378d3.yaml b/releasenotes/notes/remove-inline-edit-63f92054238378d3.yaml new file mode 100644 index 000000000..7e01d5fae --- /dev/null +++ b/releasenotes/notes/remove-inline-edit-63f92054238378d3.yaml @@ -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