From 6b7e9b3f53398a56aefcc2ef30c4313bd14c8d1f Mon Sep 17 00:00:00 2001 From: Brian Fischer Date: Wed, 7 Jan 2015 15:28:54 -0800 Subject: [PATCH] Update to horizon tutorial.rst Added documentation for addition of an action to a horizon table DocImpact Closes-Bug: #1360370 Change-Id: I0d3c5da5a860d777e21e61d36a5765c412911dba --- doc/source/index.rst | 1 + doc/source/topics/table_actions.rst | 300 ++++++++++++++++++++++++++++ doc/source/topics/tutorial.rst | 105 ++++++++-- 3 files changed, 389 insertions(+), 17 deletions(-) create mode 100644 doc/source/topics/table_actions.rst diff --git a/doc/source/index.rst b/doc/source/index.rst index a55a7acbd..c3f4c0987 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -77,6 +77,7 @@ the following topic guides. topics/tables topics/policy topics/testing + topics/table_actions API Reference ------------- diff --git a/doc/source/topics/table_actions.rst b/doc/source/topics/table_actions.rst new file mode 100644 index 000000000..30d172551 --- /dev/null +++ b/doc/source/topics/table_actions.rst @@ -0,0 +1,300 @@ +============================================ +Tutorial: Adding a complex action to a table +============================================ + +This tutorial covers how to add a more complex action to a table, one that requires +an action and form definitions, as well as changes to the view, urls, and table. + +This tutorial assumes you have already completed :doc:`Building a Dashboard using +Horizon `. If not, please do so now as we will be modifying the +files created there. + +This action will create a snapshot of the instance. When the action is taken, +it will display a form that will allow the user to enter a snapshot name, +and will create that snapshot when the form is closed using the ``Create snapshot`` +button. + +Defining the view +================= + +To define the view, we must create a view class, along with the template (``HTML``) +file and the form class for that view. + +The template file +----------------- +The template file contains the HTML that will be used to show the view. + +Create a ``create_snapshot.html`` file under the ``mypanel/templates/mypanel`` +directory and add the following code:: + + {% extends 'base.html' %} + {% load i18n %} + {% block title %}{% trans "Create Snapshot" %}{% endblock %} + + {% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create a Snapshot") %} + {% endblock page_header %} + + {% block main %} + {% include 'mydashboard/mypanel/_create_snapshot.html' %} + {% endblock %} + + +As you can see, the main body will be defined in ``_create_snapshot.html``, +so we must also create that file under the ``mypanel/templates/mypanel`` +directory. It should contain the following code:: + + {% extends "horizon/common/_modal_form.html" %} + {% load i18n %} + + {% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Snapshots preserve the disk state of a running instance." %}

