[python-django-horizon] Updated patches from master-patches

Matthias Runge mrunge at fedoraproject.org
Thu Oct 16 10:19:20 UTC 2014


commit 65c8e21fb5d6ffb87407228be4a6dd45c6644b28
Author: Matthias Runge <mrunge at redhat.com>
Date:   Thu Oct 16 10:30:56 2014 +0200

    Updated patches from master-patches

 0001-disable-debug-move-web-root.patch             |   61 ++
 ...file-location-to-tmp-and-also-add-localho.patch |    4 +-
 ...-Add-a-customization-module-based-on-RHOS.patch |    8 +-
 ...olicy-files-and-checks-to-etc-openstack-d.patch |    4 +-
 ...5-move-SECRET_KEY-secret_key_store-to-tmp.patch |    4 +-
 ...atch => 0006-RCUE-navbar-and-login-screen.patch |    8 +-
 ...e8-issues.patch => 0007-fix-flake8-issues.patch |    2 +-
 ... => 0008-remove-runtime-dep-to-python-pbr.patch |    2 +-
 ...dd-Change-password-link-to-the-RCUE-theme.patch |    2 +-
 ...rcue.patch => 0010-.less-replaced-in-rcue.patch |   15 +-
 0010-Theme-fixes.patch                             |   23 -
 0011-re-add-lesscpy-to-compile-.less.patch         |    6 +-
 ...gration-of-LESS-to-SCSS-and-various-fixes.patch |    2 +-
 ...redundant-Settings-button-on-downstream-t.patch |   35 +
 0014-Change-page-header-heading-to-H1.patch        |  143 +++
 0015-Add-dropdown-actions-to-detail-page.patch     |  203 ++++
 ...Add-dropdown-actions-to-all-details-pages.patch | 1038 ++++++++++++++++++++
 ...d-support-for-row-actions-to-detail-pages.patch |  205 ++++
 0018-Clean-up-test-output.patch                    |  116 +++
 python-django-horizon.spec                         |   26 +-
 20 files changed, 1847 insertions(+), 60 deletions(-)
