diff --git a/doc/source/refstack.rst b/doc/source/refstack.rst
index 62282ea2..2451ce47 100644
--- a/doc/source/refstack.rst
+++ b/doc/source/refstack.rst
@@ -204,6 +204,17 @@ performed to upgrade the database to the latest revision:
Now it should be some revision number other than `None`.
+(Optional) Generate About Page Content
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The RefStack About page is populated with HTML templates generated from
+our RST documentation files. If you want this information displayed, then
+run the following command from the root of the project.
+
+``./tools/convert-docs.py -o ./refstack-ui/app/components/about/templates ./doc/source/*.rst``
+
+Ignore any unknown directive errors.
+
Start RefStack
^^^^^^^^^^^^^^
diff --git a/refstack-ui/app/app.js b/refstack-ui/app/app.js
index 6efa35ea..4d03f18a 100644
--- a/refstack-ui/app/app.js
+++ b/refstack-ui/app/app.js
@@ -41,7 +41,9 @@
}).
state('about', {
url: '/about',
- templateUrl: '/components/about/about.html'
+ templateUrl: '/components/about/about.html',
+ controller: 'AboutController as ctrl'
+
}).
state('guidelines', {
url: '/guidelines',
diff --git a/refstack-ui/app/assets/css/style.css b/refstack-ui/app/assets/css/style.css
index f3480929..eea5de35 100644
--- a/refstack-ui/app/assets/css/style.css
+++ b/refstack-ui/app/assets/css/style.css
@@ -226,3 +226,26 @@ a.glyphicon {
.modal-body .row {
margin-bottom: 10px;
}
+
+.about-sidebar {
+ width: 20%;
+ float: left;
+ padding-right: 2px;
+ padding-top: 25px;
+}
+
+.about-content {
+ width: 80%;
+ float: left;
+ padding-left: 5%;
+
+}
+
+.about-option {
+ padding: 5px 5px 5px 10px;
+}
+
+.about-active {
+ background: #f2f2f2;
+ border-left: 2px solid orange;
+}
\ No newline at end of file
diff --git a/refstack-ui/app/components/about/about.html b/refstack-ui/app/components/about/about.html
index d6ab9912..348318cd 100644
--- a/refstack-ui/app/components/about/about.html
+++ b/refstack-ui/app/components/about/about.html
@@ -1,31 +1,13 @@
-
RefStack Documentation
-
-RefStack is a source of tools for interoperability testing of OpenStack clouds.
-To learn more about RefStack, visit the links below.
-
-
- -
-
- About RefStack
-
- -
-
- How to upload test results to RefStack
-
- -
-
- Vendor and product management
-
- -
-
- Test result management
-
-
+
+
+
diff --git a/refstack-ui/app/components/about/aboutController.js b/refstack-ui/app/components/about/aboutController.js
new file mode 100644
index 00000000..868762b9
--- /dev/null
+++ b/refstack-ui/app/components/about/aboutController.js
@@ -0,0 +1,85 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function () {
+ 'use strict';
+
+ angular
+ .module('refstackApp')
+ .controller('AboutController', AboutController);
+
+ AboutController.$inject = ['$location'];
+
+ /**
+ * RefStack About Controller
+ * This controller handles the about page and the multiple templates
+ * associated to the page.
+ */
+ function AboutController($location) {
+ var ctrl = this;
+
+ ctrl.selectOption = selectOption;
+ ctrl.getHash = getHash;
+
+ ctrl.options = {
+ 'about' : {
+ 'title': 'About RefStack',
+ 'template': 'components/about/templates/README.html',
+ 'order': 1
+ },
+ 'uploading-your-results': {
+ 'title': 'Uploading Your Results',
+ 'template': 'components/about/templates/' +
+ 'uploading_private_results.html',
+ 'order': 2
+ },
+ 'managing-results': {
+ 'title': 'Managing Results',
+ 'template': 'components/about/templates/' +
+ 'test_result_management.html',
+ 'order': 3
+ },
+ 'vendors-and-products': {
+ 'title': 'Vendors and Products',
+ 'template': 'components/about/templates/vendor_product.html',
+ 'order': 4
+ }
+ };
+
+ /**
+ * Given an option key, mark it as selected and set the corresponding
+ * template and URL hash.
+ */
+ function selectOption(key) {
+ ctrl.selected = key;
+ ctrl.template = ctrl.options[key].template;
+ $location.hash(key);
+ }
+
+ /**
+ * Get the hash in the URL and select it if possible.
+ */
+ function getHash() {
+ var hash = $location.hash();
+ if (hash && hash in ctrl.options) {
+ ctrl.selectOption(hash);
+ }
+ else {
+ ctrl.selectOption('about');
+ }
+ }
+
+ ctrl.getHash();
+ }
+})();
diff --git a/refstack-ui/app/index.html b/refstack-ui/app/index.html
index 5a896c2e..0df25635 100644
--- a/refstack-ui/app/index.html
+++ b/refstack-ui/app/index.html
@@ -40,6 +40,7 @@
+
diff --git a/refstack-ui/tests/unit/ControllerSpec.js b/refstack-ui/tests/unit/ControllerSpec.js
index d9a0f968..fedf3dc7 100644
--- a/refstack-ui/tests/unit/ControllerSpec.js
+++ b/refstack-ui/tests/unit/ControllerSpec.js
@@ -58,6 +58,49 @@ describe('Refstack controllers', function () {
});
});
+ describe('AboutController', function () {
+ var $location, ctrl;
+
+ beforeEach(inject(function ($controller, _$location_) {
+ $location = _$location_;
+ ctrl = $controller('AboutController', {});
+ ctrl.options = {
+ 'about' : {
+ 'title': 'About RefStack',
+ 'template': 'about-template'
+ },
+ 'option1' : {
+ 'title': 'Option One',
+ 'template': 'template-1'
+ }
+ };
+ }));
+
+ it('should have a function to select an option',
+ function () {
+ ctrl.selectOption('option1');
+ expect(ctrl.selected).toBe('option1');
+ expect(ctrl.template).toBe('template-1');
+ expect($location.hash()).toBe('option1');
+ });
+
+ it('should have a function to get the URL hash and select it',
+ function () {
+ // Test existing option.
+ $location.url('/about#option1');
+ ctrl.getHash();
+ expect(ctrl.selected).toBe('option1');
+ expect(ctrl.template).toBe('template-1');
+
+ // Test nonexistent option.
+ $location.url('/about#foobar');
+ ctrl.getHash();
+ expect(ctrl.selected).toBe('about');
+ expect(ctrl.template).toBe('about-template');
+ });
+
+ });
+
describe('GuidelinesController', function () {
var ctrl;
diff --git a/requirements.txt b/requirements.txt
index e6a048f0..27625fa2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,9 @@
SQLAlchemy>=0.8.3
alembic==0.5.0
beaker==1.6.5.post1
+beautifulsoup4
cryptography>=1.0,!=1.3.0 # BSD/Apache-2.0
+docutils>=0.11
oslo.config>=1.6.0 # Apache-2.0
oslo.db>=1.4.1 # Apache-2.0
oslo.log>=3.11.0
diff --git a/tools/convert-docs.py b/tools/convert-docs.py
new file mode 100755
index 00000000..3678be66
--- /dev/null
+++ b/tools/convert-docs.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python
+# Copyright (c) 2017 IBM, Inc.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+Convert RST files to basic HTML. The primary use case is to provide a way
+to display RefStack documentation on the RefStack website.
+"""
+
+import argparse
+import glob
+import os
+
+from bs4 import BeautifulSoup
+from docutils.core import publish_file
+
+
+def extract_body(html):
+ """Extract the content of the body tags of an HTML string."""
+ soup = BeautifulSoup(html, "html.parser")
+ return ''.join(['%s' % str(a) for a in soup.body.contents])
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(
+ description='Convert RST files to basic HTML template files.'
+ )
+ parser.add_argument('files',
+ metavar='file',
+ nargs='+',
+ help='RST file(s) to be converted to HTML templates.')
+ parser.add_argument('-o', '--output_dir',
+ required=False,
+ help='The directory where template files should be '
+ 'output to. Defaults to the current directory.')
+ args = parser.parse_args()
+
+ if args.output_dir:
+ output_dir = args.output_dir
+ # If the output directory doesn't exist, create it.
+ if not os.path.exists(output_dir):
+ try:
+ os.makedirs(output_dir)
+ except OSError:
+ if not os.path.isdir(output_dir):
+ raise
+ else:
+ output_dir = os.getcwd()
+
+ for path in args.files:
+ for file in glob.glob(path):
+ base_file = os.path.splitext(os.path.basename(file))[0]
+
+ # Calling publish_file will also print to stdout. Destination path
+ # is set to /dev/null to suppress this.
+ html = publish_file(source_path=file,
+ destination_path='/dev/null',
+ writer_name='html',)
+ body = extract_body(html)
+
+ output_file = os.path.join(output_dir, base_file + ".html")
+ with open(output_file, "w") as template_file:
+ template_file.write(body)