+ {% endblock %} + + +The form +-------- + +Horizon provides a :class:`~horizon.forms.base.SelfHandlingForm` class which simplifies +some of the details involved in creating a form. Our form will derive from this +class, adding a character field to allow the user to specify a name for the +snapshot, and handling the successful closure of the form by calling the nova +api to create the snapshot. + +Create the ``forms.py`` file under the ``mypanel`` directory and add the following:: + + from django.core.urlresolvers import reverse + from django.utils.translation import ugettext_lazy as _ + + from horizon import exceptions + from horizon import forms + + from openstack_dashboard import api + + + class CreateSnapshot(forms.SelfHandlingForm): + instance_id = forms.CharField(label=_("Instance ID"), + widget=forms.HiddenInput(), + required=False) + name = forms.CharField(max_length=255, label=_("Snapshot Name")) + + def handle(self, request, data): + try: + snapshot = api.nova.snapshot_create(request, + data['instance_id'], + data['name']) + return snapshot + except Exception: + exceptions.handle(request, + _('Unable to create snapshot.')) + + +The view +-------- + +Now, the view will tie together the template and the form. Horizon provides a +:class:`~horizon.forms.views.ModalFormView` class which simplifies the creation of a +view that will contain a modal form. + +Open the ``views.py`` file under the ``mypanel`` directory and add the code +for the CreateSnapshotView and the necessary imports. The complete +file should now look something like this:: + + from django.core.urlresolvers import reverse + from django.core.urlresolvers import reverse_lazy + from django.utils.translation import ugettext_lazy as _ + + from horizon import tabs + from horizon import exceptions + from horizon import forms + + from horizon.utils import memoized + + from openstack_dashboard import api + + from openstack_dashboard.dashboards.mydashboard.mypanel \ + import forms as project_forms + + from openstack_dashboard.dashboards.mydashboard.mypanel \ + import tabs as mydashboard_tabs + + + class IndexView(tabs.TabbedTableView): + tab_group_class = mydashboard_tabs.MypanelTabs + # A very simple class-based view... + template_name = 'mydashboard/mypanel/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context + + + class CreateSnapshotView(forms.ModalFormView): + form_class = project_forms.CreateSnapshot + template_name = 'mydashboard/mypanel/create_snapshot.html' + success_url = reverse_lazy("horizon:project:images:index") + modal_id = "create_snapshot_modal" + modal_header = _("Create Snapshot") + submit_label = _("Create Snapshot") + submit_url = "horizon:mydashboard:mypanel:create_snapshot" + + @memoized.memoized_method + def get_object(self): + try: + return api.nova.server_get(self.request, + self.kwargs["instance_id"]) + except Exception: + exceptions.handle(self.request, + _("Unable to retrieve instance.")) + + def get_initial(self): + return {"instance_id": self.kwargs["instance_id"]} + + def get_context_data(self, **kwargs): + context = super(CreateSnapshotView, self).get_context_data(**kwargs) + instance_id = self.kwargs['instance_id'] + context['instance_id'] = instance_id + context['instance'] = self.get_object() + context['submit_url'] = reverse(self.submit_url, args=[instance_id]) + return context + + +Adding the url +============== + +We must add the url for our new view. Open the ``urls.py`` file under +the ``mypanel`` directory and add the following as a new url pattern:: + + url(r'^(?P[^/]+)/create_snapshot/$', + views.CreateSnapshotView.as_view(), + name='create_snapshot'), + +The complete ``urls.py`` file should look like this:: + + from django.conf.urls import patterns + from django.conf.urls import url + + from openstack_dashboard.dashboards.mydashboard.mypanel import views + + + urlpatterns = patterns('', + url(r'^\?tab=mypanel_tabs_tab$', + views.IndexView.as_view(), name='mypanel_tabs'), + url(r'^(?P[^/]+)/create_snapshot/$', + views.CreateSnapshotView.as_view(), + name='create_snapshot'), + ) + + + +Define the action +================= + +Horizon provides a :class:`~horizon.tables.LinkAction` class which simplifies +adding an action which can be used to display another view. + +We will add a link action to the table that will be accessible from each row +in the table. The action will use the view defined above to create a snapshot +of the instance represented by the row in the table. + +To do this, we must edit the ``tables.py`` file under the ``mypanel`` directory +and add the following:: + + def is_deleting(instance): + task_state = getattr(instance, "OS-EXT-STS:task_state", None) + if not task_state: + return False + return task_state.lower() == "deleting" + + + class CreateSnapshotAction(tables.LinkAction): + name = "snapshot" + verbose_name = _("Create Snapshot") + url = "horizon:mydashboard:mypanel:create_snapshot" + classes = ("ajax-modal",) + icon = "camera" + + # This action should be disabled if the instance + # is not active, or the instance is being deleted + def allowed(self, request, instance=None): + return instance.status in ("ACTIVE") \ + and not is_deleting(instance) + + +We must also add our new action as a row action for the table:: + + row_actions = (CreateSnapshotAction,) + + +The complete ``tables.py`` file should look like this:: + + from django.utils.translation import ugettext_lazy as _ + + from horizon import tables + + + def is_deleting(instance): + task_state = getattr(instance, "OS-EXT-STS:task_state", None) + if not task_state: + return False + return task_state.lower() == "deleting" + + + class CreateSnapshotAction(tables.LinkAction): + name = "snapshot" + verbose_name = _("Create Snapshot") + url = "horizon:mydashboard:mypanel:create_snapshot" + classes = ("ajax-modal",) + icon = "camera" + + def allowed(self, request, instance=None): + return instance.status in ("ACTIVE") \ + and not is_deleting(instance) + + + class MyFilterAction(tables.FilterAction): + name = "myfilter" + + + class InstancesTable(tables.DataTable): + name = tables.Column("name", verbose_name=_("Name")) + status = tables.Column("status", verbose_name=_("Status")) + zone = tables.Column('availability_zone', verbose_name=_("Availability Zone")) + image_name = tables.Column('image_name', verbose_name=_("Image Name")) + + class Meta: + name = "instances" + verbose_name = _("Instances") + table_actions = (MyFilterAction,) + row_actions = (CreateSnapshotAction,) + + +Run and check the dashboard +=========================== + +We must once again run horizon to verify our dashboard is working:: + + ./run_tests.sh --runserver 0.0.0.0:8877 + + +Go to ``http://:8877`` using a browser. After login as an admin, +display ``My Panel`` to see the ``Instances`` table. For every ``ACTIVE`` +instance in the table, there will be a ``Create Snapshot`` action on the row. +Click on ``Create Snapshot``, enter a snapshot name in the form that is shown, +then click to close the form. The ``Project Images`` view should be shown with +the new snapshot added to the table. + + +Conclusion +========== + +What you've learned here is the fundamentals of how to add a table action that +requires a form for data entry. This can easily be expanded from creating a +snapshot to other API calls that require more complex forms to gather the +necessary information. + +If you have feedback on how this tutorial could be improved, please feel free +to submit a bug against ``Horizon`` in `launchpad`_. + + .. _launchpad: https://bugs.launchpad.net/horizon diff --git a/doc/source/topics/tutorial.rst b/doc/source/topics/tutorial.rst index b0b65bc75..2318bf67e 100644 --- a/doc/source/topics/tutorial.rst +++ b/doc/source/topics/tutorial.rst @@ -201,7 +201,7 @@ the default panel:: default_panel = 'mypanel' # Specify the slug of the default panel. -The completed ``dashoboard.py`` file should look like +The completed ``dashboard.py`` file should look like the following:: from django.utils.translation import ugettext_lazy as _ @@ -235,7 +235,7 @@ view from the pieces. Defining a table ~~~~~~~~~~~~~~~~ -Horizon provides a :class:`~horizon.tables.DataTable` class which simplifies +Horizon provides a :class:`~horizon.forms.SelfHandlingForm` :class:`~horizon.tables.DataTable` class which simplifies the vast majority of displaying data to an end-user. We're just going to skim the surface here, but it has a tremendous number of capabilities. @@ -274,6 +274,74 @@ the ``instances`` table. object is actually structured. In reality, accessing other attributes requires an additional step. +Adding actions to a table +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Horizon provides three types of basic action classes which can be taken +on a table's data: + +- :class:`~horizon.tables.Action` +- :class:`~horizon.tables.LinkAction` +- :class:`~horizon.tables.FilterAction` + + +There are also additional actions which are extensions of the basic Action classes: + +- :class:`~horizon.tables.BatchAction` +- :class:`~horizon.tables.DeleteAction` +- :class:`~horizon.tables.UpdateAction` +- :class:`~horizon.tables.FixedFilterAction` + + + +Now let's create and add a filter action to the table. To do so, we will need +to edit the ``tables.py`` file used above. To add a filter action which will +only show rows which contain the string entered in the filter field, we +must first define the action:: + + class MyFilterAction(tables.FilterAction): + name = "myfilter" + + +.. note:: + + The action specified above will default the ``filter_type`` to be ``"query"``. + This means that the filter will use the client side table sorter. + +Then, we add that action to the table actions for our table.:: + + class InstancesTable: + class Meta: + table_actions = (MyFilterAction,) + + +The completed ``tables.py`` file should look like the following:: + + from django.utils.translation import ugettext_lazy as _ + + from horizon import tables + + + class MyFilterAction(tables.FilterAction): + name = "myfilter" + + + class InstancesTable(tables.DataTable): + name = tables.Column('name', \ + verbose_name=_("Name")) + status = tables.Column('status', \ + verbose_name=_("Status")) + zone = tables.Column('availability_zone', \ + verbose_name=_("Availability Zone")) + image_name = tables.Column('image_name', \ + verbose_name=_("Image Name")) + + class Meta: + name = "instances" + verbose_name = _("Instances") + table_actions = (MyFilterAction,) + + Defining tabs ~~~~~~~~~~~~~ @@ -414,12 +482,7 @@ Adjust the import of ``IndexView`` to make the code readable:: from openstack_dashboard.dashboards.mydashboard.mypanel import views -Update the existing ``url`` pattern to use ``views`` :: - - url(r'^$', views.IndexView.as_view(), name='index'), - - -Insert the following lines after the existing ``url`` pattern:: +Replace the existing ``url`` pattern with the following line:: url(r'^\?tab=mypanel_tabs__tab$', views.IndexView.as_view(), name='mypanel_tabs'), @@ -437,7 +500,6 @@ The completed ``urls.py`` file should look like the following:: urlpatterns = patterns('', - url(r'^$', views.IndexView.as_view(), name='index'), url(r'^\?tab=mypanel_tabs_tab$', views.IndexView.as_view(), name='mypanel_tabs'), ) @@ -449,7 +511,7 @@ The template Open the ``index.html`` file in the ``mydashboard/mypanel/templates/mypanel`` directory, the auto-generated code is like the following:: - {% extends 'mydashboard/base.html' %} + {% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "Mypanel" %}{% endblock %} @@ -457,17 +519,17 @@ directory, the auto-generated code is like the following:: {% include "horizon/common/_page_header.html" with title=_("Mypanel") %} {% endblock page_header %} - {% block mydashboard_main %} + {% block main %} {% endblock %} -Insert the following code inside the ``mydashboard_main`` block:: +The ``main`` block must be modified to insert the following code:: -
+
{{ tab_group.render }}
-
+
If you want to change the title of the ``index.html`` file to be something else, @@ -476,7 +538,7 @@ you can change it. For example, change it to be ``My Panel`` in the section to be something else, you can change it. For example, change it to be ``My Panel``. The updated code could be like:: - {% extends 'mydashboard/base.html' %} + {% extends 'base.html' %} {% load i18n %} {% block title %}{% trans "My Panel" %}{% endblock %} @@ -484,7 +546,7 @@ section to be something else, you can change it. For example, change it to be {% include "horizon/common/_page_header.html" with title=_("My Panel") %} {% endblock page_header %} - {% block mydashboard_main %} + {% block main %}
{{ tab_group.render }} @@ -529,7 +591,7 @@ following:: Run and check the dashboard -============================= +=========================== Everything is in place, now run ``Horizon`` on the different port:: @@ -549,6 +611,15 @@ click the button ``Launch``. It should create an instance if the openstack or devstack is correctly set up. Once the creation of an instance is successful, go to ``My Dashboard`` again to check the data. + +Adding a complex action to a table +================================== + +For a more detailed look into adding a table action, one that requires forms for +gathering data, you can walk through :doc:`Adding a complex action to a table +` tutorial. + + Conclusion ==========