---
diff --git a/0001-disable-debug-move-web-root.patch b/0001-disable-debug-move-web-root.patch
new file mode 100644
index 0000000..f2e81f1
--- /dev/null
+++ b/0001-disable-debug-move-web-root.patch
@@ -0,0 +1,61 @@
+From 28e1333c3052545015639fb0805eba3e63fd080b Mon Sep 17 00:00:00 2001
+From: Matthias Runge <mrunge at redhat.com>
+Date: Fri, 5 Apr 2013 10:07:53 +0200
+Subject: [PATCH] disable debug, move web root
+
+---
+ openstack_dashboard/local/local_settings.py.example |  2 +-
+ openstack_dashboard/settings.py                     | 10 ++++++----
+ 2 files changed, 7 insertions(+), 5 deletions(-)
+
+diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
+index 5e988da..78f2858 100644
+--- a/openstack_dashboard/local/local_settings.py.example
++++ b/openstack_dashboard/local/local_settings.py.example
+@@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
+ 
+ from openstack_dashboard import exceptions
+ 
+-DEBUG = True
++DEBUG = False
+ TEMPLATE_DEBUG = DEBUG
+ 
+ # Required for Django 1.5.
+diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py
+index ad9e691..786978e 100644
+--- a/openstack_dashboard/settings.py
++++ b/openstack_dashboard/settings.py
+@@ -48,7 +48,7 @@ warnings.formatwarning = lambda message, category, *args, **kwargs: \
+     '%s: %s' % (category.__name__, message)
+ 
+ ROOT_PATH = os.path.dirname(os.path.abspath(__file__))
+-BIN_DIR = os.path.abspath(os.path.join(ROOT_PATH, '..', 'bin'))
++BIN_DIR = '/usr/bin'
+ 
+ if ROOT_PATH not in sys.path:
+     sys.path.append(ROOT_PATH)
+@@ -58,12 +58,13 @@ TEMPLATE_DEBUG = DEBUG
+ 
+ SITE_BRANDING = 'OpenStack Dashboard'
+ 
+-LOGIN_URL = '/auth/login/'
+-LOGOUT_URL = '/auth/logout/'
++WEBROOT = '/dashboard'
++LOGIN_URL = WEBROOT + '/auth/login/'
++LOGOUT_URL = WEBROOT + '/auth/logout/'
+ # LOGIN_REDIRECT_URL can be used as an alternative for
+ # HORIZON_CONFIG.user_home, if user_home is not set.
+ # Do not set it to '/home/', as this will cause circular redirect loop
+-LOGIN_REDIRECT_URL = '/'
++LOGIN_REDIRECT_URL = WEBROOT
+ 
+ MEDIA_ROOT = os.path.abspath(os.path.join(ROOT_PATH, '..', 'media'))
+ MEDIA_URL = '/media/'
+@@ -211,6 +212,7 @@ COMPRESS_ENABLED = True
+ COMPRESS_OUTPUT_DIR = 'dashboard'
+ COMPRESS_CSS_HASHING_METHOD = 'hash'
+ COMPRESS_PARSER = 'compressor.parser.HtmlParser'
++COMPRESS_OFFLINE = True
+ 
+ INSTALLED_APPS = [
+     'openstack_dashboard',
diff --git a/0001-change-lockfile-location-to-tmp-and-also-add-localho.patch b/0002-change-lockfile-location-to-tmp-and-also-add-localho.patch
similarity index 93%
rename from 0001-change-lockfile-location-to-tmp-and-also-add-localho.patch
rename to 0002-change-lockfile-location-to-tmp-and-also-add-localho.patch
index 6c7afa5..f237e49 100644
--- a/0001-change-lockfile-location-to-tmp-and-also-add-localho.patch
+++ b/0002-change-lockfile-location-to-tmp-and-also-add-localho.patch
@@ -1,4 +1,4 @@
-From 197ad035e3ca1edfdbef701150c07827ddc9b504 Mon Sep 17 00:00:00 2001
+From 854be29fe112581280f1126de6e5b31bb0070ed3 Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Thu, 25 Jul 2013 11:32:38 +0200
 Subject: [PATCH] change lockfile location to '/tmp' and also add localhost to
@@ -9,7 +9,7 @@ Subject: [PATCH] change lockfile location to '/tmp' and also add localhost to
  1 file changed, 2 insertions(+), 2 deletions(-)
 
 diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
-index 5e988da..dc03d5b 100644
+index 78f2858..3feab86 100644
 --- a/openstack_dashboard/local/local_settings.py.example
 +++ b/openstack_dashboard/local/local_settings.py.example
 @@ -12,7 +12,7 @@ TEMPLATE_DEBUG = DEBUG
diff --git a/0002-Add-a-customization-module-based-on-RHOS.patch b/0003-Add-a-customization-module-based-on-RHOS.patch
similarity index 97%
rename from 0002-Add-a-customization-module-based-on-RHOS.patch
rename to 0003-Add-a-customization-module-based-on-RHOS.patch
index 87e34ef..85c7a16 100644
--- a/0002-Add-a-customization-module-based-on-RHOS.patch
+++ b/0003-Add-a-customization-module-based-on-RHOS.patch
@@ -1,4 +1,4 @@
-From 882123ae68de71f80dd98c850b1fd25408e0551c Mon Sep 17 00:00:00 2001
+From ff582f08e91c9c7b4b1095b590ffe9dc7fea864c Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Thu, 14 Feb 2013 12:55:54 +0100
 Subject: [PATCH] Add a customization module based on RHOS
@@ -28,10 +28,10 @@ Conflicts:
  create mode 100644 openstack_dashboard_theme/templates/splash.html
 
 diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py
-index ad9e691..8f4a32a 100644
+index 786978e..1fad2e9 100644
 --- a/openstack_dashboard/settings.py
 +++ b/openstack_dashboard/settings.py
-@@ -137,14 +137,13 @@ TEMPLATE_CONTEXT_PROCESSORS = (
+@@ -138,14 +138,13 @@ TEMPLATE_CONTEXT_PROCESSORS = (
  )
  
  TEMPLATE_LOADERS = (
@@ -47,7 +47,7 @@ index ad9e691..8f4a32a 100644
  
  STATICFILES_FINDERS = (
      'django.contrib.staticfiles.finders.FileSystemFinder',
-@@ -227,6 +226,15 @@ INSTALLED_APPS = [
+@@ -229,6 +228,15 @@ INSTALLED_APPS = [
      'openstack_auth',
  ]
  
diff --git a/0003-move-RBAC-policy-files-and-checks-to-etc-openstack-d.patch b/0004-move-RBAC-policy-files-and-checks-to-etc-openstack-d.patch
similarity index 91%
rename from 0003-move-RBAC-policy-files-and-checks-to-etc-openstack-d.patch
rename to 0004-move-RBAC-policy-files-and-checks-to-etc-openstack-d.patch
index 1f54103..d32c6db 100644
--- a/0003-move-RBAC-policy-files-and-checks-to-etc-openstack-d.patch
+++ b/0004-move-RBAC-policy-files-and-checks-to-etc-openstack-d.patch
@@ -1,4 +1,4 @@
-From e347fd14730a22a91dc6e334a6751fff70297e22 Mon Sep 17 00:00:00 2001
+From 2e93cdcb50651eacc3bea504ad4bceebfeaa2980 Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Thu, 31 Jul 2014 10:13:12 +0200
 Subject: [PATCH] move RBAC policy files and checks to /etc/openstack-dashboard
@@ -10,7 +10,7 @@ Conflicts:
  1 file changed, 2 insertions(+), 1 deletion(-)
 
 diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
-index dc03d5b..37910da 100644
+index 3feab86..ed547eb 100644
 --- a/openstack_dashboard/local/local_settings.py.example
 +++ b/openstack_dashboard/local/local_settings.py.example
 @@ -273,7 +273,8 @@ TIME_ZONE = "UTC"
diff --git a/0004-move-SECRET_KEY-secret_key_store-to-tmp.patch b/0005-move-SECRET_KEY-secret_key_store-to-tmp.patch
similarity index 91%
rename from 0004-move-SECRET_KEY-secret_key_store-to-tmp.patch
rename to 0005-move-SECRET_KEY-secret_key_store-to-tmp.patch
index 2868040..a85d273 100644
--- a/0004-move-SECRET_KEY-secret_key_store-to-tmp.patch
+++ b/0005-move-SECRET_KEY-secret_key_store-to-tmp.patch
@@ -1,4 +1,4 @@
-From c1f1bc006a5d8b8441b83398477a85910af2c2b4 Mon Sep 17 00:00:00 2001
+From 16ea98344b5005843ba8ffe97bae04d2ff4b28f2 Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Mon, 9 Sep 2013 20:52:51 +0200
 Subject: [PATCH] move SECRET_KEY secret_key_store to /tmp
@@ -8,7 +8,7 @@ Subject: [PATCH] move SECRET_KEY secret_key_store to /tmp
  1 file changed, 1 insertion(+), 1 deletion(-)
 
 diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
-index 37910da..0aa1a02 100644
+index ed547eb..ae84041 100644
 --- a/openstack_dashboard/local/local_settings.py.example
 +++ b/openstack_dashboard/local/local_settings.py.example
 @@ -83,7 +83,6 @@ HORIZON_CONFIG = {
diff --git a/0005-RCUE-navbar-and-login-screen.patch b/0006-RCUE-navbar-and-login-screen.patch
similarity index 99%
rename from 0005-RCUE-navbar-and-login-screen.patch
rename to 0006-RCUE-navbar-and-login-screen.patch
index 1fced63..369953d 100644
--- a/0005-RCUE-navbar-and-login-screen.patch
+++ b/0006-RCUE-navbar-and-login-screen.patch
@@ -1,4 +1,4 @@
-From 0b1f097a679bf26e4c7bf51ecfc61845b0905499 Mon Sep 17 00:00:00 2001
+From 313c2529670282ffa9f8d59de97c0bccf966f8dc Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Tue, 14 Oct 2014 11:33:00 +0200
 Subject: [PATCH] RCUE navbar and login screen
@@ -19092,10 +19092,10 @@ index 0000000..f37f848
 +ADD_INSTALLED_APPS = ['openstack_dashboard.dashboards.theme']
 +
 diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py
-index 8f4a32a..4406706 100644
+index 1fad2e9..0e5084a 100644
 --- a/openstack_dashboard/settings.py
 +++ b/openstack_dashboard/settings.py
-@@ -143,7 +143,7 @@ TEMPLATE_LOADERS = (
+@@ -144,7 +144,7 @@ TEMPLATE_LOADERS = (
  
  TEMPLATE_DIRS = (
      os.path.join(ROOT_PATH, 'templates'),
@@ -19104,7 +19104,7 @@ index 8f4a32a..4406706 100644
  
  STATICFILES_FINDERS = (
      'django.contrib.staticfiles.finders.FileSystemFinder',
-@@ -226,15 +226,6 @@ INSTALLED_APPS = [
+@@ -228,15 +228,6 @@ INSTALLED_APPS = [
      'openstack_auth',
  ]
  
diff --git a/0006-fix-flake8-issues.patch b/0007-fix-flake8-issues.patch
similarity index 95%
rename from 0006-fix-flake8-issues.patch
rename to 0007-fix-flake8-issues.patch
index c574f1d..6de3417 100644
--- a/0006-fix-flake8-issues.patch
+++ b/0007-fix-flake8-issues.patch
@@ -1,4 +1,4 @@
-From b8ea41c83fbb730a2147491c18c98fbefd316e71 Mon Sep 17 00:00:00 2001
+From 6532985169afd46649062a6ae6decfef68bb33e9 Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Thu, 10 Apr 2014 09:27:21 +0200
 Subject: [PATCH] fix flake8 issues
diff --git a/0007-remove-runtime-dep-to-python-pbr.patch b/0008-remove-runtime-dep-to-python-pbr.patch
similarity index 93%
rename from 0007-remove-runtime-dep-to-python-pbr.patch
rename to 0008-remove-runtime-dep-to-python-pbr.patch
index f648344..8e0ad0d 100644
--- a/0007-remove-runtime-dep-to-python-pbr.patch
+++ b/0008-remove-runtime-dep-to-python-pbr.patch
@@ -1,4 +1,4 @@
-From aad4fa47000b8c8e9eb3908f477a6a65d1c88bca Mon Sep 17 00:00:00 2001
+From 5c081060b90598fa9f30a0449a3d14be05cf265d Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Fri, 15 Nov 2013 09:42:08 +0100
 Subject: [PATCH] remove runtime dep to python-pbr
diff --git a/0008-Add-Change-password-link-to-the-RCUE-theme.patch b/0009-Add-Change-password-link-to-the-RCUE-theme.patch
similarity index 96%
rename from 0008-Add-Change-password-link-to-the-RCUE-theme.patch
rename to 0009-Add-Change-password-link-to-the-RCUE-theme.patch
index 08d8957..c48ec79 100644
--- a/0008-Add-Change-password-link-to-the-RCUE-theme.patch
+++ b/0009-Add-Change-password-link-to-the-RCUE-theme.patch
@@ -1,4 +1,4 @@
-From af0c8b76218cfce952a88a43763b46d7443dd492 Mon Sep 17 00:00:00 2001
+From 6f017c146ba566dc28f71abe7e7d07c88cba8c90 Mon Sep 17 00:00:00 2001
 From: Julie Pichon <jpichon at redhat.com>
 Date: Tue, 20 May 2014 16:22:31 +0100
 Subject: [PATCH] Add "Change password" link to the RCUE theme.
diff --git a/0009-.less-replaced-in-rcue.patch b/0010-.less-replaced-in-rcue.patch
similarity index 73%
rename from 0009-.less-replaced-in-rcue.patch
rename to 0010-.less-replaced-in-rcue.patch
index ca42981..f10049c 100644
--- a/0009-.less-replaced-in-rcue.patch
+++ b/0010-.less-replaced-in-rcue.patch
@@ -1,17 +1,17 @@
-From 128a3b2adb6c1a97dd30f256d8de01aa264ae172 Mon Sep 17 00:00:00 2001
+From f5196bd426ccc7444cd33d88626807a4523f89d9 Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Thu, 31 Jul 2014 11:17:29 +0200
 Subject: [PATCH] .less replaced in rcue
 
 ---
- openstack_dashboard/dashboards/theme/templates/_stylesheets.html | 5 ++++-
- 1 file changed, 4 insertions(+), 1 deletion(-)
+ openstack_dashboard/dashboards/theme/templates/_stylesheets.html | 8 +++++---
+ 1 file changed, 5 insertions(+), 3 deletions(-)
 
 diff --git a/openstack_dashboard/dashboards/theme/templates/_stylesheets.html b/openstack_dashboard/dashboards/theme/templates/_stylesheets.html
-index c3899cf..29c4b69 100644
+index c3899cf..8f9c16b 100644
 --- a/openstack_dashboard/dashboards/theme/templates/_stylesheets.html
 +++ b/openstack_dashboard/dashboards/theme/templates/_stylesheets.html
-@@ -1,8 +1,11 @@
+@@ -1,10 +1,12 @@
  {% load compress %}
  
  {% compress css %}
@@ -21,6 +21,9 @@ index c3899cf..29c4b69 100644
 +<link href='{{ STATIC_URL }}dashboard/css/rickshaw.css' type='text/css' media='screen' rel='stylesheet' />
 +<link href='{{ STATIC_URL }}dashboard/scss/horizon_charts.scss' type='text/scss' media='screen' rel='stylesheet' />
 +<link href='{{ STATIC_URL }}dashboard/scss/horizon_workflow.scss' type='text/scss' media='screen' rel='stylesheet' />
++<link href='{{ STATIC_URL }}horizon/lib/font-awesome/scss/font-awesome.scss' type='text/scss' media='screen' rel='stylesheet' />
  {% endcompress %}
  
- <link href='{{ STATIC_URL }}dashboard/css/font-awesome.min.css' type='text/css' media='screen' rel='stylesheet' />
+-<link href='{{ STATIC_URL }}dashboard/css/font-awesome.min.css' type='text/css' media='screen' rel='stylesheet' />
+-
+ <link rel="shortcut icon" href="{{ STATIC_URL }}dashboard/img/rhfavicon.ico"/>
diff --git a/0011-re-add-lesscpy-to-compile-.less.patch b/0011-re-add-lesscpy-to-compile-.less.patch
index f8f82af..5e684f7 100644
--- a/0011-re-add-lesscpy-to-compile-.less.patch
+++ b/0011-re-add-lesscpy-to-compile-.less.patch
@@ -1,4 +1,4 @@
-From 2f5adea6e0e29d700d9f5f61e9321e0a1dd1a0a6 Mon Sep 17 00:00:00 2001
+From 75e41ad9bb3b945967774462132ae5b565355287 Mon Sep 17 00:00:00 2001
 From: Matthias Runge <mrunge at redhat.com>
 Date: Mon, 6 Oct 2014 12:54:03 +0200
 Subject: [PATCH] re-add lesscpy to compile .less
@@ -8,10 +8,10 @@ Subject: [PATCH] re-add lesscpy to compile .less
  1 file changed, 1 insertion(+)
 
 diff --git a/openstack_dashboard/settings.py b/openstack_dashboard/settings.py
-index 4406706..bb938cb 100644
+index 0e5084a..1d79451 100644
 --- a/openstack_dashboard/settings.py
 +++ b/openstack_dashboard/settings.py
-@@ -199,6 +199,7 @@ else:
+@@ -200,6 +200,7 @@ else:
          xstatic.main.XStatic(xstatic.pkg.jquery_ui).base_dir))
  
  COMPRESS_PRECOMPILERS = (
diff --git a/0012-Migration-of-LESS-to-SCSS-and-various-fixes.patch b/0012-Migration-of-LESS-to-SCSS-and-various-fixes.patch
index 68299f5..50e35fe 100644
--- a/0012-Migration-of-LESS-to-SCSS-and-various-fixes.patch
+++ b/0012-Migration-of-LESS-to-SCSS-and-various-fixes.patch
@@ -1,4 +1,4 @@
-From d038dc74ffec92b7cf28957d27ec4d024b4e20df Mon Sep 17 00:00:00 2001
+From 7ad131e92325f940d701a1021baec726ede0ebb6 Mon Sep 17 00:00:00 2001
 From: Robb Hamilton <rhamilto at localhost.localdomain>
 Date: Mon, 29 Sep 2014 11:32:19 -0400
 Subject: [PATCH] Migration of LESS to SCSS and various fixes
diff --git a/0013-Remove-the-redundant-Settings-button-on-downstream-t.patch b/0013-Remove-the-redundant-Settings-button-on-downstream-t.patch
new file mode 100644
index 0000000..077ecd9
--- /dev/null
+++ b/0013-Remove-the-redundant-Settings-button-on-downstream-t.patch
@@ -0,0 +1,35 @@
+From 93fc96f4ba91d5be0c382c13c6b00f051b44a576 Mon Sep 17 00:00:00 2001
+From: Julie Pichon <jpichon at redhat.com>
+Date: Wed, 15 Oct 2014 11:06:59 +0100
+Subject: [PATCH] Remove the redundant 'Settings' button on downstream theme
+
+Resolves rhbz: 1114292
+
+Change-Id: I725094a9c803c7d82bdfb3b6888e944d008e8d6f
+---
+ .../dashboards/theme/templates/horizon/_nav_list.html        | 12 +++++++-----
+ 1 file changed, 7 insertions(+), 5 deletions(-)
+
+diff --git a/openstack_dashboard/dashboards/theme/templates/horizon/_nav_list.html b/openstack_dashboard/dashboards/theme/templates/horizon/_nav_list.html
+index 6648421..ab97b79 100644
+--- a/openstack_dashboard/dashboards/theme/templates/horizon/_nav_list.html
++++ b/openstack_dashboard/dashboards/theme/templates/horizon/_nav_list.html
+@@ -2,10 +2,12 @@
+ 
+ <ul class="nav nav-tabs">
+   {% for component in components %}
+-    {% if user|has_permissions:component %}
+-      <li{% if current.slug == component.slug %} class="active"{% endif %}>
+-        <a href="{{ component.get_absolute_url }}" tabindex='1'>{{ component.name }}</a>
+-      </li>
++    {% if component.slug != 'settings' %}
++      {% if user|has_permissions:component %}
++        <li{% if current.slug == component.slug %} class="active"{% endif %}>
++          <a href="{{ component.get_absolute_url }}" tabindex='1'>{{ component.name }}</a>
++        </li>
++      {% endif %}
+     {% endif %}
+   {% endfor %}
+-</ul>
+\ No newline at end of file
++</ul>
diff --git a/0014-Change-page-header-heading-to-H1.patch b/0014-Change-page-header-heading-to-H1.patch
new file mode 100644
index 0000000..56605df
--- /dev/null
+++ b/0014-Change-page-header-heading-to-H1.patch
@@ -0,0 +1,143 @@
+From dfe519cce7c2715c9d9761b8935730289259c45b Mon Sep 17 00:00:00 2001
+From: Ana Krivokapic <akrivoka at redhat.com>
+Date: Tue, 19 Aug 2014 17:28:37 +0200
+Subject: [PATCH] Change page header heading to H1
+
+Partially implements: blueprint detail-pages-ia
+
+Change-Id: I42b58e73fd3990c5b5e197e5694eb77eeb42f77c
+---
+ horizon/templates/horizon/common/_domain_page_header.html         | 4 ++--
+ horizon/templates/horizon/common/_page_header.html                | 2 +-
+ openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py   | 2 +-
+ openstack_dashboard/dashboards/project/images/images/tests.py     | 2 +-
+ openstack_dashboard/dashboards/project/volumes/backups/tests.py   | 4 ++--
+ openstack_dashboard/dashboards/project/volumes/snapshots/tests.py | 2 +-
+ openstack_dashboard/dashboards/project/volumes/volumes/tests.py   | 2 +-
+ openstack_dashboard/static/dashboard/scss/horizon.scss            | 6 +++---
+ 8 files changed, 12 insertions(+), 12 deletions(-)
+
+diff --git a/horizon/templates/horizon/common/_domain_page_header.html b/horizon/templates/horizon/common/_domain_page_header.html
+index 63ab048..e24c933 100644
+--- a/horizon/templates/horizon/common/_domain_page_header.html
++++ b/horizon/templates/horizon/common/_domain_page_header.html
+@@ -1,11 +1,11 @@
+ {% load i18n %}
+ {% block page_header %}
+   <div class='page-header'>
+-    <h2>
++    <h1>
+     {% if request.session.domain_context_name %}
+       <em>{{ request.session.domain_context_name }}:</em>
+     {% endif %}
+     {{ title }}
+-    </h2>
++    </h1>
+   </div>
+ {% endblock %}
+diff --git a/horizon/templates/horizon/common/_page_header.html b/horizon/templates/horizon/common/_page_header.html
+index adeb06e..01a9210 100644
+--- a/horizon/templates/horizon/common/_page_header.html
++++ b/horizon/templates/horizon/common/_page_header.html
+@@ -1,6 +1,6 @@
+ {% load i18n %}
+ {% block page_header %}
+   <div class='page-header'>
+-    <h2>{{ title }}</h2>
++    <h1>{{ title }}</h1>
+   </div>
+ {% endblock %}
+diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py
+index 84a5abc..e9d76fb 100644
+--- a/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py
++++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/tests.py
+@@ -59,7 +59,7 @@ class VolumeSnapshotsViewTests(test.BaseAdminViewTests):
+         res = self.client.get(url)
+ 
+         self.assertContains(res,
+-                            "<h2>Volume Snapshot Details: %s</h2>" %
++                            "<h1>Volume Snapshot Details: %s</h1>" %
+                             snapshot.name,
+                             1, 200)
+         self.assertContains(res, "<dd>test snapshot</dd>", 1, 200)
+diff --git a/openstack_dashboard/dashboards/project/images/images/tests.py b/openstack_dashboard/dashboards/project/images/images/tests.py
+index 3507b87..afccd41 100644
+--- a/openstack_dashboard/dashboards/project/images/images/tests.py
++++ b/openstack_dashboard/dashboards/project/images/images/tests.py
+@@ -229,7 +229,7 @@ class ImageViewTests(test.TestCase):
+                             'project/images/images/detail.html')
+         self.assertEqual(res.context['image'].name, image.name)
+         self.assertEqual(res.context['image'].protected, image.protected)
+-        self.assertContains(res, "<h2>Image Details: %s</h2>" % image.name,
++        self.assertContains(res, "<h1>Image Details: %s</h1>" % image.name,
+                             1, 200)
+ 
+     @test.create_stubs({api.glance: ('image_get',)})
+diff --git a/openstack_dashboard/dashboards/project/volumes/backups/tests.py b/openstack_dashboard/dashboards/project/volumes/backups/tests.py
+index 088be05..651b79f 100644
+--- a/openstack_dashboard/dashboards/project/volumes/backups/tests.py
++++ b/openstack_dashboard/dashboards/project/volumes/backups/tests.py
+@@ -100,7 +100,7 @@ class VolumeBackupsViewTests(test.TestCase):
+         res = self.client.get(url)
+ 
+         self.assertContains(res,
+-                            "<h2>Volume Backup Details: %s</h2>" %
++                            "<h1>Volume Backup Details: %s</h1>" %
+                             backup.name,
+                             1, 200)
+         self.assertContains(res, "<dd>%s</dd>" % backup.name, 1, 200)
+@@ -141,7 +141,7 @@ class VolumeBackupsViewTests(test.TestCase):
+         res = self.client.get(url)
+ 
+         self.assertContains(res,
+-                            "<h2>Volume Backup Details: %s</h2>" %
++                            "<h1>Volume Backup Details: %s</h1>" %
+                             backup.name,
+                             1, 200)
+         self.assertContains(res, "<dd>%s</dd>" % backup.name, 1, 200)
+diff --git a/openstack_dashboard/dashboards/project/volumes/snapshots/tests.py b/openstack_dashboard/dashboards/project/volumes/snapshots/tests.py
+index 25a948a..ab46569 100644
+--- a/openstack_dashboard/dashboards/project/volumes/snapshots/tests.py
++++ b/openstack_dashboard/dashboards/project/volumes/snapshots/tests.py
+@@ -152,7 +152,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
+         res = self.client.get(url)
+ 
+         self.assertContains(res,
+-                            "<h2>Volume Snapshot Details: %s</h2>" %
++                            "<h1>Volume Snapshot Details: %s</h1>" %
+                             snapshot.name,
+                             1, 200)
+         self.assertContains(res, "<dd>test snapshot</dd>", 1, 200)
+diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
+index 58cbc1b..ec304a2 100644
+--- a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
++++ b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
+@@ -972,7 +972,7 @@ class VolumeViewTests(test.TestCase):
+                       args=[volume.id])
+         res = self.client.get(url)
+ 
+-        self.assertContains(res, "<h2>Volume Details: Volume name</h2>",
++        self.assertContains(res, "<h1>Volume Details: Volume name</h1>",
+                             1, 200)
+         self.assertContains(res, "<dd>Volume name</dd>", 1, 200)
+         self.assertContains(res, "<dd>%s</dd>" % volume.id, 1, 200)
+diff --git a/openstack_dashboard/static/dashboard/scss/horizon.scss b/openstack_dashboard/static/dashboard/scss/horizon.scss
+index 20aadaf..496e58d 100755
+--- a/openstack_dashboard/static/dashboard/scss/horizon.scss
++++ b/openstack_dashboard/static/dashboard/scss/horizon.scss
+@@ -172,12 +172,12 @@ dt {
+ 
+ .page-header {
+   margin: 0 0 5px 0;
+-  padding: 0 0 5px 0;
+-  border-bottom: 2px solid $headings-color;
++  padding: 10px 0 5px 0;
++  border-bottom: 0;
+   font-family: anivers;
+   height: auto;
+   width: 100%;
+-  h2 {
++  h1 {
+     margin: 0;
+   }
+ }
diff --git a/0015-Add-dropdown-actions-to-detail-page.patch b/0015-Add-dropdown-actions-to-detail-page.patch
new file mode 100644
index 0000000..a41868d
--- /dev/null
+++ b/0015-Add-dropdown-actions-to-detail-page.patch
@@ -0,0 +1,203 @@
+From 4beb6924bc7530f0a8c52c87070e0ab91d8235e7 Mon Sep 17 00:00:00 2001
+From: Ana Krivokapic <akrivoka at redhat.com>
+Date: Tue, 26 Aug 2014 13:19:12 +0200
+Subject: [PATCH] Add dropdown actions to detail page
+
+Add support for performing actions on an object from the object's
+detail page. Actions are displayed in the page header, in a
+dropdown menu. As a POC, this patch enables dropdown actions on a
+detail page of a volume (/project/volumes/volume-id/).
+
+Partially implements: blueprint detail-pages-ia
+
+Change-Id: Ie21c63d86c2806d9689ae7f1a67b67c83710c108
+---
+ horizon/tables/base.py                             |  7 ++--
+ .../horizon/common/_data_table_row_actions.html    |  2 +-
+ horizon/templates/horizon/common/_page_header.html |  6 +++
+ .../dashboards/project/volumes/volumes/views.py    |  6 ++-
+ .../static/dashboard/scss/horizon.scss             | 47 ++++++++++++----------
+ 5 files changed, 42 insertions(+), 26 deletions(-)
+
+diff --git a/horizon/tables/base.py b/horizon/tables/base.py
+index 16e3fd9..2f76b79 100644
+--- a/horizon/tables/base.py
++++ b/horizon/tables/base.py
+@@ -676,7 +676,7 @@ class Cell(html.HTMLElement):
+                                  form_field_attributes)
+             table._data_cache[column][table.get_object_id(datum)] = data
+         elif column.auto == "actions":
+-            data = table.render_row_actions(datum)
++            data = table.render_row_actions(datum, pull_right=False)
+             table._data_cache[column][table.get_object_id(datum)] = data
+         else:
+             data = column.get_data(datum)
+@@ -1388,7 +1388,7 @@ class DataTable(object):
+         self.set_multiselect_column_visibility(len(bound_actions) > 0)
+         return table_actions_template.render(context)
+ 
+-    def render_row_actions(self, datum):
++    def render_row_actions(self, datum, pull_right=True):
+         """Renders the actions specified in ``Meta.row_actions`` using the
+         current row data.
+         """
+@@ -1396,7 +1396,8 @@ class DataTable(object):
+         row_actions_template = template.loader.get_template(template_path)
+         bound_actions = self.get_row_actions(datum)
+         extra_context = {"row_actions": bound_actions,
+-                         "row_id": self.get_object_id(datum)}
++                         "row_id": self.get_object_id(datum),
++                         "pull_right": pull_right}
+         context = template.RequestContext(self.request, extra_context)
+         return row_actions_template.render(context)
+ 
+diff --git a/horizon/templates/horizon/common/_data_table_row_actions.html b/horizon/templates/horizon/common/_data_table_row_actions.html
+index 9508152..3f3368b 100644
+--- a/horizon/templates/horizon/common/_data_table_row_actions.html
++++ b/horizon/templates/horizon/common/_data_table_row_actions.html
+@@ -2,7 +2,7 @@
+ 
+ {% spaceless %} {# This makes sure whitespace doesn't affect positioning for dropdown. #}
+ {% if row_actions|length > 1 %}
+-<div class="btn-group">
++<div class="btn-group {% if pull_right %}pull-right{% endif %}">
+     {% for action in row_actions %}
+         {% if forloop.first %}
+           {% include "horizon/common/_data_table_row_action.html" %}
+diff --git a/horizon/templates/horizon/common/_page_header.html b/horizon/templates/horizon/common/_page_header.html
+index 01a9210..9491a55 100644
+--- a/horizon/templates/horizon/common/_page_header.html
++++ b/horizon/templates/horizon/common/_page_header.html
+@@ -2,5 +2,11 @@
+ {% block page_header %}
+   <div class='page-header'>
+     <h1>{{ title }}</h1>
++    {% if actions %}
++      <form class='actions_column pull-right' action='{{ url }}' method="POST">
++        {% csrf_token %}
++        {{ actions }}
++      </form>
++    {% endif %}
+   </div>
+ {% endblock %}
+diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/volumes/views.py
+index f068e9e..c78cd09 100644
+--- a/openstack_dashboard/dashboards/project/volumes/volumes/views.py
++++ b/openstack_dashboard/dashboards/project/volumes/volumes/views.py
+@@ -44,8 +44,12 @@ class DetailView(tabs.TabView):
+     template_name = 'project/volumes/volumes/detail.html'
+ 
+     def get_context_data(self, **kwargs):
++        datum = self.get_data()
++        table = project_tables.VolumesTable(self.request)
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["volume"] = self.get_data()
++        context["volume"] = datum
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(datum)
+         return context
+ 
+     @memoized.memoized_method
+diff --git a/openstack_dashboard/static/dashboard/scss/horizon.scss b/openstack_dashboard/static/dashboard/scss/horizon.scss
+index 496e58d..961850a 100755
+--- a/openstack_dashboard/static/dashboard/scss/horizon.scss
++++ b/openstack_dashboard/static/dashboard/scss/horizon.scss
+@@ -1090,20 +1090,25 @@ td.select {
+ 
+ /* Actions dropdown */
+ 
+-td.actions_column {
++.actions_column {
+   white-space: nowrap;
+   padding: 10px;
+   position: relative;
+   width: 200px;
+ }
+ 
+-td.actions_column .btn-group {
++form.actions_column {
++  width: auto;
++  font-family: $font-family-base;
++}
++
++.actions_column .btn-group {
+   display: inline-block;
+ }
+ 
+-td.actions_column .row_actions a,
+-td.actions_column .row_actions input,
+-td.actions_column .row_actions button,
++.actions_column .row_actions a,
++.actions_column .row_actions input,
++.actions_column .row_actions button,
+ div.table_actions_menu .dropdown-menu a,
+ div.table_actions_menu .dropdown-menu input,
+ div.table_actions_menu .dropdown-menu button {
+@@ -1118,11 +1123,11 @@ div.table_actions_menu .dropdown-menu button {
+   @include box-shadow(none);
+ }
+ 
+-td.actions_column .row_actions .hide {
++.actions_column .row_actions .hide {
+   display: none;
+ }
+ 
+-td.actions_column .btn-action-required {
++.actions_column .btn-action-required {
+   font-weight: bold;
+ }
+ 
+@@ -1156,8 +1161,8 @@ td.actions_column .btn-action-required {
+   background: none;
+ }
+ 
+-td.actions_column .dropdown-menu a:hover,
+-td.actions_column .dropdown-menu button:hover,
++.actions_column .dropdown-menu a:hover,
++.actions_column .dropdown-menu button:hover,
+ div.table_actions_menu .dropdown-menu a:hover,
+ div.table_actions_menu .dropdown-menu button:hover {
+   background-color: $gray-lighter;
+@@ -1172,30 +1177,30 @@ div.table_actions_menu .dropdown-menu button:hover {
+ 
+ /* Overrides for single-action rows (no dropdown) */
+ 
+-tr td.actions_column ul.row_actions.single,
+-tr:hover td.actions_column ul.row_actions.single,
+-td.actions_column ul.row_actions.single,
+-td.actions_column ul.row_actions.single:hover {
++tr .actions_column ul.row_actions.single,
++tr:hover .actions_column ul.row_actions.single,
++.actions_column ul.row_actions.single,
++.actions_column ul.row_actions.single:hover {
+   border: none;
+ }
+ 
+-td.actions_column ul.row_actions.single li.action {
++.actions_column ul.row_actions.single li.action {
+   display: block;
+ }
+ 
+-td.actions_column ul.row_actions.single li.action:hover {
++.actions_column ul.row_actions.single li.action:hover {
+   background-color: transparent;
+ }
+ 
+-td.actions_column ul.row_actions.single a,
+-td.actions_column ul.row_actions.single input,
+-td.actions_column ul.row_actions.single button {
++.actions_column ul.row_actions.single a,
++.actions_column ul.row_actions.single input,
++.actions_column ul.row_actions.single button {
+   color: $brand-info;
+ }
+ 
+-td.actions_column ul.row_actions.single a:hover,
+-td.actions_column ul.row_actions.single input:hover,
+-td.actions_column ul.row_actions.single button:hover {
++.actions_column ul.row_actions.single a:hover,
++.actions_column ul.row_actions.single input:hover,
++.actions_column ul.row_actions.single button:hover {
+   color: $text-color;
+ }
+ 
diff --git a/0016-Add-dropdown-actions-to-all-details-pages.patch b/0016-Add-dropdown-actions-to-all-details-pages.patch
new file mode 100644
index 0000000..6c372d8
--- /dev/null
+++ b/0016-Add-dropdown-actions-to-all-details-pages.patch
@@ -0,0 +1,1038 @@
+From dd6d7410af06051533aac01049c778267ea1d8f2 Mon Sep 17 00:00:00 2001
+From: Ana Krivokapic <akrivoka at redhat.com>
+Date: Wed, 3 Sep 2014 12:09:02 +0200
+Subject: [PATCH] Add dropdown actions to all details pages
+
+Partially implements: blueprint detail-pages-ia
+
+Change-Id: Ic09385d19b417b256cbc51d044d66ecbb54c7d52
+
+Conflicts:
+	openstack_dashboard/dashboards/admin/networks/views.py
+---
+ .../dashboards/admin/networks/tests.py             |  13 ++
+ .../dashboards/admin/networks/views.py             |  15 ++-
+ .../dashboards/admin/volumes/snapshots/views.py    |   3 +-
+ .../dashboards/project/images/images/views.py      |  15 ++-
+ .../dashboards/project/instances/views.py          |   6 +-
+ .../dashboards/project/loadbalancers/tabs.py       |  28 +----
+ .../dashboards/project/loadbalancers/views.py      |  97 ++++++++++++++-
+ .../dashboards/project/networks/ports/tabs.py      |  15 +--
+ .../dashboards/project/networks/ports/views.py     |  38 ++++++
+ .../dashboards/project/networks/subnets/tabs.py    |  17 +--
+ .../dashboards/project/networks/subnets/views.py   |  38 ++++++
+ .../dashboards/project/networks/tests.py           |   3 +
+ .../dashboards/project/networks/views.py           |  14 ++-
+ .../dashboards/project/routers/views.py            |   8 +-
+ .../dashboards/project/stacks/views.py             |  13 +-
+ .../dashboards/project/volumes/backups/views.py    |  16 ++-
+ .../dashboards/project/volumes/snapshots/views.py  |  11 +-
+ .../dashboards/project/volumes/volumes/views.py    |   8 +-
+ openstack_dashboard/dashboards/project/vpn/tabs.py |  41 +------
+ .../dashboards/project/vpn/views.py                | 131 ++++++++++++++++++++-
+ 20 files changed, 406 insertions(+), 124 deletions(-)
+
+diff --git a/openstack_dashboard/dashboards/admin/networks/tests.py b/openstack_dashboard/dashboards/admin/networks/tests.py
+index b7f5c93..e9f4890 100644
+--- a/openstack_dashboard/dashboards/admin/networks/tests.py
++++ b/openstack_dashboard/dashboards/admin/networks/tests.py
+@@ -103,6 +103,8 @@ class NetworkTests(test.BaseAdminViewTests):
+             .AndReturn(mac_learning)
+         api.neutron.is_extension_supported(IsA(http.HttpRequest),
+                 'dhcp_agent_scheduler').AndReturn(True)
++        api.neutron.is_extension_supported(IsA(http.HttpRequest),
++                'dhcp_agent_scheduler').AndReturn(True)
+ 
+         self.mox.ReplayAll()
+ 
+@@ -186,6 +188,8 @@ class NetworkTests(test.BaseAdminViewTests):
+             .AndReturn(mac_learning)
+         api.neutron.is_extension_supported(IsA(http.HttpRequest),
+                 'dhcp_agent_scheduler').AndReturn(True)
++        api.neutron.is_extension_supported(IsA(http.HttpRequest),
++                'dhcp_agent_scheduler').AndReturn(True)
+ 
+         self.mox.ReplayAll()
+ 
+@@ -228,6 +232,12 @@ class NetworkTests(test.BaseAdminViewTests):
+         api.neutron.is_extension_supported(IsA(http.HttpRequest),
+                                            'mac-learning')\
+             .AndReturn(mac_learning)
++        api.neutron.is_extension_supported(IsA(http.HttpRequest),
++                                           'dhcp_agent_scheduler')\
++            .AndReturn(True)
++        api.neutron.is_extension_supported(IsA(http.HttpRequest),
++                                           'dhcp_agent_scheduler')\
++            .AndReturn(True)
+ 
+         self.mox.ReplayAll()
+ 
+@@ -945,6 +955,9 @@ class NetworkPortTests(test.BaseAdminViewTests):
+         api.neutron.is_extension_supported(IsA(http.HttpRequest),
+                                            'mac-learning')\
+             .AndReturn(mac_learning)
++        api.neutron.is_extension_supported(IsA(http.HttpRequest),
++                                           'mac-learning')\
++            .AndReturn(mac_learning)
+ 
+         self.mox.ReplayAll()
+ 
+diff --git a/openstack_dashboard/dashboards/admin/networks/views.py b/openstack_dashboard/dashboards/admin/networks/views.py
+index ab29478..2337424 100644
+--- a/openstack_dashboard/dashboards/admin/networks/views.py
++++ b/openstack_dashboard/dashboards/admin/networks/views.py
+@@ -103,7 +103,6 @@ class DetailView(tables.MultiTableView):
+                      ports_tables.PortsTable,
+                      agents_tables.DHCPAgentsTable)
+     template_name = 'project/networks/detail.html'
+-    failure_url = reverse_lazy('horizon:admin:networks:index')
+ 
+     def get_subnets_data(self):
+         try:
+@@ -148,16 +147,15 @@ class DetailView(tables.MultiTableView):
+             network = api.neutron.network_get(self.request, network_id)
+             network.set_id_as_name_if_empty(length=0)
+         except Exception:
+-            redirect = self.failure_url
+             exceptions.handle(self.request,
+                               _('Unable to retrieve details for '
+                                 'network "%s".') % network_id,
+-                                redirect=redirect)
++                              redirect=self.get_redirect_url())
+         return network
+ 
+     def get_context_data(self, **kwargs):
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["network"] = self._get_data()
++        network = self._get_data()
+         # Needs to exclude agents table if dhcp-agent-scheduler extension
+         # is not supported.
+         try:
+@@ -166,8 +164,17 @@ class DetailView(tables.MultiTableView):
+             context['dhcp_agent_support'] = dhcp_agent_support
+         except Exception:
+             context['dhcp_agent_support'] = False
++
++        table = networks_tables.NetworksTable(self.request)
++        context["network"] = network
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(network)
+         return context
+ 
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy('horizon:admin:networks:index')
++
+ 
+ class UpdateView(user_views.UpdateView):
+     form_class = project_forms.UpdateNetwork
+diff --git a/openstack_dashboard/dashboards/admin/volumes/snapshots/views.py b/openstack_dashboard/dashboards/admin/volumes/snapshots/views.py
+index a068d92..399c49b 100644
+--- a/openstack_dashboard/dashboards/admin/volumes/snapshots/views.py
++++ b/openstack_dashboard/dashboards/admin/volumes/snapshots/views.py
+@@ -59,5 +59,6 @@ class UpdateStatusView(forms.ModalFormView):
+ class DetailView(views.DetailView):
+     tab_group_class = vol_snapshot_tabs.SnapshotDetailsTabs
+ 
+-    def get_redirect_url(self):
++    @staticmethod
++    def get_redirect_url():
+         return reverse('horizon:admin:volumes:index')
+diff --git a/openstack_dashboard/dashboards/project/images/images/views.py b/openstack_dashboard/dashboards/project/images/images/views.py
+index 36966dd..58fd5ae 100644
+--- a/openstack_dashboard/dashboards/project/images/images/views.py
++++ b/openstack_dashboard/dashboards/project/images/images/views.py
+@@ -33,6 +33,8 @@ from openstack_dashboard import api
+ from openstack_dashboard.dashboards.project.images.images \
+     import forms as project_forms
+ from openstack_dashboard.dashboards.project.images.images \
++    import tables as project_tables
++from openstack_dashboard.dashboards.project.images.images \
+     import tabs as project_tabs
+ 
+ 
+@@ -84,18 +86,25 @@ class DetailView(tabs.TabView):
+ 
+     def get_context_data(self, **kwargs):
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["image"] = self.get_data()
++        image = self.get_data()
++        table = project_tables.ImagesTable(self.request)
++        context["image"] = image
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(image)
+         return context
+ 
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy('horizon:project:images:index')
++
+     @memoized.memoized_method
+     def get_data(self):
+         try:
+             return api.glance.image_get(self.request, self.kwargs['image_id'])
+         except Exception:
+-            url = reverse('horizon:project:images:index')
+             exceptions.handle(self.request,
+                               _('Unable to retrieve image details.'),
+-                              redirect=url)
++                              redirect=self.get_redirect_url())
+ 
+     def get_tabs(self, request, *args, **kwargs):
+         image = self.get_data()
+diff --git a/openstack_dashboard/dashboards/project/instances/views.py b/openstack_dashboard/dashboards/project/instances/views.py
+index 954e2e1..07c8cc6 100644
+--- a/openstack_dashboard/dashboards/project/instances/views.py
++++ b/openstack_dashboard/dashboards/project/instances/views.py
+@@ -253,7 +253,11 @@ class DetailView(tabs.TabView):
+ 
+     def get_context_data(self, **kwargs):
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["instance"] = self.get_data()
++        instance = self.get_data()
++        context["instance"] = instance
++        table = project_tables.InstancesTable(self.request)
++        context["url"] = reverse(self.redirect_url)
++        context["actions"] = table.render_row_actions(instance)
+         return context
+ 
+     @memoized.memoized_method
+diff --git a/openstack_dashboard/dashboards/project/loadbalancers/tabs.py b/openstack_dashboard/dashboards/project/loadbalancers/tabs.py
+index 940b754..af48a07 100644
+--- a/openstack_dashboard/dashboards/project/loadbalancers/tabs.py
++++ b/openstack_dashboard/dashboards/project/loadbalancers/tabs.py
+@@ -21,7 +21,6 @@ from horizon import tabs
+ from openstack_dashboard import api
+ 
+ from openstack_dashboard.dashboards.project.loadbalancers import tables
+-from openstack_dashboard.dashboards.project.loadbalancers import utils
+ 
+ 
+ class PoolsTab(tabs.TableTab):
+@@ -94,16 +93,7 @@ class PoolDetailsTab(tabs.Tab):
+     template_name = "project/loadbalancers/_pool_details.html"
+ 
+     def get_context_data(self, request):
+-        pid = self.tab_group.kwargs['pool_id']
+-        try:
+-            pool = api.lbaas.pool_get(request, pid)
+-        except Exception:
+-            pool = []
+-            exceptions.handle(request,
+-                              _('Unable to retrieve pool details.'))
+-        for monitor in pool.health_monitors:
+-            display_name = utils.get_monitor_display_name(monitor)
+-            setattr(monitor, 'display_name', display_name)
++        pool = self.tab_group.kwargs['pool']
+         return {'pool': pool}
+ 
+ 
+@@ -129,13 +119,7 @@ class MemberDetailsTab(tabs.Tab):
+     template_name = "project/loadbalancers/_member_details.html"
+ 
+     def get_context_data(self, request):
+-        mid = self.tab_group.kwargs['member_id']
+-        try:
+-            member = api.lbaas.member_get(request, mid)
+-        except Exception:
+-            member = []
+-            exceptions.handle(self.tab_group.request,
+-                              _('Unable to retrieve member details.'))
++        member = self.tab_group.kwargs['member']
+         return {'member': member}
+ 
+ 
+@@ -145,13 +129,7 @@ class MonitorDetailsTab(tabs.Tab):
+     template_name = "project/loadbalancers/_monitor_details.html"
+ 
+     def get_context_data(self, request):
+-        mid = self.tab_group.kwargs['monitor_id']
+-        try:
+-            monitor = api.lbaas.pool_health_monitor_get(request, mid)
+-        except Exception:
+-            monitor = []
+-            exceptions.handle(self.tab_group.request,
+-                              _('Unable to retrieve monitor details.'))
++        monitor = self.tab_group.kwargs['monitor']
+         return {'monitor': monitor}
+ 
+ 
+diff --git a/openstack_dashboard/dashboards/project/loadbalancers/views.py b/openstack_dashboard/dashboards/project/loadbalancers/views.py
+index be731a4..de55889 100644
+--- a/openstack_dashboard/dashboards/project/loadbalancers/views.py
++++ b/openstack_dashboard/dashboards/project/loadbalancers/views.py
+@@ -26,7 +26,10 @@ from openstack_dashboard import api
+ from openstack_dashboard.dashboards.project.loadbalancers \
+     import forms as project_forms
+ from openstack_dashboard.dashboards.project.loadbalancers \
++    import tables as project_tables
++from openstack_dashboard.dashboards.project.loadbalancers \
+     import tabs as project_tabs
++from openstack_dashboard.dashboards.project.loadbalancers import utils
+ from openstack_dashboard.dashboards.project.loadbalancers \
+     import workflows as project_workflows
+ 
+@@ -115,24 +118,110 @@ class AddMonitorView(workflows.WorkflowView):
+ 
+ 
+ class PoolDetailsView(tabs.TabView):
+-    tab_group_class = (project_tabs.PoolDetailsTabs)
++    tab_group_class = project_tabs.PoolDetailsTabs
+     template_name = 'project/loadbalancers/details_tabs.html'
+ 
++    @memoized.memoized_method
++    def get_data(self):
++        pid = self.kwargs['pool_id']
++
++        try:
++            pool = api.lbaas.pool_get(self.request, pid)
++        except Exception:
++            pool = []
++            exceptions.handle(self.request,
++                              _('Unable to retrieve pool details.'))
++        else:
++            for monitor in pool.health_monitors:
++                display_name = utils.get_monitor_display_name(monitor)
++                setattr(monitor, 'display_name', display_name)
++
++        return pool
++
++    def get_context_data(self, **kwargs):
++        context = super(PoolDetailsView, self).get_context_data(**kwargs)
++        pool = self.get_data()
++        context['pool'] = pool
++        table = project_tables.PoolsTable(self.request)
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(pool)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        pool = self.get_data()
++        return self.tab_group_class(self.request, pool=pool, **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy("horizon:project:loadbalancers:index")
++
+ 
+ class VipDetailsView(tabs.TabView):
+-    tab_group_class = (project_tabs.VipDetailsTabs)
++    tab_group_class = project_tabs.VipDetailsTabs
+     template_name = 'project/loadbalancers/details_tabs.html'
+ 
+ 
+ class MemberDetailsView(tabs.TabView):
+-    tab_group_class = (project_tabs.MemberDetailsTabs)
++    tab_group_class = project_tabs.MemberDetailsTabs
+     template_name = 'project/loadbalancers/details_tabs.html'
+ 
++    @memoized.memoized_method
++    def get_data(self):
++        mid = self.kwargs['member_id']
++        try:
++            return api.lbaas.member_get(self.request, mid)
++        except Exception:
++            exceptions.handle(self.request,
++                              _('Unable to retrieve member details.'))
++
++    def get_context_data(self, **kwargs):
++        context = super(MemberDetailsView, self).get_context_data(**kwargs)
++        member = self.get_data()
++        context['member'] = member
++        table = project_tables.MembersTable(self.request)
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(member)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        member = self.get_data()
++        return self.tab_group_class(request, member=member, **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy("horizon:project:loadbalancers:index")
++
+ 
+ class MonitorDetailsView(tabs.TabView):
+-    tab_group_class = (project_tabs.MonitorDetailsTabs)
++    tab_group_class = project_tabs.MonitorDetailsTabs
+     template_name = 'project/loadbalancers/details_tabs.html'
+ 
++    @memoized.memoized_method
++    def get_data(self):
++        mid = self.kwargs['monitor_id']
++        try:
++            return api.lbaas.pool_health_monitor_get(self.request, mid)
++        except Exception:
++            exceptions.handle(self.request,
++                              _('Unable to retrieve monitor details.'))
++
++    def get_context_data(self, **kwargs):
++        context = super(MonitorDetailsView, self).get_context_data(**kwargs)
++        monitor = self.get_data()
++        context['monitor'] = monitor
++        table = project_tables.MonitorsTable(self.request)
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(monitor)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        monitor = self.get_data()
++        return self.tab_group_class(request, monitor=monitor, **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy("horizon:project:loadbalancers:index")
++
+ 
+ class UpdatePoolView(forms.ModalFormView):
+     form_class = project_forms.UpdatePool
+diff --git a/openstack_dashboard/dashboards/project/networks/ports/tabs.py b/openstack_dashboard/dashboards/project/networks/ports/tabs.py
+index b4b4183..5e12e0d 100644
+--- a/openstack_dashboard/dashboards/project/networks/ports/tabs.py
++++ b/openstack_dashboard/dashboards/project/networks/ports/tabs.py
+@@ -12,14 +12,10 @@
+ #    License for the specific language governing permissions and limitations
+ #    under the License.
+ 
+-from django.core.urlresolvers import reverse
+ from django.utils.translation import ugettext_lazy as _
+ 
+-from horizon import exceptions
+ from horizon import tabs
+ 
+-from openstack_dashboard import api
+-
+ 
+ class OverviewTab(tabs.Tab):
+     name = _("Overview")
+@@ -27,16 +23,7 @@ class OverviewTab(tabs.Tab):
+     template_name = "project/networks/ports/_detail_overview.html"
+ 
+     def get_context_data(self, request):
+-        port_id = self.tab_group.kwargs['port_id']
+-        try:
+-            port = api.neutron.port_get(self.request, port_id)
+-        except Exception:
+-            redirect = reverse('horizon:project:networks:index')
+-            msg = _('Unable to retrieve port details.')
+-            exceptions.handle(request, msg, redirect=redirect)
+-        if (api.neutron.is_extension_supported(request, 'mac-learning') and
+-                not hasattr(port, 'mac_state')):
+-            port.mac_state = api.neutron.OFF_STATE
++        port = self.tab_group.kwargs['port']
+         return {'port': port}
+ 
+ 
+diff --git a/openstack_dashboard/dashboards/project/networks/ports/views.py b/openstack_dashboard/dashboards/project/networks/ports/views.py
+index 39fe073..9d0e7c7 100644
+--- a/openstack_dashboard/dashboards/project/networks/ports/views.py
++++ b/openstack_dashboard/dashboards/project/networks/ports/views.py
+@@ -25,6 +25,8 @@ from openstack_dashboard import api
+ from openstack_dashboard.dashboards.project.networks.ports \
+     import forms as project_forms
+ from openstack_dashboard.dashboards.project.networks.ports \
++    import tables as project_tables
++from openstack_dashboard.dashboards.project.networks.ports \
+     import tabs as project_tabs
+ 
+ 
+@@ -32,6 +34,42 @@ class DetailView(tabs.TabView):
+     tab_group_class = project_tabs.PortDetailTabs
+     template_name = 'project/networks/ports/detail.html'
+ 
++    @memoized.memoized_method
++    def get_data(self):
++        port_id = self.kwargs['port_id']
++
++        try:
++            port = api.neutron.port_get(self.request, port_id)
++        except Exception:
++            port = []
++            redirect = self.get_redirect_url()
++            msg = _('Unable to retrieve port details.')
++            exceptions.handle(self.request, msg, redirect=redirect)
++
++        if (api.neutron.is_extension_supported(self.request, 'mac-learning')
++                and not hasattr(port, 'mac_state')):
++            port.mac_state = api.neutron.OFF_STATE
++
++        return port
++
++    def get_context_data(self, **kwargs):
++        context = super(DetailView, self).get_context_data(**kwargs)
++        port = self.get_data()
++        table = project_tables.PortsTable(self.request,
++                                          network_id=port.network_id)
++        context["port"] = port
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(port)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        port = self.get_data()
++        return self.tab_group_class(request, port=port, **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse('horizon:project:networks:index')
++
+ 
+ class UpdateView(forms.ModalFormView):
+     form_class = project_forms.UpdatePort
+diff --git a/openstack_dashboard/dashboards/project/networks/subnets/tabs.py b/openstack_dashboard/dashboards/project/networks/subnets/tabs.py
+index 049d6bd..1052e8c 100644
+--- a/openstack_dashboard/dashboards/project/networks/subnets/tabs.py
++++ b/openstack_dashboard/dashboards/project/networks/subnets/tabs.py
+@@ -12,15 +12,10 @@
+ #    License for the specific language governing permissions and limitations
+ #    under the License.
+ 
+-from django.core.urlresolvers import reverse
+ from django.utils.translation import ugettext_lazy as _
+ 
+-from horizon import exceptions
+ from horizon import tabs
+ 
+-from openstack_dashboard import api
+-from openstack_dashboard.dashboards.project.networks.subnets import utils
+-
+ 
+ class OverviewTab(tabs.Tab):
+     name = _("Overview")
+@@ -28,17 +23,7 @@ class OverviewTab(tabs.Tab):
+     template_name = "project/networks/subnets/_detail_overview.html"
+ 
+     def get_context_data(self, request):
+-        subnet_id = self.tab_group.kwargs['subnet_id']
+-        try:
+-            subnet = api.neutron.subnet_get(self.request, subnet_id)
+-        except Exception:
+-            redirect = reverse('horizon:project:networks:index')
+-            msg = _('Unable to retrieve subnet details.')
+-            exceptions.handle(request, msg, redirect=redirect)
+-        if subnet.ip_version == 6:
+-            ipv6_modes = utils.get_ipv6_modes_menu_from_attrs(
+-                subnet.ipv6_ra_mode, subnet.ipv6_address_mode)
+-            subnet.ipv6_modes_desc = utils.IPV6_MODE_MAP.get(ipv6_modes)
++        subnet = self.tab_group.kwargs['subnet']
+         return {'subnet': subnet}
+ 
+ 
+diff --git a/openstack_dashboard/dashboards/project/networks/subnets/views.py b/openstack_dashboard/dashboards/project/networks/subnets/views.py
+index 5c28d51..8d4b5e0 100644
+--- a/openstack_dashboard/dashboards/project/networks/subnets/views.py
++++ b/openstack_dashboard/dashboards/project/networks/subnets/views.py
+@@ -26,6 +26,8 @@ from horizon import workflows
+ from openstack_dashboard import api
+ 
+ from openstack_dashboard.dashboards.project.networks.subnets \
++    import tables as project_tables
++from openstack_dashboard.dashboards.project.networks.subnets \
+     import tabs as project_tabs
+ from openstack_dashboard.dashboards.project.networks.subnets import utils
+ from openstack_dashboard.dashboards.project.networks.subnets \
+@@ -99,3 +101,39 @@ class UpdateView(workflows.WorkflowView):
+ class DetailView(tabs.TabView):
+     tab_group_class = project_tabs.SubnetDetailTabs
+     template_name = 'project/networks/subnets/detail.html'
++
++    @memoized.memoized_method
++    def get_data(self):
++        subnet_id = self.kwargs['subnet_id']
++        try:
++            subnet = api.neutron.subnet_get(self.request, subnet_id)
++        except Exception:
++            subnet = []
++            msg = _('Unable to retrieve subnet details.')
++            exceptions.handle(self.request, msg,
++                              redirect=self.get_redirect_url())
++        else:
++            if subnet.ip_version == 6:
++                ipv6_modes = utils.get_ipv6_modes_menu_from_attrs(
++                    subnet.ipv6_ra_mode, subnet.ipv6_address_mode)
++                subnet.ipv6_modes_desc = utils.IPV6_MODE_MAP.get(ipv6_modes)
++
++        return subnet
++
++    def get_context_data(self, **kwargs):
++        context = super(DetailView, self).get_context_data(**kwargs)
++        subnet = self.get_data()
++        table = project_tables.SubnetsTable(self.request,
++                                            network_id=subnet.network_id)
++        context["subnet"] = subnet
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(subnet)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        subnet = self.get_data()
++        return self.tab_group_class(request, subnet=subnet, **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse('horizon:project:networks:index')
+diff --git a/openstack_dashboard/dashboards/project/networks/tests.py b/openstack_dashboard/dashboards/project/networks/tests.py
+index 802f5df..221431d 100644
+--- a/openstack_dashboard/dashboards/project/networks/tests.py
++++ b/openstack_dashboard/dashboards/project/networks/tests.py
+@@ -1609,6 +1609,9 @@ class NetworkPortTests(test.TestCase):
+         api.neutron.is_extension_supported(IsA(http.HttpRequest),
+                                            'mac-learning')\
+             .AndReturn(mac_learning)
++        api.neutron.is_extension_supported(IsA(http.HttpRequest),
++                                           'mac-learning')\
++            .AndReturn(mac_learning)
+         self.mox.ReplayAll()
+ 
+         res = self.client.get(reverse('horizon:project:networks:ports:detail',
+diff --git a/openstack_dashboard/dashboards/project/networks/views.py b/openstack_dashboard/dashboards/project/networks/views.py
+index d4a3613..0337322 100644
+--- a/openstack_dashboard/dashboards/project/networks/views.py
++++ b/openstack_dashboard/dashboards/project/networks/views.py
+@@ -95,7 +95,6 @@ class UpdateView(forms.ModalFormView):
+ class DetailView(tables.MultiTableView):
+     table_classes = (subnet_tables.SubnetsTable, port_tables.PortsTable)
+     template_name = 'project/networks/detail.html'
+-    failure_url = reverse_lazy('horizon:project:networks:index')
+ 
+     def get_subnets_data(self):
+         try:
+@@ -131,10 +130,19 @@ class DetailView(tables.MultiTableView):
+         except Exception:
+             msg = _('Unable to retrieve details for network "%s".') \
+                 % (network_id)
+-            exceptions.handle(self.request, msg, redirect=self.failure_url)
++            exceptions.handle(self.request, msg,
++                              redirect=self.get_redirect_url())
+         return network
+ 
+     def get_context_data(self, **kwargs):
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["network"] = self._get_data()
++        network = self._get_data()
++        context["network"] = network
++        table = project_tables.NetworksTable(self.request)
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(network)
+         return context
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy('horizon:project:networks:index')
+diff --git a/openstack_dashboard/dashboards/project/routers/views.py b/openstack_dashboard/dashboards/project/routers/views.py
+index 62ed37a..3bca6af 100644
+--- a/openstack_dashboard/dashboards/project/routers/views.py
++++ b/openstack_dashboard/dashboards/project/routers/views.py
+@@ -125,11 +125,17 @@ class DetailView(tabs.TabbedTableView):
+ 
+     def get_context_data(self, **kwargs):
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["router"] = self._get_data()
++        router = self._get_data()
++        table = rtables.RoutersTable(self.request)
++
++        context["router"] = router
++        context["url"] = self.failure_url
++        context["actions"] = table.render_row_actions(router)
+         context['dvr_supported'] = api.neutron.get_feature_permission(
+             self.request, "dvr", "get")
+         context['ha_supported'] = api.neutron.get_feature_permission(
+             self.request, "l3-ha", "get")
++
+         return context
+ 
+     def get(self, request, *args, **kwargs):
+diff --git a/openstack_dashboard/dashboards/project/stacks/views.py b/openstack_dashboard/dashboards/project/stacks/views.py
+index e43d9db..53b9bc5 100644
+--- a/openstack_dashboard/dashboards/project/stacks/views.py
++++ b/openstack_dashboard/dashboards/project/stacks/views.py
+@@ -197,7 +197,11 @@ class DetailView(tabs.TabView):
+ 
+     def get_context_data(self, **kwargs):
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["stack"] = self.get_data(self.request, **kwargs)
++        stack = self.get_data(self.request, **kwargs)
++        table = project_tables.StacksTable(self.request)
++        context["stack"] = stack
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(stack)
+         return context
+ 
+     @memoized.memoized_method
+@@ -210,13 +214,16 @@ class DetailView(tabs.TabView):
+             return stack
+         except Exception:
+             msg = _("Unable to retrieve stack.")
+-            redirect = reverse('horizon:project:stacks:index')
+-            exceptions.handle(request, msg, redirect=redirect)
++            exceptions.handle(request, msg, redirect=self.get_redirect_url())
+ 
+     def get_tabs(self, request, **kwargs):
+         stack = self.get_data(request, **kwargs)
+         return self.tab_group_class(request, stack=stack, **kwargs)
+ 
++    @staticmethod
++    def get_redirect_url():
++        return reverse('horizon:project:stacks:index')
++
+ 
+ class ResourceView(tabs.TabView):
+     tab_group_class = project_tabs.ResourceDetailTabs
+diff --git a/openstack_dashboard/dashboards/project/volumes/backups/views.py b/openstack_dashboard/dashboards/project/volumes/backups/views.py
+index a04fee8..7a1eb9e 100644
+--- a/openstack_dashboard/dashboards/project/volumes/backups/views.py
++++ b/openstack_dashboard/dashboards/project/volumes/backups/views.py
+@@ -23,6 +23,8 @@ from openstack_dashboard import api
+ from openstack_dashboard.dashboards.project.volumes.backups \
+     import forms as backup_forms
+ from openstack_dashboard.dashboards.project.volumes.backups \
++    import tables as backup_tables
++from openstack_dashboard.dashboards.project.volumes.backups \
+     import tabs as backup_tabs
+ 
+ 
+@@ -46,7 +48,12 @@ class BackupDetailView(tabs.TabView):
+ 
+     def get_context_data(self, **kwargs):
+         context = super(BackupDetailView, self).get_context_data(**kwargs)
+-        context["backup"] = self.get_data()
++        backup = self.get_data()
++        table = backup_tables.BackupsTable(self.request)
++        context["backup"] = backup
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(backup)
++
+         return context
+ 
+     @memoized.memoized_method
+@@ -56,16 +63,19 @@ class BackupDetailView(tabs.TabView):
+             backup = api.cinder.volume_backup_get(self.request,
+                                                   backup_id)
+         except Exception:
+-            redirect = reverse('horizon:project:volumes:index')
+             exceptions.handle(self.request,
+                               _('Unable to retrieve backup details.'),
+-                              redirect=redirect)
++                              redirect=self.get_redirect_url())
+         return backup
+ 
+     def get_tabs(self, request, *args, **kwargs):
+         backup = self.get_data()
+         return self.tab_group_class(request, backup=backup, **kwargs)
+ 
++    @staticmethod
++    def get_redirect_url():
++        return reverse('horizon:project:volumes:index')
++
+ 
+ class RestoreBackupView(forms.ModalFormView):
+     form_class = backup_forms.RestoreBackupForm
+diff --git a/openstack_dashboard/dashboards/project/volumes/snapshots/views.py b/openstack_dashboard/dashboards/project/volumes/snapshots/views.py
+index 4b50660..2e2227a 100644
+--- a/openstack_dashboard/dashboards/project/volumes/snapshots/views.py
++++ b/openstack_dashboard/dashboards/project/volumes/snapshots/views.py
+@@ -24,6 +24,8 @@ from openstack_dashboard import api
+ from openstack_dashboard.dashboards.project.volumes \
+     .snapshots import forms as vol_snapshot_forms
+ from openstack_dashboard.dashboards.project.volumes \
++    .snapshots import tables as vol_snapshot_tables
++from openstack_dashboard.dashboards.project.volumes \
+     .snapshots import tabs as vol_snapshot_tabs
+ 
+ 
+@@ -62,7 +64,11 @@ class DetailView(tabs.TabView):
+ 
+     def get_context_data(self, **kwargs):
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["snapshot"] = self.get_data()
++        snapshot = self.get_data()
++        table = vol_snapshot_tables.VolumeSnapshotsTable(self.request)
++        context["snapshot"] = snapshot
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(snapshot)
+         return context
+ 
+     @memoized.memoized_method
+@@ -78,7 +84,8 @@ class DetailView(tabs.TabView):
+                               redirect=redirect)
+         return snapshot
+ 
+-    def get_redirect_url(self):
++    @staticmethod
++    def get_redirect_url():
+         return reverse('horizon:project:volumes:index')
+ 
+     def get_tabs(self, request, *args, **kwargs):
+diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/volumes/views.py
+index c78cd09..81449ec 100644
+--- a/openstack_dashboard/dashboards/project/volumes/volumes/views.py
++++ b/openstack_dashboard/dashboards/project/volumes/volumes/views.py
+@@ -44,12 +44,12 @@ class DetailView(tabs.TabView):
+     template_name = 'project/volumes/volumes/detail.html'
+ 
+     def get_context_data(self, **kwargs):
+-        datum = self.get_data()
+-        table = project_tables.VolumesTable(self.request)
+         context = super(DetailView, self).get_context_data(**kwargs)
+-        context["volume"] = datum
++        volume = self.get_data()
++        table = project_tables.VolumesTable(self.request)
++        context["volume"] = volume
+         context["url"] = self.get_redirect_url()
+-        context["actions"] = table.render_row_actions(datum)
++        context["actions"] = table.render_row_actions(volume)
+         return context
+ 
+     @memoized.memoized_method
+diff --git a/openstack_dashboard/dashboards/project/vpn/tabs.py b/openstack_dashboard/dashboards/project/vpn/tabs.py
+index dd836c8..f5d5f8e 100644
+--- a/openstack_dashboard/dashboards/project/vpn/tabs.py
++++ b/openstack_dashboard/dashboards/project/vpn/tabs.py
+@@ -15,7 +15,6 @@
+ # @author: Tatiana Mazur
+ 
+ 
+-from django.core.urlresolvers import reverse_lazy
+ from django.utils.translation import ugettext_lazy as _
+ 
+ from horizon import exceptions
+@@ -117,15 +116,9 @@ class IKEPolicyDetailsTab(tabs.Tab):
+     name = _("IKE Policy Details")
+     slug = "ikepolicydetails"
+     template_name = "project/vpn/_ikepolicy_details.html"
+-    failure_url = reverse_lazy('horizon:project:vpn:index')
+ 
+     def get_context_data(self, request):
+-        pid = self.tab_group.kwargs['ikepolicy_id']
+-        try:
+-            ikepolicy = api.vpn.ikepolicy_get(request, pid)
+-        except Exception:
+-            msg = _('Unable to retrieve IKE Policy details.')
+-            exceptions.handle(request, msg, redirect=self.failure_url)
++        ikepolicy = self.tab_group.kwargs['ikepolicy']
+         return {'ikepolicy': ikepolicy}
+ 
+ 
+@@ -138,15 +131,9 @@ class IPSecPolicyDetailsTab(tabs.Tab):
+     name = _("IPSec Policy Details")
+     slug = "ipsecpolicydetails"
+     template_name = "project/vpn/_ipsecpolicy_details.html"
+-    failure_url = reverse_lazy('horizon:project:vpn:index')
+ 
+     def get_context_data(self, request):
+-        pid = self.tab_group.kwargs['ipsecpolicy_id']
+-        try:
+-            ipsecpolicy = api.vpn.ipsecpolicy_get(request, pid)
+-        except Exception:
+-            msg = _('Unable to retrieve IPSec Policy details.')
+-            exceptions.handle(request, msg, redirect=self.failure_url)
++        ipsecpolicy = self.tab_group.kwargs['ipsecpolicy']
+         return {'ipsecpolicy': ipsecpolicy}
+ 
+ 
+@@ -159,21 +146,9 @@ class VPNServiceDetailsTab(tabs.Tab):
+     name = _("VPN Service Details")
+     slug = "vpnservicedetails"
+     template_name = "project/vpn/_vpnservice_details.html"
+-    failure_url = reverse_lazy('horizon:project:vpn:index')
+ 
+     def get_context_data(self, request):
+-        sid = self.tab_group.kwargs['vpnservice_id']
+-        try:
+-            vpnservice = api.vpn.vpnservice_get(request, sid)
+-        except Exception:
+-            msg = _('Unable to retrieve VPN Service details.')
+-            exceptions.handle(request, msg, redirect=self.failure_url)
+-        try:
+-            connections = api.vpn.ipsecsiteconnection_list(
+-                request, vpnservice_id=sid)
+-            vpnservice.vpnconnections = connections
+-        except Exception:
+-            vpnservice.vpnconnections = []
++        vpnservice = self.tab_group.kwargs['vpnservice']
+         return {'vpnservice': vpnservice}
+ 
+ 
+@@ -186,16 +161,10 @@ class IPSecSiteConnectionDetailsTab(tabs.Tab):
+     name = _("IPSec Site Connection Details")
+     slug = "ipsecsiteconnectiondetails"
+     template_name = "project/vpn/_ipsecsiteconnection_details.html"
+-    failure_url = reverse_lazy('horizon:project:vpn:index')
+ 
+     def get_context_data(self, request):
+-        cid = self.tab_group.kwargs['ipsecsiteconnection_id']
+-        try:
+-            ipsecsiteconn = api.vpn.ipsecsiteconnection_get(request, cid)
+-        except Exception:
+-            msg = _('Unable to retrieve IPSec Site Connection details.')
+-            exceptions.handle(request, msg, redirect=self.failure_url)
+-        return {'ipsecsiteconnection': ipsecsiteconn}
++        ipsecsiteconnection = self.tab_group.kwargs['ipsecsiteconnection']
++        return {'ipsecsiteconnection': ipsecsiteconnection}
+ 
+ 
+ class IPSecSiteConnectionDetailsTabs(tabs.TabGroup):
+diff --git a/openstack_dashboard/dashboards/project/vpn/views.py b/openstack_dashboard/dashboards/project/vpn/views.py
+index 53e9f63..7dc22da 100644
+--- a/openstack_dashboard/dashboards/project/vpn/views.py
++++ b/openstack_dashboard/dashboards/project/vpn/views.py
+@@ -29,6 +29,7 @@ from openstack_dashboard import api
+ 
+ from openstack_dashboard.dashboards.project.vpn \
+     import forms as vpn_forms
++from openstack_dashboard.dashboards.project.vpn import tables as vpn_tables
+ from openstack_dashboard.dashboards.project.vpn import tabs as vpn_tabs
+ from openstack_dashboard.dashboards.project.vpn \
+     import workflows as vpn_workflows
+@@ -122,24 +123,146 @@ class AddIPSecPolicyView(workflows.WorkflowView):
+ 
+ 
+ class IKEPolicyDetailsView(tabs.TabView):
+-    tab_group_class = (vpn_tabs.IKEPolicyDetailsTabs)
++    tab_group_class = vpn_tabs.IKEPolicyDetailsTabs
+     template_name = 'project/vpn/details_tabs.html'
+ 
++    @memoized.memoized_method
++    def get_data(self):
++        pid = self.kwargs['ikepolicy_id']
++        try:
++            return api.vpn.ikepolicy_get(self.request, pid)
++        except Exception:
++            msg = _('Unable to retrieve IKE Policy details.')
++            exceptions.handle(self.request, msg,
++                              redirect=self.get_redirect_url())
++
++    def get_context_data(self, **kwargs):
++        context = super(IKEPolicyDetailsView, self).get_context_data(**kwargs)
++        ikepolicy = self.get_data()
++        table = vpn_tables.IKEPoliciesTable(self.request)
++        context["ikepolicy"] = ikepolicy
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(ikepolicy)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        ikepolicy = self.get_data()
++        return self.tab_group_class(request, ikepolicy=ikepolicy, **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy('horizon:project:vpn:index')
++
+ 
+ class IPSecPolicyDetailsView(tabs.TabView):
+-    tab_group_class = (vpn_tabs.IPSecPolicyDetailsTabs)
++    tab_group_class = vpn_tabs.IPSecPolicyDetailsTabs
+     template_name = 'project/vpn/details_tabs.html'
+ 
++    @memoized.memoized_method
++    def get_data(self):
++        pid = self.kwargs['ipsecpolicy_id']
++        try:
++            return api.vpn.ipsecpolicy_get(self.request, pid)
++        except Exception:
++            msg = _('Unable to retrieve IPSec Policy details.')
++            exceptions.handle(self.request, msg,
++                              redirect=self.get_redirect_url())
++
++    def get_context_data(self, **kwargs):
++        context = super(IPSecPolicyDetailsView, self).get_context_data(
++            **kwargs)
++        ipsecpolicy = self.get_data()
++        table = vpn_tables.IPSecPoliciesTable(self.request)
++        context["ipsecpolicy"] = ipsecpolicy
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(ipsecpolicy)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        ipsecpolicy = self.get_data()
++        return self.tab_group_class(request, ipsecpolicy=ipsecpolicy, **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy('horizon:project:vpn:index')
++
+ 
+ class VPNServiceDetailsView(tabs.TabView):
+-    tab_group_class = (vpn_tabs.VPNServiceDetailsTabs)
++    tab_group_class = vpn_tabs.VPNServiceDetailsTabs
+     template_name = 'project/vpn/details_tabs.html'
+ 
++    @memoized.memoized_method
++    def get_data(self):
++        sid = self.kwargs['vpnservice_id']
++
++        try:
++            vpnservice = api.vpn.vpnservice_get(self.request, sid)
++        except Exception:
++            vpnservice = []
++            msg = _('Unable to retrieve VPN Service details.')
++            exceptions.handle(self.request, msg,
++                              redirect=self.get_redirect_url())
++        try:
++            connections = api.vpn.ipsecsiteconnection_list(
++                self.request, vpnservice_id=sid)
++            vpnservice.vpnconnections = connections
++        except Exception:
++            vpnservice.vpnconnections = []
++
++        return vpnservice
++
++    def get_context_data(self, **kwargs):
++        context = super(VPNServiceDetailsView, self).get_context_data(**kwargs)
++        vpnservice = self.get_data()
++        table = vpn_tables.VPNServicesTable(self.request)
++        context["vpnservice"] = vpnservice
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(vpnservice)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        vpnservice = self.get_data()
++        return self.tab_group_class(request, vpnservice=vpnservice, **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy('horizon:project:vpn:index')
++
+ 
+ class IPSecSiteConnectionDetailsView(tabs.TabView):
+-    tab_group_class = (vpn_tabs.IPSecSiteConnectionDetailsTabs)
++    tab_group_class = vpn_tabs.IPSecSiteConnectionDetailsTabs
+     template_name = 'project/vpn/details_tabs.html'
+ 
++    @memoized.memoized_method
++    def get_data(self):
++        cid = self.kwargs['ipsecsiteconnection_id']
++        try:
++            return api.vpn.ipsecsiteconnection_get(self.request, cid)
++        except Exception:
++            msg = _('Unable to retrieve IPSec Site Connection details.')
++            exceptions.handle(self.request, msg,
++                              redirect=self.get_redirect_url())
++
++    def get_context_data(self, **kwargs):
++        context = super(IPSecSiteConnectionDetailsView, self).get_context_data(
++            **kwargs)
++        ipsecsiteconnection = self.get_data()
++        table = vpn_tables.IPSecSiteConnectionsTable(self.request)
++        context["ipsecsiteconnection"] = ipsecsiteconnection
++        context["url"] = self.get_redirect_url()
++        context["actions"] = table.render_row_actions(ipsecsiteconnection)
++        return context
++
++    def get_tabs(self, request, *args, **kwargs):
++        ipsecsiteconnection = self.get_data()
++        return self.tab_group_class(request,
++                                    ipsecsiteconnection=ipsecsiteconnection,
++                                    **kwargs)
++
++    @staticmethod
++    def get_redirect_url():
++        return reverse_lazy('horizon:project:vpn:index')
++
+ 
+ class UpdateVPNServiceView(forms.ModalFormView):
+     form_class = vpn_forms.UpdateVPNService
diff --git a/0017-Add-support-for-row-actions-to-detail-pages.patch b/0017-Add-support-for-row-actions-to-detail-pages.patch
new file mode 100644
index 0000000..3f55fa8
--- /dev/null
+++ b/0017-Add-support-for-row-actions-to-detail-pages.patch
@@ -0,0 +1,205 @@
+From 726e5915295251bc7a3180efa753c5d6abb6f677 Mon Sep 17 00:00:00 2001
+From: Ana Krivokapic <akrivoka at redhat.com>
+Date: Thu, 11 Sep 2014 13:31:45 +0200
+Subject: [PATCH] Add support for row actions to detail pages
+
+Add support for performing actions on an object from the object's
+detail page. Actions are displayed in the page header, in a
+row of buttons. To use this newly added capability, `row=True`
+must be passed as a parameter to the render_row_actions method,
+when called from a DetailView.
+
+Change-Id: I4fd96433f9514242c21d6c47f79bf83f3a5e4fc0
+Implements: blueprint detail-pages-ia
+---
+ horizon/static/horizon/js/horizon.tables.js        |  6 +++--
+ horizon/tables/base.py                             | 17 +++++++++----
+ .../horizon/common/_data_table_row_action.html     |  5 ----
+ .../common/_data_table_row_action_dropdown.html    |  5 ++++
+ .../horizon/common/_data_table_row_action_row.html | 11 ++++++++
+ .../horizon/common/_data_table_row_actions.html    | 29 ----------------------
+ .../common/_data_table_row_actions_dropdown.html   | 27 ++++++++++++++++++++
+ .../common/_data_table_row_actions_row.html        |  8 ++++++
+ 8 files changed, 67 insertions(+), 41 deletions(-)
+ delete mode 100644 horizon/templates/horizon/common/_data_table_row_action.html
+ create mode 100644 horizon/templates/horizon/common/_data_table_row_action_dropdown.html
+ create mode 100644 horizon/templates/horizon/common/_data_table_row_action_row.html
+ delete mode 100644 horizon/templates/horizon/common/_data_table_row_actions.html
+ create mode 100644 horizon/templates/horizon/common/_data_table_row_actions_dropdown.html
+ create mode 100644 horizon/templates/horizon/common/_data_table_row_actions_row.html
+
+diff --git a/horizon/static/horizon/js/horizon.tables.js b/horizon/static/horizon/js/horizon.tables.js
+index 861f915..70c3d0a 100644
+--- a/horizon/static/horizon/js/horizon.tables.js
++++ b/horizon/static/horizon/js/horizon.tables.js
+@@ -138,8 +138,10 @@ horizon.datatables = {
+       var action_buttons = $(this).find(".table_actions button.btn-danger");
+ 
+       // Buttons should be enabled only if there are checked checkboxes
+-      action_buttons.toggleClass("disabled",
+-                                 !checkboxes.filter(":checked").length);
++      if (checkboxes.length) {
++        action_buttons.toggleClass("disabled",
++          !checkboxes.filter(":checked").length);
++        }
+     });
+   },
+ 
+diff --git a/horizon/tables/base.py b/horizon/tables/base.py
+index 2f76b79..7e5d1b4 100644
+--- a/horizon/tables/base.py
++++ b/horizon/tables/base.py
+@@ -982,8 +982,10 @@ class DataTableOptions(object):
+         self.template = getattr(options,
+                                 'template',
+                                 'horizon/common/_data_table.html')
+-        self.row_actions_template = \
+-            'horizon/common/_data_table_row_actions.html'
++        self.row_actions_dropdown_template = ('horizon/common/_data_table_'
++                                              'row_actions_dropdown.html')
++        self.row_actions_row_template = ('horizon/common/_data_table_'
++                                         'row_actions_row.html')
+         self.table_actions_template = \
+             'horizon/common/_data_table_table_actions.html'
+         self.context_var_name = unicode(getattr(options,
+@@ -1388,11 +1390,16 @@ class DataTable(object):
+         self.set_multiselect_column_visibility(len(bound_actions) > 0)
+         return table_actions_template.render(context)
+ 
+-    def render_row_actions(self, datum, pull_right=True):
++    def render_row_actions(self, datum, pull_right=True, row=False):
+         """Renders the actions specified in ``Meta.row_actions`` using the
+-        current row data.
++        current row data. If `row` is True, the actions are rendered in a row
++        of buttons. Otherwise they are rendered in a dropdown box.
+         """
+-        template_path = self._meta.row_actions_template
++        if row:
++            template_path = self._meta.row_actions_row_template
++        else:
++            template_path = self._meta.row_actions_dropdown_template
++
+         row_actions_template = template.loader.get_template(template_path)
+         bound_actions = self.get_row_actions(datum)
+         extra_context = {"row_actions": bound_actions,
+diff --git a/horizon/templates/horizon/common/_data_table_row_action.html b/horizon/templates/horizon/common/_data_table_row_action.html
+deleted file mode 100644
+index d766fb3..0000000
+--- a/horizon/templates/horizon/common/_data_table_row_action.html
++++ /dev/null
+@@ -1,5 +0,0 @@
+-{% if action.method != "GET" %}
+-  <button {{ action.attr_string|safe }} name="action" value="{{ action.table.name }}__{{ action.name }}__{{ row_id }}" type="submit">{{ action.verbose_name }}</button>
+-{% else %}
+-  <a href='{{ action.bound_url }}' {{ action.attr_string|safe }}>{{ action.verbose_name }}</a>
+-{% endif %}
+diff --git a/horizon/templates/horizon/common/_data_table_row_action_dropdown.html b/horizon/templates/horizon/common/_data_table_row_action_dropdown.html
+new file mode 100644
+index 0000000..d766fb3
+--- /dev/null
++++ b/horizon/templates/horizon/common/_data_table_row_action_dropdown.html
+@@ -0,0 +1,5 @@
++{% if action.method != "GET" %}
++  <button {{ action.attr_string|safe }} name="action" value="{{ action.table.name }}__{{ action.name }}__{{ row_id }}" type="submit">{{ action.verbose_name }}</button>
++{% else %}
++  <a href='{{ action.bound_url }}' {{ action.attr_string|safe }}>{{ action.verbose_name }}</a>
++{% endif %}
+diff --git a/horizon/templates/horizon/common/_data_table_row_action_row.html b/horizon/templates/horizon/common/_data_table_row_action_row.html
+new file mode 100644
+index 0000000..8602439
+--- /dev/null
++++ b/horizon/templates/horizon/common/_data_table_row_action_row.html
+@@ -0,0 +1,11 @@
++{% if action.method != "GET" %}
++  <button {{ action.attr_string|safe }} name="action" value="{{ action.table.name }}__{{ action.name }}__{{ row_id }}" type="submit">
++    {% if action.icon != None %}<span class="glyphicon glyphicon-{{ action.icon }}"></span> {% endif %}
++    {{ action.verbose_name }}
++  </button>
++{% else %}
++  <a href='{{ action.bound_url }}' title='{{ action.verbose_name }}' {{ action.attr_string|safe }}>
++    {% if action.icon != None %}<span class="glyphicon glyphicon-{{ action.icon }}"></span> {% endif %}
++    {{ action.verbose_name }}
++  </a>
++{% endif %}
+diff --git a/horizon/templates/horizon/common/_data_table_row_actions.html b/horizon/templates/horizon/common/_data_table_row_actions.html
+deleted file mode 100644
+index 3f3368b..0000000
+--- a/horizon/templates/horizon/common/_data_table_row_actions.html
++++ /dev/null
+@@ -1,29 +0,0 @@
+-{% load horizon i18n %}
+-
+-{% spaceless %} {# This makes sure whitespace doesn't affect positioning for dropdown. #}
+-{% if row_actions|length > 1 %}
+-<div class="btn-group {% if pull_right %}pull-right{% endif %}">
+-    {% for action in row_actions %}
+-        {% if forloop.first %}
+-          {% include "horizon/common/_data_table_row_action.html" %}
+-          <a class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" href="#">
+-            <span class="caret"></span>
+-          </a>
+-          <ul class="dropdown-menu row_actions clearfix">
+-        {% else %}
+-          <li class="clearfix">
+-              {% include "horizon/common/_data_table_row_action.html" %}
+-          </li>
+-        {% endif %}
+-        {% if forloop.last %}
+-          </ul>
+-        {% endif %}
+-    {% endfor %}
+-</div>
+-{% endif %}
+-{% if row_actions|length == 1%}
+-  {% for action in row_actions %}
+-    {% include "horizon/common/_data_table_row_action.html" %}
+-  {% endfor %}
+-{% endif %}
+-{% endspaceless %}
+diff --git a/horizon/templates/horizon/common/_data_table_row_actions_dropdown.html b/horizon/templates/horizon/common/_data_table_row_actions_dropdown.html
+new file mode 100644
+index 0000000..781a30e
+--- /dev/null
++++ b/horizon/templates/horizon/common/_data_table_row_actions_dropdown.html
+@@ -0,0 +1,27 @@
++{% load horizon i18n %}
++
++{% spaceless %} {# This makes sure whitespace doesn't affect positioning for dropdown. #}
++{% if row_actions|length > 1 %}
++<div class="btn-group {% if pull_right %}pull-right{% endif %}">
++    {% for action in row_actions %}
++        {% if forloop.first %}
++          {% include "horizon/common/_data_table_row_action_dropdown.html" %}
++          <a class="btn btn-default btn-sm dropdown-toggle" data-toggle="dropdown" href="#">
++            <span class="caret"></span>
++          </a>
++          <ul class="dropdown-menu row_actions clearfix">
++        {% else %}
++          <li class="clearfix">
++              {% include "horizon/common/_data_table_row_action_dropdown.html" %}
++          </li>
++        {% endif %}
++        {% if forloop.last %}
++          </ul>
++        {% endif %}
++    {% endfor %}
++</div>
++{% endif %}
++{% if row_actions|length == 1%}
++  {% include "horizon/common/_data_table_row_action_dropdown.html" with action=row_actions.0%}
++{% endif %}
++{% endspaceless %}
+diff --git a/horizon/templates/horizon/common/_data_table_row_actions_row.html b/horizon/templates/horizon/common/_data_table_row_actions_row.html
+new file mode 100644
+index 0000000..dd2a5f0
+--- /dev/null
++++ b/horizon/templates/horizon/common/_data_table_row_actions_row.html
+@@ -0,0 +1,8 @@
++{% load i18n %}
++<div class="table_actions clearfix">
++{% block table_actions %}
++  {% for action in row_actions %}
++    {%  include "horizon/common/_data_table_row_action_row.html" %}
++  {% endfor %}
++{% endblock table_actions %}
++</div>
diff --git a/0018-Clean-up-test-output.patch b/0018-Clean-up-test-output.patch
new file mode 100644
index 0000000..b4b4d21
--- /dev/null
+++ b/0018-Clean-up-test-output.patch
@@ -0,0 +1,116 @@
+From ca952c4ce36bdcb0d02445c31255e974da81d407 Mon Sep 17 00:00:00 2001
+From: Ana Krivokapic <akrivoka at redhat.com>
+Date: Thu, 9 Oct 2014 15:20:09 +0200
+Subject: [PATCH] Clean up test output
+
+Some of the API calls weren't mocked properly. Add the missing mocks so
+the test output is again clean.
+
+Change-Id: I83fff8ee3f63e79ef5786602d6516703a22f0702
+Partial-bug: #1378907
+---
+ .../dashboards/admin/networks/tests.py             |  8 +++++--
+ .../dashboards/project/instances/tests.py          | 25 +++++++++++++++++-----
+ .../dashboards/project/networks/tests.py           |  8 +++++--
+ openstack_dashboard/test/test_data/cinder_data.py  |  1 +
+ 4 files changed, 33 insertions(+), 9 deletions(-)
+
+diff --git a/openstack_dashboard/dashboards/admin/networks/tests.py b/openstack_dashboard/dashboards/admin/networks/tests.py
+index e9f4890..2235c10 100644
+--- a/openstack_dashboard/dashboards/admin/networks/tests.py
++++ b/openstack_dashboard/dashboards/admin/networks/tests.py
+@@ -643,11 +643,15 @@ class NetworkTests(test.BaseAdminViewTests):
+ 
+ class NetworkSubnetTests(test.BaseAdminViewTests):
+ 
+-    @test.create_stubs({api.neutron: ('subnet_get',)})
++    @test.create_stubs({api.neutron: ('network_get', 'subnet_get',)})
+     def test_subnet_detail(self):
++        network = self.networks.first()
+         subnet = self.subnets.first()
++
++        api.neutron.network_get(IsA(http.HttpRequest), network.id)\
++            .AndReturn(network)
+         api.neutron.subnet_get(IsA(http.HttpRequest), subnet.id)\
+-            .AndReturn(self.subnets.first())
++            .AndReturn(subnet)
+ 
+         self.mox.ReplayAll()
+ 
+diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
+index 6ee921d..163af69 100644
+--- a/openstack_dashboard/dashboards/project/instances/tests.py
++++ b/openstack_dashboard/dashboards/project/instances/tests.py
+@@ -652,11 +652,20 @@ class InstanceTests(helpers.TestCase):
+ 
+         self.assertRedirectsNoFollow(res, INDEX_URL)
+ 
+-    @helpers.create_stubs({api.nova: ("server_get",
+-                                   "instance_volumes_list",
+-                                   "flavor_get"),
+-                        api.network: ("server_security_groups",
+-                                      "servers_update_addresses")})
++    @helpers.create_stubs({
++        api.nova: (
++            "server_get",
++            "instance_volumes_list",
++            "flavor_get",
++            "extension_supported"
++        ),
++        api.network: (
++            "server_security_groups",
++            "servers_update_addresses",
++            "floating_ip_simple_associate_supported",
++            "floating_ip_supported"
++        )
++    })
+     def _get_instance_details(self, server, qs=None,
+                               flavor_return=None, volumes_return=None,
+                               security_groups_return=None, ):
+@@ -683,6 +692,12 @@ class InstanceTests(helpers.TestCase):
+                 .AndReturn(flavor_return)
+         api.network.server_security_groups(IsA(http.HttpRequest), server.id) \
+                 .AndReturn(security_groups_return)
++        api.network.floating_ip_simple_associate_supported(
++            IsA(http.HttpRequest)).MultipleTimes().AndReturn(True)
++        api.network.floating_ip_supported(IsA(http.HttpRequest)) \
++            .MultipleTimes().AndReturn(True)
++        api.nova.extension_supported('AdminActions', IsA(http.HttpRequest)) \
++            .MultipleTimes().AndReturn(True)
+ 
+         self.mox.ReplayAll()
+ 
+diff --git a/openstack_dashboard/dashboards/project/networks/tests.py b/openstack_dashboard/dashboards/project/networks/tests.py
+index 221431d..0224dba 100644
+--- a/openstack_dashboard/dashboards/project/networks/tests.py
++++ b/openstack_dashboard/dashboards/project/networks/tests.py
+@@ -788,11 +788,15 @@ class NetworkTests(test.TestCase):
+ 
+ class NetworkSubnetTests(test.TestCase):
+ 
+-    @test.create_stubs({api.neutron: ('subnet_get',)})
++    @test.create_stubs({api.neutron: ('network_get', 'subnet_get',)})
+     def test_subnet_detail(self):
++        network = self.networks.first()
+         subnet = self.subnets.first()
++
++        api.neutron.network_get(IsA(http.HttpRequest), network.id)\
++            .AndReturn(network)
+         api.neutron.subnet_get(IsA(http.HttpRequest), subnet.id)\
+-            .AndReturn(self.subnets.first())
++            .AndReturn(subnet)
+ 
+         self.mox.ReplayAll()
+ 
+diff --git a/openstack_dashboard/test/test_data/cinder_data.py b/openstack_dashboard/test/test_data/cinder_data.py
+index a26f742..3a04e13 100644
+--- a/openstack_dashboard/test/test_data/cinder_data.py
++++ b/openstack_dashboard/test/test_data/cinder_data.py
+@@ -161,6 +161,7 @@ def data(TEST):
+ 
+     TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot))
+     TEST.cinder_volume_snapshots.add(api.cinder.VolumeSnapshot(snapshot2))
++    TEST.cinder_volume_snapshots.first()._volume = volume
+ 
+     volume_backup1 = vol_backups.VolumeBackup(vol_backups.
+                                               VolumeBackupManager(None),
diff --git a/python-django-horizon.spec b/python-django-horizon.spec
index 02c346e..ff9b897 100644
--- a/python-django-horizon.spec
+++ b/python-django-horizon.spec
@@ -25,18 +25,24 @@ Source5:    python-django-horizon-logrotate.conf
 #
 # patches_base=2014.2.rc2
 #
-Patch0001: 0001-change-lockfile-location-to-tmp-and-also-add-localho.patch
-Patch0002: 0002-Add-a-customization-module-based-on-RHOS.patch
-Patch0003: 0003-move-RBAC-policy-files-and-checks-to-etc-openstack-d.patch
-Patch0004: 0004-move-SECRET_KEY-secret_key_store-to-tmp.patch
-Patch0005: 0005-RCUE-navbar-and-login-screen.patch
-Patch0006: 0006-fix-flake8-issues.patch
-Patch0007: 0007-remove-runtime-dep-to-python-pbr.patch
-Patch0008: 0008-Add-Change-password-link-to-the-RCUE-theme.patch
-Patch0009: 0009-.less-replaced-in-rcue.patch
-Patch0010: 0010-Theme-fixes.patch
+Patch0001: 0001-disable-debug-move-web-root.patch
+Patch0002: 0002-change-lockfile-location-to-tmp-and-also-add-localho.patch
+Patch0003: 0003-Add-a-customization-module-based-on-RHOS.patch
+Patch0004: 0004-move-RBAC-policy-files-and-checks-to-etc-openstack-d.patch
+Patch0005: 0005-move-SECRET_KEY-secret_key_store-to-tmp.patch
+Patch0006: 0006-RCUE-navbar-and-login-screen.patch
+Patch0007: 0007-fix-flake8-issues.patch
+Patch0008: 0008-remove-runtime-dep-to-python-pbr.patch
+Patch0009: 0009-Add-Change-password-link-to-the-RCUE-theme.patch
+Patch0010: 0010-.less-replaced-in-rcue.patch
 Patch0011: 0011-re-add-lesscpy-to-compile-.less.patch
 Patch0012: 0012-Migration-of-LESS-to-SCSS-and-various-fixes.patch
+Patch0013: 0013-Remove-the-redundant-Settings-button-on-downstream-t.patch
+Patch0014: 0014-Change-page-header-heading-to-H1.patch
+Patch0015: 0015-Add-dropdown-actions-to-detail-page.patch
+Patch0016: 0016-Add-dropdown-actions-to-all-details-pages.patch
+Patch0017: 0017-Add-support-for-row-actions-to-detail-pages.patch
+Patch0018: 0018-Clean-up-test-output.patch
 
 #
 # BuildArch needs to be located below patches in the spec file. Don't ask!


More information about the scm-commits mailing list