1.. _install-customizing:
2
3===================
4Customizing Horizon
5===================
6
7.. seealso::
8
9   You may also be interested in :doc:`themes` and :doc:`branding`.
10
11Changing the Site Title
12=======================
13
14The OpenStack Dashboard Site Title branding (i.e. "**OpenStack** Dashboard")
15can be overwritten by adding the attribute ``SITE_BRANDING``
16to ``local_settings.py`` with the value being the desired name.
17
18The file ``local_settings.py`` can be found at the Horizon directory path of
19``openstack_dashboard/local/local_settings.py``.
20
21Changing the Brand Link
22=======================
23
24The logo also acts as a hyperlink. The default behavior is to redirect to
25``horizon:user_home``. By adding the attribute ``SITE_BRANDING_LINK`` with
26the desired url target e.g., ``http://sample-company.com`` in
27``local_settings.py``, the target of the hyperlink can be changed.
28
29Customizing the Footer
30======================
31
32It is possible to customize the global and login footers by using Django's
33recursive inheritance to extend the ``base.html``, ``auth/login.html``, and
34``auth/_login_form.html`` templates. You do this by naming your template the
35same name as the template you wish to extend and only overriding the blocks you
36wish to change.
37
38Your theme's ``base.html``::
39
40    {% extends "base.html" %}
41
42    {% block footer %}
43      <p>My custom footer</p>
44    {% endblock %}
45
46Your theme's ``auth/login.html``::
47
48    {% extends "auth/login.html" %}
49
50    {% block footer %}
51      <p>My custom login footer</p>
52    {% endblock %}
53
54Your theme's ``auth/_login_form.html``::
55
56    {% extends "auth/_login_form.html" %}
57
58    {% block login_footer %}
59      {% comment %}
60        You MUST have block.super because that includes the login button.
61      {% endcomment %}
62     {{ block.super }}
63      <p>My custom login form footer</p>
64    {% endblock %}
65
66See the ``example`` theme for a working theme that uses these blocks.
67
68
69Modifying Existing Dashboards and Panels
70========================================
71
72If you wish to alter dashboards or panels which are not part of your codebase,
73you can specify a custom python module which will be loaded after the entire
74Horizon site has been initialized, but prior to the URLconf construction.
75This allows for common site-customization requirements such as:
76
77* Registering or unregistering panels from an existing dashboard.
78* Changing the names of dashboards and panels.
79* Re-ordering panels within a dashboard or panel group.
80
81Default Horizon panels are loaded based upon files within the
82openstack_dashboard/enabled/ folder. These files are loaded based upon the
83filename order, with space left for more files to be added. There are some
84example files available within this folder, with the .example suffix
85added. Developers and deployers should strive to use this method of
86customization as much as possible, and support for this is given preference
87over more exotic methods such as monkey patching and overrides files.
88
89.. _horizon-customization-module:
90
91Horizon customization module (overrides)
92========================================
93
94Horizon has a global overrides mechanism available to perform customizations
95that are not yet customizable via configuration settings. This file can perform
96monkey patching and other forms of customization which are not possible via the
97enabled folder's customization method.
98
99This method of customization is meant to be available for deployers of Horizon,
100and use of this should be avoided by Horizon plugins at all cost. Plugins
101needing this level of monkey patching and flexibility should instead look for
102changing their __init__.py file and performing customizations through other
103means.
104
105To specify the python module containing your modifications, add the key
106``customization_module`` to your ``HORIZON_CONFIG`` dictionary in
107``local_settings.py``. The value should be a string containing the path to your
108module in dotted python path notation. Example::
109
110    HORIZON_CONFIG["customization_module"] = "my_project.overrides"
111
112You can do essentially anything you like in the customization module. For
113example, you could change the name of a panel::
114
115    from django.utils.translation import ugettext_lazy as _
116
117    import horizon
118
119    # Rename "User Settings" to "User Options"
120    settings = horizon.get_dashboard("settings")
121    user_panel = settings.get_panel("user")
122    user_panel.name = _("User Options")
123
124Or get the instances panel::
125
126    projects_dashboard = horizon.get_dashboard("project")
127    instances_panel = projects_dashboard.get_panel("instances")
128
129Or just remove it entirely::
130
131    projects_dashboard.unregister(instances_panel.__class__)
132
133You cannot unregister a ``default_panel``. If you wish to remove a
134``default_panel``, you need to make a different panel in the dashboard as a
135``default_panel`` and then unregister the former. For example, if you wished
136to remove the ``overview_panel`` from the ``Project`` dashboard, you could do
137the following::
138
139    project = horizon.get_dashboard('project')
140    project.default_panel = "instances"
141    overview = project.get_panel('overview')
142    project.unregister(overview.__class__)
143
144You can also override existing methods with your own versions::
145
146    from openstack_dashboard.dashboards.admin.info import tabs
147    from openstack_dashboard.dashboards.project.instances import tables
148
149    NO = lambda *x: False
150
151    tables.AssociateIP.allowed = NO
152    tables.SimpleAssociateIP.allowed = NO
153    tables.SimpleDisassociateIP.allowed = NO
154
155You could also customize what columns are displayed in an existing
156table, by redefining the ``columns`` attribute of its ``Meta``
157class. This can be achieved in 3 steps:
158
159#. Extend the table that you wish to modify
160#. Redefine the ``columns`` attribute under the ``Meta`` class for this
161   new table
162#. Modify the ``table_class`` attribute for the related view so that it
163   points to the new table
164
165
166For example, if you wished to remove the Admin State column from the
167:class:`~openstack_dashboard.dashboards.admin.networks.tables.NetworksTable`,
168you could do the following::
169
170    from openstack_dashboard.dashboards.project.networks import tables
171    from openstack_dashboard.dashboards.project.networks import views
172
173    class MyNetworksTable(tables.NetworksTable):
174
175        class Meta(tables.NetworksTable.Meta):
176            columns = ('name', 'subnets', 'shared', 'status')
177
178    views.IndexView.table_class = MyNetworksTable
179
180If you want to add a column you can override the parent table in a
181similar way, add the new column definition and then use the ``Meta``
182``columns`` attribute to control the column order as needed.
183
184.. NOTE::
185
186    ``my_project.overrides`` needs to be importable by the python process running
187    Horizon.
188    If your module is not installed as a system-wide python package,
189    you can either make it installable (e.g., with a setup.py)
190    or you can adjust the python path used by your WSGI server to include its location.
191
192    Probably the easiest way is to add a ``python-path`` argument to
193    the ``WSGIDaemonProcess`` line in Apache's Horizon config.
194
195    Assuming your ``my_project`` module lives in ``/opt/python/my_project``,
196    you'd make it look like the following::
197
198        WSGIDaemonProcess [... existing options ...] python-path=/opt/python
199
200
201Customize the project and user table columns
202============================================
203
204
205Keystone V3 has a place to store extra information regarding project and user.
206Using the override mechanism described in :ref:`horizon-customization-module`,
207Horizon is able to show these extra information as a custom column.
208For example, if a user in Keystone has an attribute ``phone_num``, you could
209define new column::
210
211    from django.utils.translation import ugettext_lazy as _
212
213    from horizon import forms
214    from horizon import tables
215
216    from openstack_dashboard.dashboards.identity.users import tables as user_tables
217    from openstack_dashboard.dashboards.identity.users import views
218
219    class MyUsersTable(user_tables.UsersTable):
220        phone_num = tables.Column('phone_num',
221                                  verbose_name=_('Phone Number'),
222                                  form_field=forms.CharField(),)
223
224        class Meta(user_tables.UsersTable.Meta):
225            columns = ('name', 'description', 'phone_num')
226
227    views.IndexView.table_class = MyUsersTable
228
229
230Customize Angular dashboards
231============================
232
233In Angular, you may write a plugin to extend certain features. Two components
234in the Horizon framework that make this possible are the extensibility service
235and the resource type registry service. The ``extensibleService`` allows
236certain Horizon elements to be extended dynamically, including add, remove, and
237replace. The ``resourceTypeRegistry`` service provides methods to set and get
238information pertaining to a resource type object. We use Heat type names like
239``OS::Glance::Image`` as our reference name.
240
241Some information you may place in the registry include:
242
243* API to fetch data from
244* Property names
245* Actions (e.g. "Create Volume")
246* URL paths to detail view or detail drawer
247* Property information like labels or formatting for property values
248
249These properties in the registry use the extensibility service (as of Newton
250release):
251
252* globalActions
253* batchActions
254* itemActions
255* detailViews
256* tableColumns
257* filterFacets
258
259Using the information from the registry, we can build out our dashboard panels.
260Panels use the high-level directive ``hzResourceTable`` that replaces common
261templates so we do not need to write boilerplate HTML and controller code. It
262gives developers a quick way to build a new table or change an existing table.
263
264.. note::
265
266    You may still choose to use the HTML template for complete control of form
267    and functionality. For example, you may want to create a custom footer.
268    You may also use the ``hzDynamicTable`` directive (what ``hzResourceTable``
269    uses under the hood) directly. However, neither of these is extensible.
270    You would need to override the panel completely.
271
272This is a sample module file to demonstrate how to make some customizations to
273the Images Panel.::
274
275    (function() {
276      'use strict';
277
278      angular
279        .module('horizon.app.core.images')
280        .run(customizeImagePanel);
281
282      customizeImagePanel.$inject = [
283        'horizon.framework.conf.resource-type-registry.service',
284        'horizon.app.core.images.basePath',
285        'horizon.app.core.images.resourceType',
286        'horizon.app.core.images.actions.surprise.service'
287      ];
288
289      function customizeImagePanel(registry, basePath, imageResourceType, surpriseService) {
290        // get registry for ``OS::Glance::Image``
291        registry = registry.getResourceType(imageResourceType);
292
293        // replace existing Size column to make the font color red
294        var column = {
295          id: 'size',
296          priority: 2,
297          template: '<a style="color:red;">{$ item.size | bytes $}</a>'
298        };
299        registry.tableColumns.replace('size', column);
300
301        // add a new detail view
302        registry.detailsViews
303          .append({
304            id: 'anotherDetailView',
305            name: gettext('Another Detail View'),
306            template: basePath + 'demo/detail.html'
307        });
308
309        // set a different summary drawer template
310        registry.setSummaryTemplateUrl(basePath + 'demo/drawer.html');
311
312        // add a new global action
313        registry.globalActions
314          .append({
315            id: 'surpriseAction',
316            service: surpriseService,
317            template: {
318              text: gettext('Surprise')
319            }
320        });
321      }
322    })();
323
324Additionally, you should have content defined in ``detail.html`` and
325``drawer.html``, as well as define the ``surpriseService`` which is based off
326the ``actions`` directive and needs allowed and perform methods defined.
327
328
329Icons
330=====
331
332Horizon uses font icons from Font Awesome.  Please see `Font Awesome`_ for
333instructions on how to use icons in the code.
334
335To add icon to Table Action, use icon property. Example:
336
337.. code-block:: python
338
339   class CreateSnapshot(tables.LinkAction):
340       name = "snapshot"
341       verbose_name = _("Create Snapshot")
342       icon = "camera"
343
344Additionally, the site-wide default button classes can be configured by
345setting ``ACTION_CSS_CLASSES`` to a tuple of the classes you wish to appear
346on all action buttons in your ``local_settings.py`` file.
347
348
349Custom Stylesheets
350==================
351
352It is possible to define custom stylesheets for your dashboards. Horizon's base
353template ``openstack_dashboard/templates/base.html`` defines multiple blocks
354that can be overridden.
355
356To define custom css files that apply only to a specific dashboard, create
357a base template in your dashboard's templates folder, which extends Horizon's
358base template e.g. ``openstack_dashboard/dashboards/my_custom_dashboard/
359templates/my_custom_dashboard/base.html``.
360
361In this template, redefine ``block css``. (Don't forget to include
362``_stylesheets.html`` which includes all Horizon's default stylesheets.)::
363
364    {% extends 'base.html' %}
365
366    {% block css %}
367      {% include "_stylesheets.html" %}
368
369      {% load compress %}
370      {% compress css %}
371      <link href='{{ STATIC_URL }}my_custom_dashboard/scss/my_custom_dashboard.scss' type='text/scss' media='screen' rel='stylesheet' />
372      {% endcompress %}
373    {% endblock %}
374
375The custom stylesheets then reside in the dashboard's own ``static`` folder
376``openstack_dashboard/dashboards/my_custom_dashboard/static/my_custom_dashboard/scss/my_custom_dashboard.scss``.
377
378All dashboard's templates have to inherit from dashboard's base.html::
379
380    {% extends 'my_custom_dashboard/base.html' %}
381    ...
382
383
384Custom Javascript
385=================
386
387Similarly to adding custom styling (see above), it is possible to include
388custom javascript files.
389
390All Horizon's javascript files are listed in the
391``openstack_dashboard/templates/horizon/_scripts.html``
392partial template, which is included in Horizon's base template in ``block js``.
393
394To add custom javascript files, create an ``_scripts.html`` partial template in
395your dashboard
396``openstack_dashboard/dashboards/my_custom_dashboard/templates/my_custom_dashboard/_scripts.html``
397which extends ``horizon/_scripts.html``. In this template override the
398``block custom_js_files`` including your custom javascript files::
399
400    {% extends 'horizon/_scripts.html' %}
401
402    {% block custom_js_files %}
403        <script src='{{ STATIC_URL }}my_custom_dashboard/js/my_custom_js.js' type='text/javascript' charset='utf-8'></script>
404    {% endblock %}
405
406
407In your dashboard's own base template ``openstack_dashboard/dashboards/
408my_custom_dashboard/templates/my_custom_dashboard/base.html`` override
409``block js`` with inclusion of dashboard's own ``_scripts.html``::
410
411    {% block js %}
412        {% include "my_custom_dashboard/_scripts.html" %}
413    {% endblock %}
414
415The result is a single compressed js file consisting both Horizon and
416dashboard's custom scripts.
417
418Custom Head js
419--------------
420
421Additionally, some scripts require you to place them within the page's <head>
422tag. To do this, recursively extend the ``base.html`` template in your theme
423to override the ``custom_head_js`` block.
424
425Your theme's ``base.html``::
426
427    {% extends "base.html" %}
428
429    {% block custom_head_js %}
430      <script src='{{ STATIC_URL }}/my_custom_dashboard/js/my_custom_js.js' type='text/javascript' charset='utf-8'></script>
431    {% endblock %}
432
433See the ``example`` theme for a working theme that uses these blocks.
434
435.. warning::
436
437    Don't use the ``custom_head_js`` block for analytics tracking. See below.
438
439Custom Analytics
440----------------
441
442For analytics or tracking scripts you should avoid the ``custom_head_js``
443block. We have a specific block instead called ``custom_analytics``. Much like
444the ``custom_head_js`` block this inserts additional content into the head of
445the ``base.html`` template and it will be on all pages.
446
447The reason for an analytics specific block is that for security purposes we
448want to be able to turn off tracking on certain pages that we deem sensitive.
449This is done for the safety of the users and the cloud admins. By using this
450block instead, pages using ``base.html`` can override it themselves when they
451want to avoid tracking. They can't simply override the custom js because it may
452be non-tracking code.
453
454Your theme's ``base.html``::
455
456    {% extends "base.html" %}
457
458    {% block custom_analytics %}
459      <script src='{{ STATIC_URL }}/my_custom_dashboard/js/my_tracking_js.js' type='text/javascript' charset='utf-8'></script>
460    {% endblock %}
461
462See the ``example`` theme for a working theme that uses these blocks.
463
464Customizing Meta Attributes
465===========================
466
467To add custom metadata attributes to your project's base template use the
468``custom_metadata`` block. To do this, recursively extend the ``base.html``
469template in your theme to override the ``custom_metadata`` block. The contents
470of this block will be inserted into the page's <head> just after the default
471Horizon meta tags.
472
473Your theme's ``base.html``::
474
475    {% extends "base.html" %}
476
477    {% block custom_metadata %}
478      <meta name="description" content="My custom metadata.">
479    {% endblock %}
480
481See the ``example`` theme for a working theme that uses these blocks.
482
483..  _Font Awesome: https://fortawesome.github.io/Font-Awesome/
484