From 2e042e4c4501d7239d877fbf0a4c6d4269ed6ab0 Mon Sep 17 00:00:00 2001 From: adrian-turjak Date: Wed, 22 Mar 2017 15:13:07 +1300 Subject: [PATCH] Initial code base for StackTask UI * Ported over from the Catalyst repos into its own plugin for better maintainability and ease of sharing with upstream. Change-Id: I4e7e1787330d549c59ede6f024e1216f5a073ecd --- .gitignore | 47 ++ .gitreview | 5 + LICENSE | 176 ++++++ MANIFEST.in | 7 + README.rst | 60 ++ babel-django.cfg | 5 + babel-djangojs.cfg | 14 + doc/Makefile | 153 +++++ doc/source/conf.py | 441 +++++++++++++ doc/source/index.rst | 71 +++ doc/source/releases/0.1.0.rst | 2 + manage.py | 23 + requirements.txt | 14 + run_tests.sh | 583 ++++++++++++++++++ setup.cfg | 29 + setup.py | 29 + stacktask_ui/__init__.py | 0 stacktask_ui/api/__init__.py | 0 stacktask_ui/api/stacktask.py | 239 +++++++ stacktask_ui/content/__init__.py | 0 stacktask_ui/content/default/__init__.py | 0 stacktask_ui/content/default/panel.py | 24 + .../default/templates/default/base.html | 3 + .../content/forgot_password/__init__.py | 0 stacktask_ui/content/forgot_password/forms.py | 55 ++ stacktask_ui/content/forgot_password/panel.py | 23 + .../templates/auth/_login_page.html | 7 + .../_forgot_password_form.html | 52 ++ .../templates/forgot_password/_index.html | 23 + .../templates/forgot_password/_sent.html | 20 + .../templates/forgot_password/_sent_text.html | 34 + .../templates/forgot_password/index.html | 11 + .../templates/forgot_password/sent.html | 9 + stacktask_ui/content/forgot_password/urls.py | 25 + stacktask_ui/content/forgot_password/views.py | 29 + .../content/project_users/__init__.py | 0 stacktask_ui/content/project_users/forms.py | 129 ++++ stacktask_ui/content/project_users/panel.py | 23 + stacktask_ui/content/project_users/tables.py | 181 ++++++ .../templates/project_users/_invite.html | 30 + .../templates/project_users/_update.html | 29 + .../templates/project_users/index.html | 11 + .../templates/project_users/invite.html | 7 + .../templates/project_users/update.html | 6 + stacktask_ui/content/project_users/urls.py | 28 + stacktask_ui/content/project_users/utils.py | 33 + stacktask_ui/content/project_users/views.py | 90 +++ stacktask_ui/content/token/__init__.py | 0 stacktask_ui/content/token/forms.py | 83 +++ stacktask_ui/content/token/panel.py | 24 + .../token/templates/token/_setpassword.html | 20 + .../templates/token/_setpassword_form.html | 52 ++ .../token/templates/token/_tokenconfirm.html | 20 + .../templates/token/_tokenconfirm_form.html | 38 ++ .../token/templates/token/setpassword.html | 12 + .../token/templates/token/tokenconfirm.html | 12 + stacktask_ui/content/token/urls.py | 25 + stacktask_ui/content/token/views.py | 138 +++++ stacktask_ui/dashboards/__init__.py | 0 .../dashboards/forgot_password_dash.py | 31 + stacktask_ui/dashboards/management.py | 29 + stacktask_ui/dashboards/token_dash.py | 31 + stacktask_ui/enabled/_6000_management.py | 7 + stacktask_ui/enabled/_6020_token.py | 8 + stacktask_ui/enabled/_6030_forgot_password.py | 9 + .../_6040_management_access_control_group.py | 8 + .../enabled/_6050_management_project_users.py | 9 + stacktask_ui/enabled/__init__.py | 0 stacktask_ui/karma.conf.js | 155 +++++ stacktask_ui/test/__init__.py | 0 stacktask_ui/test/api_tests/__init__.py | 0 stacktask_ui/test/api_tests/rest_api_tests.py | 28 + stacktask_ui/test/helpers.py | 20 + .../test/integration_tests/__init__.py | 0 stacktask_ui/test/settings.py | 38 ++ stacktask_ui/test/test_data.py | 18 + stacktask_ui/version.py | 15 + test-requirements.txt | 29 + test-shim.js | 97 +++ tools/install_venv.py | 71 +++ tools/install_venv_common.py | 172 ++++++ tools/with_venv.sh | 13 + tox.ini | 81 +++ 83 files changed, 4073 insertions(+) create mode 100644 .gitignore create mode 100644 .gitreview create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README.rst create mode 100644 babel-django.cfg create mode 100644 babel-djangojs.cfg create mode 100644 doc/Makefile create mode 100644 doc/source/conf.py create mode 100644 doc/source/index.rst create mode 100644 doc/source/releases/0.1.0.rst create mode 100755 manage.py create mode 100644 requirements.txt create mode 100755 run_tests.sh create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 stacktask_ui/__init__.py create mode 100644 stacktask_ui/api/__init__.py create mode 100644 stacktask_ui/api/stacktask.py create mode 100644 stacktask_ui/content/__init__.py create mode 100644 stacktask_ui/content/default/__init__.py create mode 100644 stacktask_ui/content/default/panel.py create mode 100644 stacktask_ui/content/default/templates/default/base.html create mode 100644 stacktask_ui/content/forgot_password/__init__.py create mode 100644 stacktask_ui/content/forgot_password/forms.py create mode 100644 stacktask_ui/content/forgot_password/panel.py create mode 100644 stacktask_ui/content/forgot_password/templates/auth/_login_page.html create mode 100644 stacktask_ui/content/forgot_password/templates/forgot_password/_forgot_password_form.html create mode 100644 stacktask_ui/content/forgot_password/templates/forgot_password/_index.html create mode 100644 stacktask_ui/content/forgot_password/templates/forgot_password/_sent.html create mode 100644 stacktask_ui/content/forgot_password/templates/forgot_password/_sent_text.html create mode 100644 stacktask_ui/content/forgot_password/templates/forgot_password/index.html create mode 100644 stacktask_ui/content/forgot_password/templates/forgot_password/sent.html create mode 100644 stacktask_ui/content/forgot_password/urls.py create mode 100644 stacktask_ui/content/forgot_password/views.py create mode 100644 stacktask_ui/content/project_users/__init__.py create mode 100644 stacktask_ui/content/project_users/forms.py create mode 100644 stacktask_ui/content/project_users/panel.py create mode 100644 stacktask_ui/content/project_users/tables.py create mode 100644 stacktask_ui/content/project_users/templates/project_users/_invite.html create mode 100644 stacktask_ui/content/project_users/templates/project_users/_update.html create mode 100644 stacktask_ui/content/project_users/templates/project_users/index.html create mode 100644 stacktask_ui/content/project_users/templates/project_users/invite.html create mode 100644 stacktask_ui/content/project_users/templates/project_users/update.html create mode 100644 stacktask_ui/content/project_users/urls.py create mode 100644 stacktask_ui/content/project_users/utils.py create mode 100644 stacktask_ui/content/project_users/views.py create mode 100644 stacktask_ui/content/token/__init__.py create mode 100644 stacktask_ui/content/token/forms.py create mode 100644 stacktask_ui/content/token/panel.py create mode 100644 stacktask_ui/content/token/templates/token/_setpassword.html create mode 100644 stacktask_ui/content/token/templates/token/_setpassword_form.html create mode 100644 stacktask_ui/content/token/templates/token/_tokenconfirm.html create mode 100644 stacktask_ui/content/token/templates/token/_tokenconfirm_form.html create mode 100644 stacktask_ui/content/token/templates/token/setpassword.html create mode 100644 stacktask_ui/content/token/templates/token/tokenconfirm.html create mode 100644 stacktask_ui/content/token/urls.py create mode 100644 stacktask_ui/content/token/views.py create mode 100644 stacktask_ui/dashboards/__init__.py create mode 100644 stacktask_ui/dashboards/forgot_password_dash.py create mode 100644 stacktask_ui/dashboards/management.py create mode 100644 stacktask_ui/dashboards/token_dash.py create mode 100644 stacktask_ui/enabled/_6000_management.py create mode 100644 stacktask_ui/enabled/_6020_token.py create mode 100644 stacktask_ui/enabled/_6030_forgot_password.py create mode 100644 stacktask_ui/enabled/_6040_management_access_control_group.py create mode 100644 stacktask_ui/enabled/_6050_management_project_users.py create mode 100644 stacktask_ui/enabled/__init__.py create mode 100644 stacktask_ui/karma.conf.js create mode 100644 stacktask_ui/test/__init__.py create mode 100644 stacktask_ui/test/api_tests/__init__.py create mode 100644 stacktask_ui/test/api_tests/rest_api_tests.py create mode 100644 stacktask_ui/test/helpers.py create mode 100644 stacktask_ui/test/integration_tests/__init__.py create mode 100644 stacktask_ui/test/settings.py create mode 100644 stacktask_ui/test/test_data.py create mode 100644 stacktask_ui/version.py create mode 100644 test-requirements.txt create mode 100644 test-shim.js create mode 100644 tools/install_venv.py create mode 100644 tools/install_venv_common.py create mode 100755 tools/with_venv.sh create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7830312 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +*.egg* +*.mo +*.pot +*.pyc +*.sw? +*.sqlite3 +*.lock +.environment_version +.selenium_log +.coverage* +.noseids +.DS_STORE +.DS_Store +/cover +coverage.xml +coverage-karma +nosetests.xml +pep8.txt +pylint.txt +# Files created by releasenotes build +releasenotes/build +reports +openstack_dashboard/local/* +!openstack_dashboard/local/local_settings.py.example +!openstack_dashboard/local/enabled/_50__settings.py.example +!openstack_dashboard/local/local_settings.d +openstack_dashboard/local/local_settings.d/* +!openstack_dashboard/local/local_settings.d/*.example +openstack_dashboard/test/.secret_key_store +openstack_dashboard/test/integration_tests/local-horizon.conf +openstack_dashboard/test/integration_tests/test_reports/ +openstack_dashboard/wsgi/horizon.wsgi +doc/build/ +/static/ +integration_tests_screenshots/ +.venv +.tox +node_modules +npm-debug.log +build +dist +AUTHORS +ChangeLog +tags +ghostdriver.log +.testrepository +.idea diff --git a/.gitreview b/.gitreview new file mode 100644 index 0000000..71beb1d --- /dev/null +++ b/.gitreview @@ -0,0 +1,5 @@ +[gerrit] +host=gerrit.dmz.wgtn.cat-it.co.nz +port=29418 +project=openstack-stacktask-ui.git +defaultbranch=master diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..44bc8a0 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,7 @@ +include setup.py + +recursive-include stacktask_ui/content/default/templates * +recursive-include stacktask_ui/content/forgotpassword/templates * +recursive-include stacktask_ui/content/project_users/templates * +recursive-include stacktask_ui/content/token/templates * +recursive-include stacktask_ui/static * diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b8f3b5b --- /dev/null +++ b/README.rst @@ -0,0 +1,60 @@ +============ +stacktask-ui +============ + +StackTask Dashboard + +* Free software: Apache license +* Source: https://github.com/catalyst/stacktask-ui + +Manual Installation +------------------- + +Begin by cloning the Horizon and StackTask UI repositories:: + + git clone https://github.com/openstack/horizon + git clone https://github.com/catalyst/stacktask-ui + +Create a virtual environment and install Horizon dependencies:: + + cd horizon + python tools/install_venv.py + +Set up your ``local_settings.py`` file:: + + cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py + +Open up the copied ``local_settings.py`` file in your preferred text +editor. You will want to customize several settings: + +- ``OPENSTACK_HOST`` should be configured with the hostname of your + OpenStack server. Verify that the ``OPENSTACK_KEYSTONE_URL`` and + ``OPENSTACK_KEYSTONE_DEFAULT_ROLE`` settings are correct for your + environment. (They should be correct unless you modified your + OpenStack server to change them.) +- ``OPENSTACK_REGISTRATION_URL`` should also be configured to point to + you StackTask server and version. + +You will also need to update the ``keystone_policy.json`` file in horizon with +the following lines:: + + "project_mod": "role:project_mod", + "project_admin": "role:project_admin", + "project_mod_or_admin": "rule:admin_required or rule:project_mod or rule:project_admin", + "project_admin_only": "rule:admin_required or rule:project_admin", + "identity:project_users_access": "rule:project_mod_or_admin", + +Install StackTask UI with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -e ../stacktask-ui/ + +And enable it in Horizon:: + + cp ../stacktask-ui/enabled/ openstack_dashboard/local/enabled + +To run horizon with the newly enabled StackTask UI plugin run:: + + ./run_tests.sh --runserver 0.0.0.0:8080 + +to have the application start on port 8080 and the horizon dashboard will be +available in your browser at http://localhost:8080/ diff --git a/babel-django.cfg b/babel-django.cfg new file mode 100644 index 0000000..ad09d34 --- /dev/null +++ b/babel-django.cfg @@ -0,0 +1,5 @@ +[extractors] +django = django_babel.extract:extract_django + +[python: **.py] +[django: templates/**.html] diff --git a/babel-djangojs.cfg b/babel-djangojs.cfg new file mode 100644 index 0000000..a8273b6 --- /dev/null +++ b/babel-djangojs.cfg @@ -0,0 +1,14 @@ +[extractors] +# We use a custom extractor to find translatable strings in AngularJS +# templates. The extractor is included in horizon.utils for now. +# See http://babel.pocoo.org/docs/messages/#referencing-extraction-methods for +# details on how this works. +angular = horizon.utils.babel_extract_angular:extract_angular + +[javascript: **.js] + +# We need to look into all static folders for HTML files. +# The **/static ensures that we also search within +# /openstack_dashboard/dashboards/XYZ/static which will ensure +# that plugins are also translated. +[angular: **/static/**.html] diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..efaeb8e --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,153 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Cisco.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Cisco.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Cisco" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Cisco" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..18dd72c --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,441 @@ +# 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. +# +# Horizon documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 27 11:38:59 2011. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +from __future__ import print_function + +import os +import subprocess +import sys + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..")) + +sys.path.insert(0, ROOT) + +# This is required for ReadTheDocs.org, but isn't a bad idea anyway. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', + 'openstack_dashboard.test.settings') + +from stacktask_ui \ + import version as stacktask_ui_ver + + +def write_autodoc_index(): + + def find_autodoc_modules(module_name, sourcedir): + """returns a list of modules in the SOURCE directory.""" + modlist = [] + os.chdir(os.path.join(sourcedir, module_name)) + print("SEARCHING %s" % sourcedir) + for root, dirs, files in os.walk("."): + for filename in files: + if filename == 'tests.py': + continue + if filename.endswith(".py"): + # remove the pieces of the root + elements = root.split(os.path.sep) + # replace the leading "." with the module name + elements[0] = module_name + # and get the base module name + base, extension = os.path.splitext(filename) + if not (base == "__init__"): + elements.append(base) + result = ".".join(elements) + # print result + modlist.append(result) + return modlist + + RSTDIR = os.path.abspath(os.path.join(BASE_DIR, "sourcecode")) + SRCS = [('stacktask_ui_ver', ROOT), ] + + EXCLUDED_MODULES = () + CURRENT_SOURCES = {} + + if not(os.path.exists(RSTDIR)): + os.mkdir(RSTDIR) + CURRENT_SOURCES[RSTDIR] = ['autoindex.rst'] + + INDEXOUT = open(os.path.join(RSTDIR, "autoindex.rst"), "w") + INDEXOUT.write(""" +================= +Source Code Index +================= + +.. contents:: + :depth: 1 + :local: + +""") + + for modulename, path in SRCS: + sys.stdout.write("Generating source documentation for %s\n" % + modulename) + INDEXOUT.write("\n%s\n" % modulename.capitalize()) + INDEXOUT.write("%s\n" % ("=" * len(modulename),)) + INDEXOUT.write(".. toctree::\n") + INDEXOUT.write(" :maxdepth: 1\n") + INDEXOUT.write("\n") + + MOD_DIR = os.path.join(RSTDIR, modulename) + CURRENT_SOURCES[MOD_DIR] = [] + if not(os.path.exists(MOD_DIR)): + os.mkdir(MOD_DIR) + for module in find_autodoc_modules(modulename, path): + if any([module.startswith(exclude) for exclude + in EXCLUDED_MODULES]): + print("Excluded module %s." % module) + continue + mod_path = os.path.join(path, *module.split(".")) + generated_file = os.path.join(MOD_DIR, "%s.rst" % module) + + INDEXOUT.write(" %s/%s\n" % (modulename, module)) + + # Find the __init__.py module if this is a directory + if os.path.isdir(mod_path): + source_file = ".".join((os.path.join(mod_path, "__init__"), + "py",)) + else: + source_file = ".".join((os.path.join(mod_path), "py")) + + CURRENT_SOURCES[MOD_DIR].append("%s.rst" % module) + # Only generate a new file if the source has changed or we don't + # have a doc file to begin with. + if not os.access(generated_file, os.F_OK) or ( + os.stat(generated_file).st_mtime < + os.stat(source_file).st_mtime): + print("Module %s updated, generating new documentation." + % module) + FILEOUT = open(generated_file, "w") + header = "The :mod:`%s` Module" % module + FILEOUT.write("%s\n" % ("=" * len(header),)) + FILEOUT.write("%s\n" % header) + FILEOUT.write("%s\n" % ("=" * len(header),)) + FILEOUT.write(".. automodule:: %s\n" % module) + FILEOUT.write(" :members:\n") + FILEOUT.write(" :undoc-members:\n") + FILEOUT.write(" :show-inheritance:\n") + FILEOUT.write(" :noindex:\n") + FILEOUT.close() + + INDEXOUT.close() + + # Delete auto-generated .rst files for sources which no longer exist + for directory, subdirs, files in list(os.walk(RSTDIR)): + for old_file in files: + if old_file not in CURRENT_SOURCES.get(directory, []): + print("Removing outdated file for %s" % old_file) + os.remove(os.path.join(directory, old_file)) + + +write_autodoc_index() + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ---------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. +# They can be extensions coming with Sphinx (named 'sphinx.ext.*') +# or your custom ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.viewcode', + 'oslosphinx', + ] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'StackTask UI' +copyright = u'2017, Catalyst IT Ltd' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = stacktask_ui_ver.version_info.version_string() +# The full version, including alpha/beta/rc tags. +release = stacktask_ui_ver.version_info.release_string() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['**/#*', '**~', '**/#*#'] + +# The reST default role (used for this markup: `text`) +# to use for all documents. +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +primary_domain = 'py' +nitpicky = False + + +# -- Options for HTML output -------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# html_theme_path = ['.'] +# html_theme = '_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +html_theme_options = { + "nosidebar": "false" +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +# html_last_updated_fmt = '%b %d, %Y' +git_cmd = ["git", "log", "--pretty=format:'%ad, commit %h'", "--date=local", + "-n1"] +html_last_updated_fmt = subprocess.check_output(git_cmd, + stdin=subprocess.PIPE) + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# html_additional_pages = {} + +# If false, no module index is generated. +# html_domain_indices = True + +# If false, no index is generated. +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Horizondoc' + + +# -- Options for LaTeX output ------------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # 'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass +# [howto/manual]). +latex_documents = [ + ('index', 'Horizon.tex', u'Horizon Documentation', + u'OpenStack Foundation', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +# latex_domain_indices = True + + +# -- Options for manual page output ------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', u'StackTask UI Documentation', + 'Documentation for the StackTask UI plugin to the OpenStack\ + Dashboard (Horizon)', + [u'OpenStack'], 1) +] + +# If true, show URL addresses after external links. +# man_show_urls = False + + +# -- Options for Texinfo output ----------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Horizon', u'Horizon Documentation', u'OpenStack', + 'Horizon', 'One line description of project.', 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# texinfo_appendices = [] + +# If false, no module index is generated. +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# texinfo_show_urls = 'footnote' + + +# -- Options for Epub output -------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'Horizon' +epub_author = u'OpenStack' +epub_publisher = u'OpenStack' +epub_copyright = u'2012, OpenStack' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +# epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +# epub_scheme = '' + +# The unique identifier of the text. This can be an ISBN number +# or the project homepage. +# epub_identifier = '' + +# A unique identification for the text. +# epub_uid = '' + +# A tuple containing the cover image and cover page html template filenames. +# epub_cover = () + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +# epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +# epub_post_files = [] + +# A list of files that should not be packed into the epub file. +# epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +# epub_tocdepth = 3 + +# Allow duplicate toc entries. +# epub_tocdup = True diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..b98ce02 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,71 @@ +============ +stacktask-ui +============ + +StackTask Dashboard + +* Free software: Apache license +* Source: https://github.com/catalyst/stacktask-ui + +Installation instructions +========================= + +Begin by cloning the Horizon and StackTask UI repositories:: + + git clone https://github.com/openstack/horizon + git clone https://github.com/catalyst/stacktask-ui + +Create a virtual environment and install Horizon dependencies:: + + cd horizon + python tools/install_venv.py + +Set up your ``local_settings.py`` file:: + + cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py + +Open up the copied ``local_settings.py`` file in your preferred text +editor. You will want to customize several settings: + +- ``OPENSTACK_HOST`` should be configured with the hostname of your + OpenStack server. Verify that the ``OPENSTACK_KEYSTONE_URL`` and + ``OPENSTACK_KEYSTONE_DEFAULT_ROLE`` settings are correct for your + environment. (They should be correct unless you modified your + OpenStack server to change them.) +- ``OPENSTACK_REGISTRATION_URL`` should also be configured to point to + you StackTask server and version. + +You will also need to update the ``keystone_policy.json`` file in horizon with +the following lines:: + + "project_mod": "role:project_mod", + "project_admin": "role:project_admin", + "project_mod_or_admin": "rule:admin_required or rule:project_mod or rule:project_admin", + "project_admin_only": "rule:admin_required or rule:project_admin", + "identity:project_users_access": "rule:project_mod_or_admin", + +Install StackTask UI with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -e ../stacktask-ui/ + +And enable it in Horizon:: + + cp ../stacktask-ui/enabled/ openstack_dashboard/local/enabled + +Release Notes +============= + +.. toctree:: + :glob: + :maxdepth: 1 + + releases/* + +Source Code Reference +===================== + +.. toctree:: + :glob: + :maxdepth: 1 + + sourcecode/autoindex diff --git a/doc/source/releases/0.1.0.rst b/doc/source/releases/0.1.0.rst new file mode 100644 index 0000000..2d9a147 --- /dev/null +++ b/doc/source/releases/0.1.0.rst @@ -0,0 +1,2 @@ +StackTask UI 0.1.1 +=============== diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..5818a6d --- /dev/null +++ b/manage.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# 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. + +import os +import sys + +from django.core.management import execute_from_command_line # noqa + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", + "openstack_dashboard.settings") + execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0ffc433 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +# Order matters to the pip dependency resolver, so sorting this file +# changes how packages are installed. New dependencies should be +# added in alphabetical order, however, some dependencies may need to +# be installed in a specific order. +# +# PBR should always appear first +pbr>=2.0.0 # Apache-2.0 +Babel>=2.3.4 # BSD +Django<1.9,>=1.8 # BSD +django-babel>=0.5.1 # BSD +django-overextends>=0.4.2 diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 0000000..976b686 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,583 @@ +#!/bin/bash + +set -o errexit + +function usage { + echo "Usage: $0 [OPTION]..." + echo "Run Horizon's test suite(s)" + echo "" + echo " -V, --virtual-env Always use virtualenv. Install automatically" + echo " if not present" + echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local" + echo " environment" + echo " -c, --coverage Generate reports using Coverage" + echo " -f, --force Force a clean re-build of the virtual" + echo " environment. Useful when dependencies have" + echo " been added." + echo " -m, --manage Run a Django management command." + echo " --makemessages Create/Update English translation files." + echo " --compilemessages Compile all translation files." + echo " --check-only Do not update translation files (--makemessages only)." + echo " --pseudo Pseudo translate a language." + echo " -p, --pep8 Just run pep8" + echo " -8, --pep8-changed []" + echo " Just run PEP8 and HACKING compliance check" + echo " on files changed since HEAD~1 (or )" + echo " -P, --no-pep8 Don't run pep8 by default" + echo " -t, --tabs Check for tab characters in files." + echo " -y, --pylint Just run pylint" + echo " -e, --eslint Just run eslint" + echo " -k, --karma Just run karma" + echo " -q, --quiet Run non-interactively. (Relatively) quiet." + echo " Implies -V if -N is not set." + echo " --only-selenium Run only the Selenium unit tests" + echo " --with-selenium Run unit tests including Selenium tests" + echo " --selenium-headless Run Selenium tests headless" + echo " --integration Run the integration tests (requires a running " + echo " OpenStack environment)" + echo " --runserver Run the Django development server for" + echo " openstack_dashboard in the virtual" + echo " environment." + echo " --docs Just build the documentation" + echo " --backup-environment Make a backup of the environment on exit" + echo " --restore-environment Restore the environment before running" + echo " --destroy-environment Destroy the environment and exit" + echo " -h, --help Print this usage message" + echo "" + echo "Note: with no options specified, the script will try to run the tests in" + echo " a virtual environment, If no virtualenv is found, the script will ask" + echo " if you would like to create one. If you prefer to run tests NOT in a" + echo " virtual environment, simply pass the -N option." + exit +} + +# DEFAULTS FOR RUN_TESTS.SH +# +root=`pwd -P` +venv=$root/.venv +venv_env_version=$venv/environments +with_venv=tools/with_venv.sh +included_dirs="stacktask_ui" + +always_venv=0 +backup_env=0 +command_wrapper="" +destroy=0 +force=0 +just_pep8=0 +just_pep8_changed=0 +no_pep8=0 +just_pylint=0 +just_docs=0 +just_tabs=0 +just_eslint=0 +just_karma=0 +never_venv=0 +quiet=0 +restore_env=0 +runserver=0 +only_selenium=0 +with_selenium=0 +selenium_headless=0 +integration=0 +testopts="" +testargs="" +with_coverage=0 +makemessages=0 +compilemessages=0 +check_only=0 +pseudo=0 +manage=0 + +# Jenkins sets a "JOB_NAME" variable, if it's not set, we'll make it "default" +[ "$JOB_NAME" ] || JOB_NAME="default" + +function process_option { + # If running manage command, treat the rest of options as arguments. + if [ $manage -eq 1 ]; then + testargs="$testargs $1" + return 0 + fi + + case "$1" in + -h|--help) usage;; + -V|--virtual-env) always_venv=1; never_venv=0;; + -N|--no-virtual-env) always_venv=0; never_venv=1;; + -p|--pep8) just_pep8=1;; + -8|--pep8-changed) just_pep8_changed=1;; + -P|--no-pep8) no_pep8=1;; + -y|--pylint) just_pylint=1;; + -e|--eslint) just_eslint=1;; + -k|--karma) just_karma=1;; + -f|--force) force=1;; + -t|--tabs) just_tabs=1;; + -q|--quiet) quiet=1;; + -c|--coverage) with_coverage=1;; + -m|--manage) manage=1;; + --makemessages) makemessages=1;; + --compilemessages) compilemessages=1;; + --check-only) check_only=1;; + --pseudo) pseudo=1;; + --only-selenium) only_selenium=1;; + --with-selenium) with_selenium=1;; + --selenium-headless) selenium_headless=1;; + --integration) integration=1;; + --docs) just_docs=1;; + --runserver) runserver=1;; + --backup-environment) backup_env=1;; + --restore-environment) restore_env=1;; + --destroy-environment) destroy=1;; + -*) testopts="$testopts $1";; + *) testargs="$testargs $1" + esac +} + +function run_management_command { + ${command_wrapper} python $root/manage.py $testopts $testargs +} + +function run_server { + echo "Starting Django development server..." + ${command_wrapper} python $root/manage.py runserver $testopts $testargs + echo "Server stopped." +} + +function run_pylint { + echo "Running pylint ..." + PYTHONPATH=$root ${command_wrapper} pylint --rcfile=.pylintrc -f parseable $included_dirs > pylint.txt || true + CODE=$? + grep Global -A2 pylint.txt + if [ $CODE -lt 32 ]; then + echo "Completed successfully." + exit 0 + else + echo "Completed with problems." + exit $CODE + fi +} + +function run_eslint { + echo "Running eslint ..." + if [ "`which npm`" == '' ] ; then + echo "npm is not present; please install, e.g. sudo apt-get install npm" + else + npm install + npm run lint + fi +} + +function run_karma { + echo "Running karma ..." + npm install + npm run test +} + +function warn_on_flake8_without_venv { + set +o errexit + ${command_wrapper} python -c "import hacking" 2>/dev/null + no_hacking=$? + set -o errexit + if [ $never_venv -eq 1 -a $no_hacking -eq 1 ]; then + echo "**WARNING**:" >&2 + echo "OpenStack hacking is not installed on your host. Its detection will be missed." >&2 + echo "Please install or use virtual env if you need OpenStack hacking detection." >&2 + fi +} + +function run_pep8 { + echo "Running flake8 ..." + warn_on_flake8_without_venv + DJANGO_SETTINGS_MODULE=openstack_dashboard.test.settings ${command_wrapper} flake8 +} + +function run_pep8_changed { + # NOTE(gilliard) We want use flake8 to check the entirety of every file that has + # a change in it. Unfortunately the --filenames argument to flake8 only accepts + # file *names* and there are no files named (eg) "nova/compute/manager.py". The + # --diff argument behaves surprisingly as well, because although you feed it a + # diff, it actually checks the file on disk anyway. + local base_commit=${testargs:-HEAD~1} + files=$(git diff --name-only $base_commit | tr '\n' ' ') + echo "Running flake8 on ${files}" + warn_on_flake8_without_venv + diff -u --from-file /dev/null ${files} | DJANGO_SETTINGS_MODULE=openstack_dashboard.test.settings ${command_wrapper} flake8 --diff + exit +} + +function run_sphinx { + echo "Building sphinx..." + DJANGO_SETTINGS_MODULE=openstack_dashboard.test.settings ${command_wrapper} python setup.py build_sphinx + echo "Build complete." +} + +function tab_check { + TAB_VIOLATIONS=`find $included_dirs -type f -regex ".*\.\(css\|js\|py\|html\)" -print0 | xargs -0 awk '/\t/' | wc -l` + if [ $TAB_VIOLATIONS -gt 0 ]; then + echo "TABS! $TAB_VIOLATIONS of them! Oh no!" + HORIZON_FILES=`find $included_dirs -type f -regex ".*\.\(css\|js\|py|\html\)"` + for TABBED_FILE in $HORIZON_FILES + do + TAB_COUNT=`awk '/\t/' $TABBED_FILE | wc -l` + if [ $TAB_COUNT -gt 0 ]; then + echo "$TABBED_FILE: $TAB_COUNT" + fi + done + fi + return $TAB_VIOLATIONS; +} + +function destroy_venv { + echo "Cleaning environment..." + echo "Removing virtualenv..." + rm -rf $venv + echo "Virtualenv removed." +} + +function environment_check { + echo "Checking environment." + if [ -f $venv_env_version ]; then + set +o errexit + cat requirements.txt test-requirements.txt | cmp $venv_env_version - > /dev/null + local env_check_result=$? + set -o errexit + if [ $env_check_result -eq 0 ]; then + # If the environment exists and is up-to-date then set our variables + command_wrapper="${root}/${with_venv}" + echo "Environment is up to date." + return 0 + fi + fi + + if [ $always_venv -eq 1 ]; then + install_venv + else + if [ ! -e ${venv} ]; then + echo -e "Environment not found. Install? (Y/n) \c" + else + echo -e "Your environment appears to be out of date. Update? (Y/n) \c" + fi + read update_env + if [ "x$update_env" = "xY" -o "x$update_env" = "x" -o "x$update_env" = "xy" ]; then + install_venv + else + # Set our command wrapper anyway. + command_wrapper="${root}/${with_venv}" + fi + fi +} + +function sanity_check { + # Anything that should be determined prior to running the tests, server, etc. + # Don't sanity-check anything environment-related in -N flag is set + if [ $never_venv -eq 0 ]; then + if [ ! -e ${venv} ]; then + echo "Virtualenv not found at $venv. Did install_venv.py succeed?" + exit 1 + fi + fi + # Remove .pyc files. This is sanity checking because they can linger + # after old files are deleted. + find . -name "*.pyc" -exec rm -rf {} \; +} + +function backup_environment { + if [ $backup_env -eq 1 ]; then + echo "Backing up environment \"$JOB_NAME\"..." + if [ ! -e ${venv} ]; then + echo "Environment not installed. Cannot back up." + return 0 + fi + if [ -d /tmp/.horizon_environment/$JOB_NAME ]; then + mv /tmp/.horizon_environment/$JOB_NAME /tmp/.horizon_environment/$JOB_NAME.old + rm -rf /tmp/.horizon_environment/$JOB_NAME + fi + mkdir -p /tmp/.horizon_environment/$JOB_NAME + cp -r $venv /tmp/.horizon_environment/$JOB_NAME/ + # Remove the backup now that we've completed successfully + rm -rf /tmp/.horizon_environment/$JOB_NAME.old + echo "Backup completed" + fi +} + +function restore_environment { + if [ $restore_env -eq 1 ]; then + echo "Restoring environment from backup..." + if [ ! -d /tmp/.horizon_environment/$JOB_NAME ]; then + echo "No backup to restore from." + return 0 + fi + + cp -r /tmp/.horizon_environment/$JOB_NAME/.venv ./ || true + echo "Environment restored successfully." + fi +} + +function install_venv { + # Install with install_venv.py + export PIP_DOWNLOAD_CACHE=${PIP_DOWNLOAD_CACHE-/tmp/.pip_download_cache} + export PIP_USE_MIRRORS=true + if [ $quiet -eq 1 ]; then + export PIP_NO_INPUT=true + fi + echo "Fetching new src packages..." + rm -rf $venv/src + python tools/install_venv.py + command_wrapper="$root/${with_venv}" + # Make sure it worked and record the environment version + sanity_check + chmod -R 754 $venv + cat requirements.txt test-requirements.txt > $venv_env_version +} + +function run_tests { + sanity_check + + if [ $with_selenium -eq 1 ]; then + export WITH_SELENIUM=1 + elif [ $only_selenium -eq 1 ]; then + export WITH_SELENIUM=1 + export SKIP_UNITTESTS=1 + fi + + if [ $with_selenium -eq 0 -a $integration -eq 0 ]; then + testopts="$testopts --exclude-dir=stacktask_ui/test/integration_tests" + fi + + if [ $selenium_headless -eq 1 ]; then + export SELENIUM_HEADLESS=1 + fi + + if [ -z "$testargs" ]; then + run_tests_all + else + run_tests_subset + fi +} + +function run_tests_subset { + project=`echo $testargs | awk -F. '{print $1}'` + ${command_wrapper} python $root/manage.py test --settings=$project.test.settings $testopts $testargs +} + +function run_tests_all { + echo "Running StackTask UI tests" + export NOSE_XUNIT_FILE=stacktask_ui/nosetests.xml + if [ "$NOSE_WITH_HTML_OUTPUT" = '1' ]; then + export NOSE_HTML_OUT_FILE='dashboard_nose_results.html' + fi + ${command_wrapper} ${coverage_run} $root/manage.py test stacktask_ui --settings=openstack_dashboard.test.settings $testopts + # get results of the openstack_dashboard tests + DASHBOARD_RESULT=$? + + if [ $with_coverage -eq 1 ]; then + echo "Generating coverage reports" + ${command_wrapper} python -m coverage.__main__ combine + ${command_wrapper} python -m coverage.__main__ xml -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' + ${command_wrapper} python -m coverage.__main__ html -i --include="horizon/*,openstack_dashboard/*" --omit='/usr*,setup.py,*egg*,.venv/*' -d reports + fi + # Remove the leftover coverage files from the -p flag earlier. + rm -f .coverage.* + + PEP8_RESULT=0 + if [ $no_pep8 -eq 0 ] && [ $only_selenium -eq 0 ]; then + run_pep8 + PEP8_RESULT=$? + fi + + TEST_RESULT=$(($DASHBOARD_RESULT || $PEP8_RESULT)) + if [ $TEST_RESULT -eq 0 ]; then + echo "Tests completed successfully." + else + echo "Tests failed." + fi + exit $TEST_RESULT +} + +function run_integration_tests { + export INTEGRATION_TESTS=1 + + if [ $selenium_headless -eq 1 ]; then + export SELENIUM_HEADLESS=1 + fi + + echo "Running Horizon integration tests..." + if [ -z "$testargs" ]; then + ${command_wrapper} nosetests openstack_dashboard/test/integration_tests/tests + else + ${command_wrapper} nosetests $testargs + fi + exit 0 +} + +function babel_extract { + DOMAIN=$1 + KEYWORDS="-k gettext_noop -k gettext_lazy -k ngettext_lazy:1,2" + KEYWORDS+=" -k ugettext_noop -k ugettext_lazy -k ungettext_lazy:1,2" + KEYWORDS+=" -k npgettext:1c,2,3 -k pgettext_lazy:1c,2 -k npgettext_lazy:1c,2,3" + + mkdir -p locale + ${command_wrapper} pybabel extract -F ../babel-${DOMAIN}.cfg -o locale/${DOMAIN}.pot $KEYWORDS . +} + +function run_makemessages { + echo -n "stacktask ui: " + cd + babel_extract django + STACKTASK_PY_RESULT=$? + + echo -n "stacktask ui javascript: " + babel_extract djangojs + STACKTASK_JS_RESULT=$? + + cd ../ + if [ $check_only -eq 1 ]; then + rm -f stacktask_ui/locale/django*.pot + fi + + exit $(($STACKTASK_PY_RESULT || $STACKTASK_JS_RESULT)) +} + +function run_compilemessages { + cd horizon + ${command_wrapper} $root/manage.py compilemessages + HORIZON_PY_RESULT=$? + cd ../openstack_dashboard + ${command_wrapper} $root/manage.py compilemessages + DASHBOARD_RESULT=$? + exit $(($HORIZON_PY_RESULT || $DASHBOARD_RESULT)) +} + +function run_pseudo { + for lang in $testargs + # Use English pot file as the source file/pot file just like real Horizon translations + do + ${command_wrapper} $root/tools/pseudo.py openstack_dashboard/locale/django.pot openstack_dashboard/locale/$lang/LC_MESSAGES/django.po $lang + ${command_wrapper} $root/tools/pseudo.py openstack_dashboard/locale/djangojs.pot openstack_dashboard/locale/$lang/LC_MESSAGES/djangojs.po $lang + ${command_wrapper} $root/tools/pseudo.py horizon/locale/django.pot horizon/locale/$lang/LC_MESSAGES/django.po $lang + ${command_wrapper} $root/tools/pseudo.py horizon/locale/djangojs.pot horizon/locale/$lang/LC_MESSAGES/djangojs.po $lang + done + exit $? +} + + +# ---------PREPARE THE ENVIRONMENT------------ # + +# PROCESS ARGUMENTS, OVERRIDE DEFAULTS +for arg in "$@"; do + process_option $arg +done + +if [ $quiet -eq 1 ] && [ $never_venv -eq 0 ] && [ $always_venv -eq 0 ] +then + always_venv=1 +fi + +# If destroy is set, just blow it away and exit. +if [ $destroy -eq 1 ]; then + destroy_venv + exit 0 +fi + +# Ignore all of this if the -N flag was set +if [ $never_venv -eq 0 ]; then + + # Restore previous environment if desired + if [ $restore_env -eq 1 ]; then + restore_environment + fi + + # Remove the virtual environment if --force used + if [ $force -eq 1 ]; then + destroy_venv + fi + + # Then check if it's up-to-date + environment_check + + # Create a backup of the up-to-date environment if desired + if [ $backup_env -eq 1 ]; then + backup_environment + fi +fi + +# ---------EXERCISE THE CODE------------ # + +# Run management commands +if [ $manage -eq 1 ]; then + run_management_command + exit $? +fi + +# Build the docs +if [ $just_docs -eq 1 ]; then + run_sphinx + exit $? +fi + +# Update translation files +if [ $makemessages -eq 1 ]; then + run_makemessages + exit $? +fi + +# Compile translation files +if [ $compilemessages -eq 1 ]; then + run_compilemessages + exit $? +fi + +# Generate Pseudo translation +if [ $pseudo -eq 1 ]; then + run_pseudo + exit $? +fi + +# PEP8 +if [ $just_pep8 -eq 1 ]; then + run_pep8 + exit $? +fi + +if [ $just_pep8_changed -eq 1 ]; then + run_pep8_changed + exit $? +fi + +# Pylint +if [ $just_pylint -eq 1 ]; then + run_pylint + exit $? +fi + +# ESLint +if [ $just_eslint -eq 1 ]; then + run_eslint + exit $? +fi + +# Karma +if [ $just_karma -eq 1 ]; then + run_karma + exit $? +fi + +# Tab checker +if [ $just_tabs -eq 1 ]; then + tab_check + exit $? +fi + +# Integration tests +if [ $integration -eq 1 ]; then + run_integration_tests + exit $? +fi + +# Django development server +if [ $runserver -eq 1 ]; then + run_server + exit $? +fi + +# Full test suite +run_tests || exit diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cc2c03c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,29 @@ +[metadata] +name = stacktask-ui +summary = StackTask User Interface +description-file = + README.rst +author = Adrian Turjak +author-email = adriant@catalyst.net.nz +home-page = https://github.com/catalyst/stacktask-ui +classifier = + Environment :: OpenStack + Framework :: Django + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 2.7 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.4 + +[files] +packages = + stacktask_ui + +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..782bb21 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. +# +# 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. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr>=1.8'], + pbr=True) diff --git a/stacktask_ui/__init__.py b/stacktask_ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/api/__init__.py b/stacktask_ui/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/api/stacktask.py b/stacktask_ui/api/stacktask.py new file mode 100644 index 0000000..df927c4 --- /dev/null +++ b/stacktask_ui/api/stacktask.py @@ -0,0 +1,239 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +import collections +import json +import logging +import requests +from six.moves.urllib.parse import urljoin + +from django.conf import settings + +from openstack_dashboard.api import base + +LOG = logging.getLogger(__name__) +USER = collections.namedtuple('User', + ['id', 'name', 'email', + 'roles', 'cohort', 'status']) +TOKEN = collections.namedtuple('Token', + ['action']) + + +def _get_endpoint_url(request): + # If the request is made by an anonymous user, this endpoint request fails. + # Thus, we must hardcode this in Horizon. + if getattr(request.user, "service_catalog", None): + url = base.url_for(request, service_type='registration') + else: + url = getattr(settings, 'OPENSTACK_REGISTRATION_URL') + + # Ensure ends in slash + if not url.endswith('/'): + url += '/' + + return url + + +def _request(request, method, url, headers, **kwargs): + try: + endpoint_url = _get_endpoint_url(request) + url = urljoin(endpoint_url, url) + session = requests.Session() + data = kwargs.pop("data", None) + return session.request(method, url, headers=headers, + data=data, **kwargs) + except Exception as e: + LOG.error(e) + raise + + +def head(request, url, **kwargs): + return _request(request, 'HEAD', url, **kwargs) + + +def get(request, url, **kwargs): + return _request(request, 'GET', url, **kwargs) + + +def post(request, url, **kwargs): + return _request(request, 'POST', url, **kwargs) + + +def put(request, url, **kwargs): + return _request(request, 'PUT', url, **kwargs) + + +def patch(request, url, **kwargs): + return _request(request, 'PATCH', url, **kwargs) + + +def delete(request, url, **kwargs): + return _request(request, 'DELETE', url, **kwargs) + + +def user_invite(request, user): + headers = {'Content-Type': 'application/json', + 'X-Auth-Token': request.user.token.id} + user['project_id'] = request.user.tenant_id + return post(request, 'openstack/users', + headers=headers, data=json.dumps(user)) + + +def user_list(request): + users = [] + try: + headers = {'Content-Type': 'application/json', + 'X-Auth-Token': request.user.token.id} + resp = json.loads(get(request, 'openstack/users', + headers=headers).content) + + for user in resp['users']: + users.append( + USER( + id=user['id'], + name=user['name'], + email=user['email'], + roles=user['roles'], + status=user['status'], + cohort=user['cohort'] + ) + ) + except Exception as e: + LOG.error(e) + raise + return users + + +def user_get(request, user_id): + try: + headers = {'X-Auth-Token': request.user.token.id} + resp = get(request, 'openstack/users/%s' % user_id, + headers=headers).content + return json.loads(resp) + except Exception as e: + LOG.error(e) + raise + + +def user_roles_update(request, user): + try: + headers = {'Content-Type': 'application/json', + 'X-Auth-Token': request.user.token.id} + user['project_id'] = request.user.tenant_id + user['roles'] = user.roles + return put(request, 'openstack/users/%s/roles' % user['id'], + headers=headers, + data=json.dumps(user)) + except Exception as e: + LOG.error(e) + raise + + +def user_roles_add(request, user_id, roles): + try: + headers = {'Content-Type': 'application/json', + 'X-Auth-Token': request.user.token.id} + params = {} + params['project_id'] = request.user.tenant_id + params['roles'] = roles + return put(request, 'openstack/users/%s/roles' % user_id, + headers=headers, + data=json.dumps(params)) + except Exception as e: + LOG.error(e) + raise + + +def user_roles_remove(request, user_id, roles): + try: + headers = {'Content-Type': 'application/json', + 'X-Auth-Token': request.user.token.id} + params = {} + params['project_id'] = request.user.tenant_id + params['roles'] = roles + return delete(request, 'openstack/users/%s/roles' % user_id, + headers=headers, + data=json.dumps(params)) + except Exception as e: + LOG.error(e) + raise + + +def user_revoke(request, user_id): + try: + headers = {'Content-Type': 'application/json', + 'X-Auth-Token': request.user.token.id} + data = dict() + return delete(request, 'openstack/users/%s' % user_id, + headers=headers, + data=json.dumps(data)) + except Exception as e: + LOG.error(e) + raise + + +def user_invitation_resend(request, user_id): + headers = {'Content-Type': 'application/json', + 'X-Auth-Token': request.user.token.id} + # For non-active users, the task id is the same as their userid + # For active users, re-sending an invitation doesn't make sense. + data = { + "task": user_id + } + return post(request, 'tokens', + headers=headers, + data=json.dumps(data)) + + +def valid_roles_get(request): + headers = {'Content-Type': 'application/json', + 'X-Auth-Token': request.user.token.id} + role_data = get(request, 'openstack/roles', headers=headers) + return role_data.json() + + +def valid_role_names_get(request): + roles_data = valid_roles_get(request) + role_names = [r['name'] for r in roles_data['roles']] + return role_names + + +def token_get(request, token, data): + headers = {'Content-Type': 'application/json'} + return get(request, 'tokens/%s' % token, + data=json.dumps(data), headers=headers) + + +def token_submit(request, token, data): + headers = {"Content-Type": "application/json"} + return post(request, 'tokens/%s' % token, + data=json.dumps(data), headers=headers) + + +def forgotpassword_submit(request, email, username=None): + # Username is optional only if the registration API considers it so + # In this case the backend assumes email==username + headers = {"Content-Type": "application/json"} + data = { + 'email': email + } + if username: + data['username'] = username + try: + return post(request, 'openstack/users/password-reset', + data=json.dumps(data), + headers=headers) + except Exception as e: + LOG.error(e) + raise diff --git a/stacktask_ui/content/__init__.py b/stacktask_ui/content/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/content/default/__init__.py b/stacktask_ui/content/default/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/content/default/panel.py b/stacktask_ui/content/default/panel.py new file mode 100644 index 0000000..63d4cfb --- /dev/null +++ b/stacktask_ui/content/default/panel.py @@ -0,0 +1,24 @@ +# Copyright (c) 2014 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class Default(horizon.Panel): + name = _("Default") + slug = 'default' + urls = 'stacktask_ui.content.project_users.urls' + nav = False diff --git a/stacktask_ui/content/default/templates/default/base.html b/stacktask_ui/content/default/templates/default/base.html new file mode 100644 index 0000000..c3696ad --- /dev/null +++ b/stacktask_ui/content/default/templates/default/base.html @@ -0,0 +1,3 @@ +{% extends 'base.html' %} + + diff --git a/stacktask_ui/content/forgot_password/__init__.py b/stacktask_ui/content/forgot_password/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/content/forgot_password/forms.py b/stacktask_ui/content/forgot_password/forms.py new file mode 100644 index 0000000..f857160 --- /dev/null +++ b/stacktask_ui/content/forgot_password/forms.py @@ -0,0 +1,55 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.conf import settings +from django import forms +from django import http +from django.utils.translation import ugettext_lazy as _ + +from horizon import forms as hforms +from horizon.utils import functions as utils + +from stacktask_ui.api import stacktask + +# Username is ignored, use email address instead. +USERNAME_IS_EMAIL = True + + +class ForgotPasswordForm(hforms.SelfHandlingForm): + email = forms.EmailField( + label=_("Email"), + widget=forms.TextInput(attrs={"autofocus": "autofocus"}) + ) + + def clean(self, *args, **kwargs): + # validate username and email? + return super(ForgotPasswordForm, self).clean(*args, **kwargs) + + def handle(self, *args, **kwargs): + email = self.cleaned_data['email'] + + try: + submit_response = stacktask.forgotpassword_submit(self.request, + email) + if submit_response.ok: + return True + except Exception: + pass + + # Send the user back to the login page. + msg = _("The password reset service is currently unavailable. " + "Please try again later.") + response = http.HttpResponseRedirect(settings.LOGOUT_URL) + utils.add_logout_reason(self.request, response, msg) + return response diff --git a/stacktask_ui/content/forgot_password/panel.py b/stacktask_ui/content/forgot_password/panel.py new file mode 100644 index 0000000..0ce5c45 --- /dev/null +++ b/stacktask_ui/content/forgot_password/panel.py @@ -0,0 +1,23 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class ForgotPasswordPanel(horizon.Panel): + name = _('Forgot Password') + slug = 'forgot_password' + nav = False diff --git a/stacktask_ui/content/forgot_password/templates/auth/_login_page.html b/stacktask_ui/content/forgot_password/templates/auth/_login_page.html new file mode 100644 index 0000000..474102b --- /dev/null +++ b/stacktask_ui/content/forgot_password/templates/auth/_login_page.html @@ -0,0 +1,7 @@ +{% overextends 'auth/_login_page.html' %} +{% load i18n %} + +{% block login_footer %} + {{ block.super }} +

Can't log in? Reset your password.

+{% endblock %} diff --git a/stacktask_ui/content/forgot_password/templates/forgot_password/_forgot_password_form.html b/stacktask_ui/content/forgot_password/templates/forgot_password/_forgot_password_form.html new file mode 100644 index 0000000..b1af447 --- /dev/null +++ b/stacktask_ui/content/forgot_password/templates/forgot_password/_forgot_password_form.html @@ -0,0 +1,52 @@ +{% load i18n %} + +{% block pre_login %} +
+ {% csrf_token %} +{% endblock %} + +
+ +
+ {% block login_header %} + + {% endblock %} +
+ +
+ {% block login_body %} + {% comment %} + These fake fields are required to prevent Chrome v34+ from autofilling form. + {% endcomment %} + {% if HORIZON_CONFIG.password_autocomplete != "on" %} + + {%endif%} +
+ {% include "horizon/common/_form_fields.html" %} +
+ {% endblock %} +
+ + +
+ +{% block post_login%} +
+{% endblock %} diff --git a/stacktask_ui/content/forgot_password/templates/forgot_password/_index.html b/stacktask_ui/content/forgot_password/templates/forgot_password/_index.html new file mode 100644 index 0000000..f7df39c --- /dev/null +++ b/stacktask_ui/content/forgot_password/templates/forgot_password/_index.html @@ -0,0 +1,23 @@ +{% extends 'forgot_password/_forgot_password_form.html' %} + +{% load url from future %} +{% load i18n %} + +{% block pre_login %} + +{% endblock %} diff --git a/stacktask_ui/content/forgot_password/templates/forgot_password/_sent.html b/stacktask_ui/content/forgot_password/templates/forgot_password/_sent.html new file mode 100644 index 0000000..3148d03 --- /dev/null +++ b/stacktask_ui/content/forgot_password/templates/forgot_password/_sent.html @@ -0,0 +1,20 @@ +{% extends 'forgot_password/_sent_text.html' %} + +{% block pre_login %} + +{% endblock %} diff --git a/stacktask_ui/content/forgot_password/templates/forgot_password/_sent_text.html b/stacktask_ui/content/forgot_password/templates/forgot_password/_sent_text.html new file mode 100644 index 0000000..c323acc --- /dev/null +++ b/stacktask_ui/content/forgot_password/templates/forgot_password/_sent_text.html @@ -0,0 +1,34 @@ +{% load i18n %} + +{% block pre_login %} +
+ {% csrf_token %} +{% endblock %} + +
+ +
+ {% block login_header %} + + {% endblock %} +
+ +
+ {% block login_body %} + {% trans "A password reset link has been sent to you, provided an account exists for your email address." %} + {% endblock %} +
+ + +
+ +{% block post_login%} +
+{% endblock %} diff --git a/stacktask_ui/content/forgot_password/templates/forgot_password/index.html b/stacktask_ui/content/forgot_password/templates/forgot_password/index.html new file mode 100644 index 0000000..86657f8 --- /dev/null +++ b/stacktask_ui/content/forgot_password/templates/forgot_password/index.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load i18n %} + + +{% block title %}{% trans "Forgot Password" %}{% endblock %} + +{% block body_id %}splash{% endblock %} + +{% block content %} + {% include 'forgot_password/_index.html' %} +{% endblock %} diff --git a/stacktask_ui/content/forgot_password/templates/forgot_password/sent.html b/stacktask_ui/content/forgot_password/templates/forgot_password/sent.html new file mode 100644 index 0000000..ac31c67 --- /dev/null +++ b/stacktask_ui/content/forgot_password/templates/forgot_password/sent.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% load i18n %} +{% block title %}{% trans "Forgot password" %}{% endblock %} + +{% block body_id %}splash{% endblock %} + +{% block content %} + {% include 'forgot_password/_sent.html' %} +{% endblock %} diff --git a/stacktask_ui/content/forgot_password/urls.py b/stacktask_ui/content/forgot_password/urls.py new file mode 100644 index 0000000..1a32a51 --- /dev/null +++ b/stacktask_ui/content/forgot_password/urls.py @@ -0,0 +1,25 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.conf.urls import patterns +from django.conf.urls import url + +from stacktask_ui.content.forgot_password import views + +# NOTE(adriant): These urls are registered in the project_users urls.py file. +urlpatterns = patterns( + '', + url(r'^/?$', views.ForgotPasswordView.as_view(), name='forgot_index'), + url(r'^sent/?$', views.password_sent_view, name='forgot_sent'), +) diff --git a/stacktask_ui/content/forgot_password/views.py b/stacktask_ui/content/forgot_password/views.py new file mode 100644 index 0000000..86e8028 --- /dev/null +++ b/stacktask_ui/content/forgot_password/views.py @@ -0,0 +1,29 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.shortcuts import render + +from horizon import forms + +from stacktask_ui.content.forgot_password import forms as fp_forms + + +class ForgotPasswordView(forms.ModalFormView): + form_class = fp_forms.ForgotPasswordForm + template_name = 'forgot_password/index.html' + success_url = '/forgot_password/sent' + + +def password_sent_view(request): + return render(request, 'forgot_password/sent.html') diff --git a/stacktask_ui/content/project_users/__init__.py b/stacktask_ui/content/project_users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/content/project_users/forms.py b/stacktask_ui/content/project_users/forms.py new file mode 100644 index 0000000..db03770 --- /dev/null +++ b/stacktask_ui/content/project_users/forms.py @@ -0,0 +1,129 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from stacktask_ui.api import stacktask +from stacktask_ui.content.project_users import utils + + +def get_role_choices(request): + """Get manageable roles for user. + + Returns a list of sorted 2-ary tuples containing the roles the current + user can manage. + """ + role_names = stacktask.valid_role_names_get(request) + role_tuples = [(r, utils.get_role_text(r)) for r in role_names] + role_tuples = sorted(role_tuples, key=lambda role: role[1]) + return role_tuples + + +class InviteUserForm(forms.SelfHandlingForm): + email = forms.EmailField() + roles = forms.MultipleChoiceField(label=_("Roles"), + required=True, + widget=forms.CheckboxSelectMultiple(), + help_text=_("Select roles to grant to " + "the user within the " + "current project.")) + + def __init__(self, *args, **kwargs): + super(InviteUserForm, self).__init__(*args, **kwargs) + self.fields['roles'].choices = get_role_choices(self.request) + self.fields['roles'].initial = ['_member_'] + + def handle(self, request, data): + try: + response = stacktask.user_invite(request, data) + if response.status_code == 200: + messages.success(request, _('Invited user successfully.')) + else: + messages.error(request, _('Failed to invite user.')) + return True + except Exception: + messages.error(request, _('Failed to invite user.')) + return False + + +class UpdateUserForm(forms.SelfHandlingForm): + id = forms.Field() + id.widget = forms.HiddenInput() + name = forms.CharField() + name.widget.attrs['readonly'] = True + + roles = forms.MultipleChoiceField( + label=_("Roles"), + required=True, + widget=forms.CheckboxSelectMultiple(), + help_text=_("Select roles to limit the " + "permission of the user.") + ) + + def __init__(self, *args, **kwargs): + super(UpdateUserForm, self).__init__(*args, **kwargs) + self.fields['roles'].choices = get_role_choices(self.request) + + def handle(self, request, data): + # Get the before and after role lists, make two lists: + # roles_added and roles_removed. + # Submit each list to the api (add first?) + try: + user_id = data['id'] + current_user = stacktask.user_get(request, user_id) + current_roles = set(current_user['roles']) + managable_roles = set( + stacktask.valid_role_names_get(request)) + current_managable_roles = current_roles & managable_roles + desired_roles = set(data['roles']) + roles_added = list(desired_roles - current_managable_roles) + roles_removed = list(current_managable_roles - desired_roles) + + # Remove roles from user + remove_status = 200 + if len(roles_removed) > 0: + remove_response = stacktask.user_roles_remove( + request, + user_id, + roles_removed) + remove_status = remove_response.status_code + if remove_status != 200: + messages.error(request, _('Failed to remove roles from user.')) + return False + + # Add new roles + added_status = 200 + if len(roles_added) > 0: + added_response = stacktask.user_roles_add( + request, + user_id, + roles_added) + added_status = added_response.status_code + if added_status != 200: + messages.error(request, _('Failed to add roles to user.')) + return False + + except Exception: + msg = _('Failed to update user.') + url = reverse('horizon:management:project_users:index') + exceptions.handle(request, msg, redirect=url) + return False + + messages.success(request, _('Updated user successfully.')) + return True diff --git a/stacktask_ui/content/project_users/panel.py b/stacktask_ui/content/project_users/panel.py new file mode 100644 index 0000000..de055aa --- /dev/null +++ b/stacktask_ui/content/project_users/panel.py @@ -0,0 +1,23 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class ProjectUsers(horizon.Panel): + name = _('Project Users') + slug = 'project_users' + policy_rules = (("identity", "identity:project_users_access"),) diff --git a/stacktask_ui/content/project_users/tables.py b/stacktask_ui/content/project_users/tables.py new file mode 100644 index 0000000..4205bf0 --- /dev/null +++ b/stacktask_ui/content/project_users/tables.py @@ -0,0 +1,181 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from collections import defaultdict + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import exceptions +from horizon import tables + +from stacktask_ui.api import stacktask +from stacktask_ui.content.project_users import utils + + +class InviteUser(tables.LinkAction): + name = "invite" + verbose_name = _("Invite User") + url = "horizon:management:project_users:invite" + classes = ("ajax-modal",) + icon = "plus" + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Invited User", + u"Invited Users", + count + ) + + +class ResendInvitation(tables.BatchAction): + name = "resend" + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Resend Invitation", + u"Resend Invitations", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Invitation re-sent", + u"Invitations re-sent", + count + ) + + def allowed(self, request, user=None): + # Only invited users can be re-invited + return user.cohort == 'Invited' + + def action(self, request, datum_id): + user = self.table.get_object_by_id(datum_id) + stacktask.user_invitation_resend(request, user.id) + + +class UpdateUser(tables.LinkAction): + name = "update" + verbose_name = _("Update User") + url = "horizon:management:project_users:update" + classes = ("ajax-modal",) + icon = "pencil" + + def allowed(self, request, user=None): + # Currently, we only support updating existing users + return user.cohort == 'Member' + + +class RevokeUser(tables.DeleteAction): + help_text = _("This will remove the selected user(s) from the current " + "project.") + + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Revoke User", + u"Revoke Users", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Revoked User", + u"Revoked Users", + count + ) + + def delete(self, request, obj_id): + user = self.table.get_object_by_id(obj_id) + + # NOTE(dale): There is a different endpoint to revoke an invited user + # than an Active user. + result = None + if user.cohort == 'Invited': + # Revoke invite for the user + result = stacktask.user_revoke(request, user.id) + else: + # Revoke all roles from the user. + # That'll get them out of our project; they keep their account. + result = stacktask.user_roles_remove( + request, user.id, user.roles) + if not result or result.status_code != 200: + exception = exceptions.NotAvailable() + exception._safe_message = False + raise exception + + +class UpdateUserRow(tables.Row): + ajax = True + + def get_data(self, request, user_id): + user = stacktask.user_get(request, user_id) + return user + + def load_cells(self, user=None): + super(UpdateUserRow, self).load_cells(user) + user = self.datum + self.classes.append('category-' + user.cohort) + + +class CohortFilter(tables.FixedFilterAction): + def get_fixed_buttons(self): + def make_dict(text, value, icon): + return dict(text=text, value=value, icon=icon) + + buttons = [] + buttons.append(make_dict(_('Project Users'), + 'Member', + 'fa-user-md')) + buttons.append(make_dict(_('Invited Users'), + 'Invited', + 'fa-user')) + return buttons + + def categorize(self, table, users): + categorized_users = defaultdict(list) + for u in users: + categorized_users[u.cohort].append(u) + return categorized_users + + +def UserRoleDisplayFilter(role_list): + roles = [utils.get_role_text(r) for r in role_list] + return ', '.join(roles) + + +class UsersTable(tables.DataTable): + uid = tables.Column('id', verbose_name=_('User ID')) + name = tables.Column('name', verbose_name=_('Name')) + email = tables.Column('email', verbose_name=_('Email')) + role = tables.Column('roles', + verbose_name=_('Roles'), + filters=[UserRoleDisplayFilter]) + status = tables.Column('status', verbose_name=_('Status')) + cohort = tables.Column('cohort', + verbose_name=_('Member Type'), + hidden=True) + + class Meta(object): + name = 'users' + row_class = UpdateUserRow + verbose_name = _('Users') + columns = ('id', 'name', 'email', 'role', 'status', 'cohort') + table_actions = (CohortFilter, InviteUser, RevokeUser) + row_actions = (UpdateUser, ResendInvitation, RevokeUser) + multi_select = True diff --git a/stacktask_ui/content/project_users/templates/project_users/_invite.html b/stacktask_ui/content/project_users/templates/project_users/_invite.html new file mode 100644 index 0000000..2244232 --- /dev/null +++ b/stacktask_ui/content/project_users/templates/project_users/_invite.html @@ -0,0 +1,30 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block modal_id %}invite_user_form{% endblock %} +{% block modal-header %}{% trans "Invite User" %}{% endblock %} +{% block form_action %}{% url 'horizon:management:project_users:invite' %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "Invite a person to join your project. If the user does not exist they will be emailed instructions to set up their account then automatically added to your project." %}

+

{% trans "Roles:" %}

+

{% trans "The “Project Admin” role allows users to have full control over your project, including adding moderators and inviting other people to join it." %}

+

{% trans "The “Project Moderator” role can invite other people to join your project and update their roles, but cannot change the project admin." %}

+

{% trans "The “Project Member” role gives people access to all services on your project, but does not allow them to invite other people to join the project or update roles." %}

+

{% trans "For other roles or more information about access control, please refer to the access control section of the Catalyst Cloud documentation." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} + diff --git a/stacktask_ui/content/project_users/templates/project_users/_update.html b/stacktask_ui/content/project_users/templates/project_users/_update.html new file mode 100644 index 0000000..a575ed0 --- /dev/null +++ b/stacktask_ui/content/project_users/templates/project_users/_update.html @@ -0,0 +1,29 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block modal_id %}update_user_form{% endblock %} +{% block modal-header %}{% trans "Update User" %}{% endblock %} +{% block form_action %}{% url 'horizon:management:project_users:update' user.id %}{% endblock %} +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "Adjust user roles to revoke or grant roles within this project." %}

+

{% trans "Roles:" %}

+

{% trans "The “Project Admin” role allows users to have full control over your project, including adding moderators and inviting other people to join it." %}

+

{% trans "The “Project Moderator” role can invite other people to join your project and update their roles, but cannot change the project admin." %}

+

{% trans "The “Project Member” role gives people access to all services on your project, but does not allow them to invite other people to join the project or update roles." %}

+

{% trans "For other roles or more information about access control, please refer to the access control section of the Catalyst Cloud documentation." %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} + diff --git a/stacktask_ui/content/project_users/templates/project_users/index.html b/stacktask_ui/content/project_users/templates/project_users/index.html new file mode 100644 index 0000000..4e0218a --- /dev/null +++ b/stacktask_ui/content/project_users/templates/project_users/index.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Project Users" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Project Users") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/stacktask_ui/content/project_users/templates/project_users/invite.html b/stacktask_ui/content/project_users/templates/project_users/invite.html new file mode 100644 index 0000000..17f3520 --- /dev/null +++ b/stacktask_ui/content/project_users/templates/project_users/invite.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Invite User" %}{% endblock %} + +{% block main %} + {% include 'management/project_users/_invite.html' %} +{% endblock %} diff --git a/stacktask_ui/content/project_users/templates/project_users/update.html b/stacktask_ui/content/project_users/templates/project_users/update.html new file mode 100644 index 0000000..d415e8f --- /dev/null +++ b/stacktask_ui/content/project_users/templates/project_users/update.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update User" %}{% endblock %} +{% block main %} + {% include 'management/project_users/_update.html' %} +{% endblock %} diff --git a/stacktask_ui/content/project_users/urls.py b/stacktask_ui/content/project_users/urls.py new file mode 100644 index 0000000..6c393d5 --- /dev/null +++ b/stacktask_ui/content/project_users/urls.py @@ -0,0 +1,28 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.conf.urls import patterns +from django.conf.urls import url + +from stacktask_ui.content.project_users import views + + +urlpatterns = patterns( + '', + url(r'^invite/$', views.InviteUserView.as_view(), name='invite'), + url(r'^(?P[^/]+)/update/$', + views.UpdateUserView.as_view(), + name='update'), + url(r'^$', views.UsersView.as_view(), name='index') +) diff --git a/stacktask_ui/content/project_users/utils.py b/stacktask_ui/content/project_users/utils.py new file mode 100644 index 0000000..e174113 --- /dev/null +++ b/stacktask_ui/content/project_users/utils.py @@ -0,0 +1,33 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +ROLE_TRANSLATIONS = { + 'project_admin': _('Project Admin'), + 'project_mod': _('Project Moderator'), + '_member_': _('Project Member'), + 'heat_stack_owner': _('Heat Stack Owner'), + 'project_readonly': _('Project Read-only'), + 'compute_start_stop': _('Compute Start/Stop'), + 'object_storage': _('Object Storage') +} + + +def get_role_text(rname): + # Gets the role text for a given role. + # If it doesn't exist will simply return the role name. + if rname in ROLE_TRANSLATIONS: + return ROLE_TRANSLATIONS[rname].format() + return rname diff --git a/stacktask_ui/content/project_users/views.py b/stacktask_ui/content/project_users/views.py new file mode 100644 index 0000000..b4020a4 --- /dev/null +++ b/stacktask_ui/content/project_users/views.py @@ -0,0 +1,90 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.core.urlresolvers import reverse +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import tables +from horizon.utils import memoized + +from stacktask_ui.content.project_users import tables as users_tables + +from stacktask_ui.content.project_users import forms as users_forms + +from stacktask_ui.api import stacktask + + +class InviteUserView(forms.ModalFormView): + form_class = users_forms.InviteUserForm + form_id = "invite_user_form" + modal_header = _("Invite User") + submit_label = _("Invite User") + submit_url = reverse_lazy('horizon:management:project_users:invite') + template_name = 'management/project_users/invite.html' + context_object_name = 'project_users' + success_url = reverse_lazy("horizon:management:project_users:index") + page_title = _("Invite User") + + +class UpdateUserView(forms.ModalFormView): + form_class = users_forms.UpdateUserForm + form_id = "update_user_form" + modal_header = _("Update User") + submit_label = _("Update User") + submit_url = 'horizon:management:project_users:update' + template_name = 'management/project_users/update.html' + context_object_name = 'project_users' + success_url = reverse_lazy("horizon:management:project_users:index") + page_title = _("Update User") + + @memoized.memoized_method + def get_object(self): + try: + return stacktask.user_get(self.request, + self.kwargs['user_id']) + except Exception: + msg = _('Unable to retrieve user.') + url = reverse('horizon:management:project_users:index') + exceptions.handle(self.request, msg, redirect=url) + + def get_context_data(self, **kwargs): + context = super(UpdateUserView, self).get_context_data(**kwargs) + context['user'] = self.get_object() + args = (self.kwargs['user_id'],) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + user = self.get_object() + data = {'id': self.kwargs['user_id'], + 'name': user['username'], + 'roles': user['roles'], + } + return data + + +class UsersView(tables.DataTableView): + table_class = users_tables.UsersTable + template_name = 'management/project_users/index.html' + page_title = _("Project Users") + + def get_data(self): + try: + return stacktask.user_list(self.request) + except Exception: + exceptions.handle(self.request, _('Failed to list users.')) + return [] diff --git a/stacktask_ui/content/token/__init__.py b/stacktask_ui/content/token/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/content/token/forms.py b/stacktask_ui/content/token/forms.py new file mode 100644 index 0000000..61de668 --- /dev/null +++ b/stacktask_ui/content/token/forms.py @@ -0,0 +1,83 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.conf import settings +from django.forms import ValidationError # noqa +from django import http +from django.utils.translation import ugettext_lazy as _ +from django.views.decorators.debug import sensitive_variables # noqa + +from horizon import exceptions +from horizon import forms +from horizon import messages +from horizon.utils import functions as utils +from horizon.utils import validators + +from openstack_dashboard import api + + +class PasswordForm(forms.SelfHandlingForm): + new_password = forms.RegexField( + label=_("New password"), + widget=forms.PasswordInput(render_value=False), + regex=validators.password_validator(), + error_messages={ + 'invalid': validators.password_validator_msg() + } + ) + confirm_password = forms.CharField( + label=_("Confirm new password"), + widget=forms.PasswordInput(render_value=False) + ) + no_autocomplete = True + + def clean(self): + '''Check to make sure password fields match.''' + data = super(forms.Form, self).clean() + if 'new_password' in data: + if data['new_password'] != data.get('confirm_password', None): + raise ValidationError(_('Passwords do not match.')) + return data + + # We have to protect the entire "data" dict because it contains + # newpassword strings. + @sensitive_variables('data') + def handle(self, request, data): + usable_data = self.cleaned_data + user_is_editable = api.keystone.keystone_can_edit_user() + + if user_is_editable and usable_data: + try: + api.keystone.user_update_own_password( + request, + usable_data['current_password'], + usable_data['new_password'] + ) + response = http.HttpResponseRedirect(settings.LOGOUT_URL) + msg = _("Password changed. Please log in again to continue.") + utils.add_logout_reason(request, response, msg) + return response + except Exception: + exceptions.handle(request, + _('Unable to change password.')) + return False + else: + messages.error(request, _('Changing password is not supported.')) + return False + + +class ConfirmForm(forms.SelfHandlingForm): + + def handle(self, request, data): + pass diff --git a/stacktask_ui/content/token/panel.py b/stacktask_ui/content/token/panel.py new file mode 100644 index 0000000..74a1378 --- /dev/null +++ b/stacktask_ui/content/token/panel.py @@ -0,0 +1,24 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class TokenPanel(horizon.Panel): + name = _('Token') + slug = 'token' + urls = 'stacktask_ui.content.token.urls' + nav = False diff --git a/stacktask_ui/content/token/templates/token/_setpassword.html b/stacktask_ui/content/token/templates/token/_setpassword.html new file mode 100644 index 0000000..ede0c01 --- /dev/null +++ b/stacktask_ui/content/token/templates/token/_setpassword.html @@ -0,0 +1,20 @@ +{% extends 'token/_setpassword_form.html' %} + +{% block pre_login %} + +{% endblock %} diff --git a/stacktask_ui/content/token/templates/token/_setpassword_form.html b/stacktask_ui/content/token/templates/token/_setpassword_form.html new file mode 100644 index 0000000..09308bb --- /dev/null +++ b/stacktask_ui/content/token/templates/token/_setpassword_form.html @@ -0,0 +1,52 @@ +{% load i18n %} + +{% block pre_login %} +
+ {% csrf_token %} +{% endblock %} + +
+ +
+ {% block login_header %} + + {% endblock %} +
+ +
+ {% block login_body %} + {% comment %} + These fake fields are required to prevent Chrome v34+ from autofilling form. + {% endcomment %} + {% if HORIZON_CONFIG.password_autocomplete != "on" %} + + {%endif%} +
+ {% include "horizon/common/_form_fields.html" %} +
+ {% endblock %} +
+ + +
+ +{% block post_login%} +
+{% endblock %} diff --git a/stacktask_ui/content/token/templates/token/_tokenconfirm.html b/stacktask_ui/content/token/templates/token/_tokenconfirm.html new file mode 100644 index 0000000..ead4ce3 --- /dev/null +++ b/stacktask_ui/content/token/templates/token/_tokenconfirm.html @@ -0,0 +1,20 @@ +{% extends 'token/_tokenconfirm_form.html' %} + +{% block pre_login %} + +{% endblock %} diff --git a/stacktask_ui/content/token/templates/token/_tokenconfirm_form.html b/stacktask_ui/content/token/templates/token/_tokenconfirm_form.html new file mode 100644 index 0000000..43609c5 --- /dev/null +++ b/stacktask_ui/content/token/templates/token/_tokenconfirm_form.html @@ -0,0 +1,38 @@ +{% load i18n %} + +{% block pre_login %} +
+ {% csrf_token %} +{% endblock %} + +
+ +
+ {% block login_header %} + + {% endblock %} +
+ +
+ {% block login_body %} + You have been invited to join a project. + {% endblock %} +
+ + +
+ +{% block post_login%} +
+{% endblock %} diff --git a/stacktask_ui/content/token/templates/token/setpassword.html b/stacktask_ui/content/token/templates/token/setpassword.html new file mode 100644 index 0000000..31e8c51 --- /dev/null +++ b/stacktask_ui/content/token/templates/token/setpassword.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% load i18n %} +{% block title %}{% trans "Set Password" %}{% endblock %} + +{% block body_id %}splash{% endblock %} + +{% block content %} + {% include 'token/_setpassword.html' %} +{% endblock %} + +{% comment %}Explicitly redeclare the piwik block as empty{% endcomment %} +{% block piwik %}{% endblock %} diff --git a/stacktask_ui/content/token/templates/token/tokenconfirm.html b/stacktask_ui/content/token/templates/token/tokenconfirm.html new file mode 100644 index 0000000..44321bd --- /dev/null +++ b/stacktask_ui/content/token/templates/token/tokenconfirm.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% load i18n %} +{% block title %}{% trans "Confirm project invitation" %}{% endblock %} + +{% block body_id %}splash{% endblock %} + +{% block content %} + {% include 'token/_tokenconfirm.html' %} +{% endblock %} + +{% comment %}Explicitly redeclare the piwik block as empty{% endcomment %} +{% block piwik %}{% endblock %} diff --git a/stacktask_ui/content/token/urls.py b/stacktask_ui/content/token/urls.py new file mode 100644 index 0000000..6b857ff --- /dev/null +++ b/stacktask_ui/content/token/urls.py @@ -0,0 +1,25 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + + +from django.conf.urls import patterns +from django.conf.urls import url + +from stacktask_ui.content.token import views + +urlpatterns = patterns( + '', + url(r'^(?P\w+)/?$', views.submit_token_router, + name='token_verify'), +) diff --git a/stacktask_ui/content/token/views.py b/stacktask_ui/content/token/views.py new file mode 100644 index 0000000..ba9763c --- /dev/null +++ b/stacktask_ui/content/token/views.py @@ -0,0 +1,138 @@ +# Copyright (c) 2016 Catalyst IT Ltd. +# +# 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. + + +from django.conf import settings +from django import http +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon.utils import functions as utils + +from stacktask_ui.api import stacktask +from stacktask_ui.content.token import forms as token_forms + + +def _logout_msg_response(request, msg): + response = http.HttpResponseRedirect(settings.LOGOUT_URL) + utils.add_logout_reason(request, response, msg) + return response + + +def _logout_msg_response_success(request, msg): + response = _logout_msg_response(request, msg) + response.set_cookie('logout_reason_class', 'success', max_age=10) + return response + + +def submit_token_router(request, *args, **kwargs): + """Routes requests to the correct view, based on submission parameters.""" + + # Get details of the token + token_uuid = kwargs['token'] + token = stacktask.token_get(request, token_uuid, {}) + if not token or token.status_code != 200: + msg = _("Invalid token. Please request another.") + return _logout_msg_response(request, msg) + + json = token.json() + if 'password' in json['required_fields']: + return SubmitTokenPasswordView.as_view()(request, *args, **kwargs) + elif 'confirm' in json['required_fields']: + return SubmitTokenConfirmView.as_view()(request, *args, **kwargs) + + return _logout_msg_response(request, _("Unsupported token type.")) + + +class SubmitTokenPasswordView(forms.ModalFormView): + form_class = token_forms.PasswordForm + template_name = 'token/setpassword.html' + + def get(self, request, *args, **kwargs): + sc = super(SubmitTokenPasswordView, self) + return sc.get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + sc = super(SubmitTokenPasswordView, self) + return sc.post(request, *args, **kwargs) + + def form_valid(self, form): + # All form data has been validated and POSTed. + # We can now submit the token for processing along with parameters. + # All codepaths here return a Redirect response. + parameters = { + 'password': form.cleaned_data['new_password'] + } + token_uuid = self.kwargs['token'] + token_response = stacktask.token_submit(form.request, + token_uuid, + parameters) + + if token_response.ok: + msg = _("Password successfully set. Please log in to continue.") + return _logout_msg_response_success(form.request, msg) + + msg = (_("Token form submission failed. Response code %(code)s.") % + {'code': token_response.status_code}) + return _logout_msg_response(form.request, msg) + + def get_context_data(self, **kwargs): + sc = super(SubmitTokenPasswordView, self) + context = sc.get_context_data(**kwargs) + try: + context['token'] = self.kwargs['token'] + except Exception: + exceptions.handle(self.request) + return context + + +class SubmitTokenConfirmView(forms.ModalFormView): + form_class = token_forms.ConfirmForm + template_name = 'token/tokenconfirm.html' + + def get(self, request, *args, **kwargs): + sc = super(SubmitTokenConfirmView, self) + return sc.get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + sc = super(SubmitTokenConfirmView, self) + return sc.post(request, *args, **kwargs) + + def form_valid(self, form): + token_uuid = self.kwargs['token'] + parameters = { + 'confirm': True + } + token_response = stacktask.token_submit(form.request, + token_uuid, + parameters) + + if token_response.ok: + msg = _("Welcome to the project! Please log in to continue.") + return _logout_msg_response_success(form.request, msg) + + msg = (_("Invitation accept form submission failed. " + "Response code %(code)s.") % {'code': + token_response.status_code}) + return _logout_msg_response(form.request, msg) + + def get_context_data(self, **kwargs): + sc = super(SubmitTokenConfirmView, self) + context = sc.get_context_data(**kwargs) + try: + context['token'] = self.kwargs['token'] + except Exception: + exceptions.handle(self.request) + return context diff --git a/stacktask_ui/dashboards/__init__.py b/stacktask_ui/dashboards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/dashboards/forgot_password_dash.py b/stacktask_ui/dashboards/forgot_password_dash.py new file mode 100644 index 0000000..764317d --- /dev/null +++ b/stacktask_ui/dashboards/forgot_password_dash.py @@ -0,0 +1,31 @@ +# Copyright (c) 2014 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from stacktask_ui.content.forgot_password import panel + + +class ForgotPasswordDashboard(horizon.Dashboard): + name = _("Forgot Password") + slug = "forgot_password" + default_panel = 'forgot_password' + nav = False + public = True + + +horizon.register(ForgotPasswordDashboard) +ForgotPasswordDashboard.register(panel.ForgotPasswordPanel) diff --git a/stacktask_ui/dashboards/management.py b/stacktask_ui/dashboards/management.py new file mode 100644 index 0000000..1927be5 --- /dev/null +++ b/stacktask_ui/dashboards/management.py @@ -0,0 +1,29 @@ +# Copyright (c) 2014 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from stacktask_ui.content.default import panel + + +class ManagementDashboard(horizon.Dashboard): + name = _("Management") + slug = "management" + default_panel = 'default' + + +horizon.register(ManagementDashboard) +ManagementDashboard.register(panel.Default) diff --git a/stacktask_ui/dashboards/token_dash.py b/stacktask_ui/dashboards/token_dash.py new file mode 100644 index 0000000..9301455 --- /dev/null +++ b/stacktask_ui/dashboards/token_dash.py @@ -0,0 +1,31 @@ +# Copyright (c) 2014 Catalyst IT Ltd. +# +# 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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon + +from stacktask_ui.content.token import panel + + +class TokenDashboard(horizon.Dashboard): + name = _("Token") + slug = "token" + default_panel = 'token' + nav = False + public = True + + +horizon.register(TokenDashboard) +TokenDashboard.register(panel.TokenPanel) diff --git a/stacktask_ui/enabled/_6000_management.py b/stacktask_ui/enabled/_6000_management.py new file mode 100644 index 0000000..7a5f659 --- /dev/null +++ b/stacktask_ui/enabled/_6000_management.py @@ -0,0 +1,7 @@ +# The name of the dashboard to be added to HORIZON['dashboards']. Required. +DASHBOARD = 'management' + +# A list of applications to be added to INSTALLED_APPS. +ADD_INSTALLED_APPS = [ + 'stacktask_ui.dashboards.management' +] diff --git a/stacktask_ui/enabled/_6020_token.py b/stacktask_ui/enabled/_6020_token.py new file mode 100644 index 0000000..3fe83e9 --- /dev/null +++ b/stacktask_ui/enabled/_6020_token.py @@ -0,0 +1,8 @@ +# The name of the dashboard to be added to HORIZON['dashboards']. Required. +DASHBOARD = 'token' + +# A list of applications to be added to INSTALLED_APPS. +ADD_INSTALLED_APPS = [ + 'stacktask_ui.dashboards.token_dash', + 'stacktask_ui.content.token', +] diff --git a/stacktask_ui/enabled/_6030_forgot_password.py b/stacktask_ui/enabled/_6030_forgot_password.py new file mode 100644 index 0000000..1cf44af --- /dev/null +++ b/stacktask_ui/enabled/_6030_forgot_password.py @@ -0,0 +1,9 @@ +# The name of the dashboard to be added to HORIZON['dashboards']. Required. +DASHBOARD = 'forgot_password' + +# A list of applications to be added to INSTALLED_APPS. +ADD_INSTALLED_APPS = [ + 'stacktask_ui.dashboards.forgot_password_dash', + 'stacktask_ui.content.forgot_password', + 'overextends', +] diff --git a/stacktask_ui/enabled/_6040_management_access_control_group.py b/stacktask_ui/enabled/_6040_management_access_control_group.py new file mode 100644 index 0000000..e0586e3 --- /dev/null +++ b/stacktask_ui/enabled/_6040_management_access_control_group.py @@ -0,0 +1,8 @@ +from django.utils.translation import ugettext_lazy as _ + +# The slug of the panel group to be added to HORIZON_CONFIG. Required. +PANEL_GROUP = 'access_control' +# The display name of the PANEL_GROUP. Required. +PANEL_GROUP_NAME = _('Access Control') +# The slug of the dashboard the PANEL_GROUP associated with. Required. +PANEL_GROUP_DASHBOARD = 'management' diff --git a/stacktask_ui/enabled/_6050_management_project_users.py b/stacktask_ui/enabled/_6050_management_project_users.py new file mode 100644 index 0000000..31a0954 --- /dev/null +++ b/stacktask_ui/enabled/_6050_management_project_users.py @@ -0,0 +1,9 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'project_users' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'management' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'access_control' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'stacktask_ui.content.project_users.panel.ProjectUsers' diff --git a/stacktask_ui/enabled/__init__.py b/stacktask_ui/enabled/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/karma.conf.js b/stacktask_ui/karma.conf.js new file mode 100644 index 0000000..19c7cf0 --- /dev/null +++ b/stacktask_ui/karma.conf.js @@ -0,0 +1,155 @@ +/* + * 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. + */ + +'use strict'; + +var fs = require('fs'); +var path = require('path'); + +module.exports = function (config) { + // This tox venv is setup in the post-install npm step + var toxPath = '../.tox/py27/lib/python2.7/site-packages/'; + + config.set({ + preprocessors: { + // Used to collect templates for preprocessing. + // NOTE: the templates must also be listed in the files section below. + './static/**/*.html': ['ng-html2js'], + // Used to indicate files requiring coverage reports. + './static/**/!(*.spec).js': ['coverage'], + }, + + // Sets up module to process templates. + ngHtml2JsPreprocessor: { + prependPrefix: '/', + moduleName: 'templates' + }, + + basePath: './', + + // Contains both source and test files. + files: [ + /* + * shim, partly stolen from /i18n/js/horizon/ + * Contains expected items not provided elsewhere (dynamically by + * Django or via jasmine template. + */ + '../test-shim.js', + + // from jasmine.html + toxPath + 'xstatic/pkg/jquery/data/jquery.js', + toxPath + 'xstatic/pkg/angular/data/angular.js', + toxPath + 'xstatic/pkg/angular/data/angular-route.js', + toxPath + 'xstatic/pkg/angular/data/angular-mocks.js', + toxPath + 'xstatic/pkg/angular/data/angular-cookies.js', + toxPath + 'xstatic/pkg/angular_bootstrap/data/angular-bootstrap.js', + toxPath + 'xstatic/pkg/angular_gettext/data/angular-gettext.js', + toxPath + 'xstatic/pkg/angular/data/angular-sanitize.js', + toxPath + 'xstatic/pkg/d3/data/d3.js', + toxPath + 'xstatic/pkg/rickshaw/data/rickshaw.js', + toxPath + 'xstatic/pkg/angular_smart_table/data/smart-table.js', + toxPath + 'xstatic/pkg/angular_lrdragndrop/data/lrdragndrop.js', + toxPath + 'xstatic/pkg/spin/data/spin.js', + toxPath + 'xstatic/pkg/spin/data/spin.jquery.js', + toxPath + 'xstatic/pkg/tv4/data/tv4.js', + toxPath + 'xstatic/pkg/objectpath/data/ObjectPath.js', + toxPath + 'xstatic/pkg/angular_schema_form/data/schema-form.js', + + // TODO: These should be mocked. + toxPath + '/horizon/static/horizon/js/horizon.js', + + /** + * Include framework source code from horizon that we need. + * Otherwise, karma will not be able to find them when testing. + * These files should be mocked in the foreseeable future. + */ + toxPath + 'horizon/static/framework/**/*.module.js', + toxPath + 'horizon/static/framework/**/!(*.spec|*.mock).js', + toxPath + 'openstack_dashboard/static/**/*.module.js', + toxPath + 'openstack_dashboard/static/**/!(*.spec|*.mock).js', + toxPath + 'openstack_dashboard/dashboards/**/static/**/*.module.js', + toxPath + 'openstack_dashboard/dashboards/**/static/**/!(*.spec|*.mock).js', + + /** + * First, list all the files that defines application's angular modules. + * Those files have extension of `.module.js`. The order among them is + * not significant. + */ + './static/**/*.module.js', + + /** + * Followed by other JavaScript files that defines angular providers + * on the modules defined in files listed above. And they are not mock + * files or spec files defined below. The order among them is not + * significant. + */ + './static/**/!(*.spec|*.mock).js', + + /** + * Then, list files for mocks with `mock.js` extension. The order + * among them should not be significant. + */ + toxPath + 'openstack_dashboard/static/**/*.mock.js', + + /** + * Finally, list files for spec with `spec.js` extension. The order + * among them should not be significant. + */ + './static/**/*.spec.js', + + /** + * Angular external templates + */ + './static/**/*.html' + ], + + autoWatch: true, + + frameworks: ['jasmine'], + + browsers: ['PhantomJS'], + + browserNoActivityTimeout: 60000, + + phantomjsLauncher: { + // Have phantomjs exit if a ResourceError is encountered + // (useful if karma exits without killing phantom) + exitOnResourceError: true + }, + + reporters: ['progress', 'coverage', 'threshold'], + + plugins: [ + 'karma-phantomjs-launcher', + 'karma-jasmine', + 'karma-ng-html2js-preprocessor', + 'karma-coverage', + 'karma-threshold-reporter' + ], + + // Places coverage report in HTML format in the subdirectory below. + coverageReporter: { + type: 'html', + dir: '../cover/karma/' + }, + + // Coverage threshold values. + thresholdReporter: { + statements: 10, // target 100 + branches: 0, // target 100 + functions: 10, // target 100 + lines: 10 // target 100 + } + }); +}; diff --git a/stacktask_ui/test/__init__.py b/stacktask_ui/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/test/api_tests/__init__.py b/stacktask_ui/test/api_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/test/api_tests/rest_api_tests.py b/stacktask_ui/test/api_tests/rest_api_tests.py new file mode 100644 index 0000000..1476ded --- /dev/null +++ b/stacktask_ui/test/api_tests/rest_api_tests.py @@ -0,0 +1,28 @@ +# 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. + +import mock + +from openstack_dashboard.test.test_data import utils + +from stacktask_ui.test import test_data + +TEST = utils.TestData(test_data.data) + + +def mock_resource(resource): + """Utility function to make mocking more DRY""" + + mocked_data = \ + [mock.Mock(**{'to_dict.return_value': item}) for item in resource] + + return mocked_data diff --git a/stacktask_ui/test/helpers.py b/stacktask_ui/test/helpers.py new file mode 100644 index 0000000..11d203a --- /dev/null +++ b/stacktask_ui/test/helpers.py @@ -0,0 +1,20 @@ +# 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. + +from openstack_dashboard.test import helpers + + +class APITestCase(helpers.APITestCase): + """Extends the base Horizon APITestCase for stacktaskclient""" + + def setUp(self): + super(APITestCase, self).setUp() diff --git a/stacktask_ui/test/integration_tests/__init__.py b/stacktask_ui/test/integration_tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stacktask_ui/test/settings.py b/stacktask_ui/test/settings.py new file mode 100644 index 0000000..fa3555f --- /dev/null +++ b/stacktask_ui/test/settings.py @@ -0,0 +1,38 @@ +# 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. + +# Default to Horizons test settings to avoid any missing keys +from horizon.test.settings import * # noqa +from openstack_dashboard.test.settings import * # noqa + +# Update the dashboards with stacktask_ui +import openstack_dashboard.enabled +from openstack_dashboard.utils import settings + +import stacktask_ui.enabled + +# pop these keys to avoid log warnings about deprecation +# update_dashboards will populate them anyway +HORIZON_CONFIG.pop('dashboards', None) +HORIZON_CONFIG.pop('default_dashboard', None) + +settings.update_dashboards( + [ + stacktask_ui.enabled, + openstack_dashboard.enabled, + ], + HORIZON_CONFIG, + INSTALLED_APPS +) + +# Ensure any duplicate apps are removed after the update_dashboards call +INSTALLED_APPS = list(set(INSTALLED_APPS)) diff --git a/stacktask_ui/test/test_data.py b/stacktask_ui/test/test_data.py new file mode 100644 index 0000000..c1c0e62 --- /dev/null +++ b/stacktask_ui/test/test_data.py @@ -0,0 +1,18 @@ +# 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. + +from openstack_dashboard.test.test_data import utils + + +def data(TEST): + # Test Data Container in Horizon + TEST.queues = utils.TestDataContainer() diff --git a/stacktask_ui/version.py b/stacktask_ui/version.py new file mode 100644 index 0000000..a836e82 --- /dev/null +++ b/stacktask_ui/version.py @@ -0,0 +1,15 @@ +# 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. + +import pbr.version + +version_info = pbr.version.VersionInfo('stacktask-ui') diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..364025e --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,29 @@ +# The order of packages is significant, because pip processes them in the order +# of appearance. Changing the order has an impact on the overall integration +# process, which may cause wedges in the gate later. +# Order matters to the pip dependency resolver, so sorting this file +# changes how packages are installed. New dependencies should be +# added in alphabetical order, however, some dependencies may need to +# be installed in a specific order. +# +# Hacking should appear first in case something else depends on pep8 +hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0 +coverage>=4.0 # Apache-2.0 +django-nose>=1.4.4 # BSD +mock>=2.0 # BSD +mox3!=0.19.0,>=0.7.0 # Apache-2.0 +nodeenv>=0.9.4 # BSD +nose # LGPL +nose-exclude # LGPL +nosehtmloutput>=0.0.3 # Apache-2.0 +nosexcover # BSD +openstack.nose-plugin>=0.7 # Apache-2.0 +oslosphinx>=4.7.0 # Apache-2.0 +reno>=1.8.0 # Apache-2.0 +selenium>=2.50.1 # Apache-2.0 +sphinx>=1.5.1 # BSD +testtools>=1.4.0 # MIT +# This also needs xvfb library installed on your OS +xvfbwrapper>=0.1.3 #license: MIT +# Include horizon as test requirement +http://tarballs.openstack.org/horizon/horizon-master.tar.gz#egg=horizon diff --git a/test-shim.js b/test-shim.js new file mode 100644 index 0000000..918ff0d --- /dev/null +++ b/test-shim.js @@ -0,0 +1,97 @@ +/* + * Shim for Javascript unit tests; supplying expected global features. + * This should be removed from the codebase once i18n services are provided. + * Taken from default i18n file provided by Django. + */ + +var horizonPlugInModules = []; + + +(function (globals) { + + var django = globals.django || (globals.django = {}); + + + django.pluralidx = function (count) { return (count == 1) ? 0 : 1; }; + + /* gettext identity library */ + + django.gettext = function (msgid) { return msgid; }; + django.ngettext = function (singular, plural, count) { return (count == 1) ? singular : plural; }; + django.gettext_noop = function (msgid) { return msgid; }; + django.pgettext = function (context, msgid) { return msgid; }; + django.npgettext = function (context, singular, plural, count) { return (count == 1) ? singular : plural; }; + + + django.interpolate = function (fmt, obj, named) { + if (named) { + return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); + } else { + return fmt.replace(/%s/g, function(match){return String(obj.shift())}); + } + }; + + + /* formatting library */ + + django.formats = { + "DATETIME_FORMAT": "N j, Y, P", + "DATETIME_INPUT_FORMATS": [ + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M:%S.%f", + "%Y-%m-%d %H:%M", + "%Y-%m-%d", + "%m/%d/%Y %H:%M:%S", + "%m/%d/%Y %H:%M:%S.%f", + "%m/%d/%Y %H:%M", + "%m/%d/%Y", + "%m/%d/%y %H:%M:%S", + "%m/%d/%y %H:%M:%S.%f", + "%m/%d/%y %H:%M", + "%m/%d/%y" + ], + "DATE_FORMAT": "N j, Y", + "DATE_INPUT_FORMATS": [ + "%Y-%m-%d", + "%m/%d/%Y", + "%m/%d/%y" + ], + "DECIMAL_SEPARATOR": ".", + "FIRST_DAY_OF_WEEK": "0", + "MONTH_DAY_FORMAT": "F j", + "NUMBER_GROUPING": "3", + "SHORT_DATETIME_FORMAT": "m/d/Y P", + "SHORT_DATE_FORMAT": "m/d/Y", + "THOUSAND_SEPARATOR": ",", + "TIME_FORMAT": "P", + "TIME_INPUT_FORMATS": [ + "%H:%M:%S", + "%H:%M:%S.%f", + "%H:%M" + ], + "YEAR_MONTH_FORMAT": "F Y" + }; + + django.get_format = function (format_type) { + var value = django.formats[format_type]; + if (typeof(value) == 'undefined') { + return format_type; + } else { + return value; + } + }; + + /* add to global namespace */ + globals.pluralidx = django.pluralidx; + globals.gettext = django.gettext; + globals.ngettext = django.ngettext; + globals.gettext_noop = django.gettext_noop; + globals.pgettext = django.pgettext; + globals.npgettext = django.npgettext; + globals.interpolate = django.interpolate; + globals.get_format = django.get_format; + globals.STATIC_URL = '/static/'; + globals.WEBROOT = '/'; + +}(this)); + diff --git a/tools/install_venv.py b/tools/install_venv.py new file mode 100644 index 0000000..e96521e --- /dev/null +++ b/tools/install_venv.py @@ -0,0 +1,71 @@ +# Copyright 2010 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2010 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +import os +import sys + +import install_venv_common as install_venv # noqa + + +def print_help(venv, root): + help = """ + OpenStack development environment setup is complete. + + OpenStack development uses virtualenv to track and manage Python + dependencies while in development and testing. + + To activate the OpenStack virtualenv for the extent of your current shell + session you can run: + + $ source %s/bin/activate + + Or, if you prefer, you can run commands in the virtualenv on a case by case + basis by running: + + $ %s/tools/with_venv.sh + + Also, make test will automatically use the virtualenv. + """ + print(help % (venv, root)) + + +def main(argv): + root = os.path.dirname(os.path.dirname(os.path.realpath(__file__))) + + if os.environ.get('tools_path'): + root = os.environ['tools_path'] + venv = os.path.join(root, '.venv') + if os.environ.get('venv'): + venv = os.environ['venv'] + + pip_requires = os.path.join(root, 'requirements.txt') + test_requires = os.path.join(root, 'test-requirements.txt') + py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1]) + project = 'OpenStack' + install = install_venv.InstallVenv(root, venv, pip_requires, test_requires, + py_version, project) + options = install.parse_args(argv) + install.check_python_version() + install.check_dependencies() + install.create_virtualenv(no_site_packages=options.no_site_packages) + install.install_dependencies() + print_help(venv, root) + +if __name__ == '__main__': + main(sys.argv) diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py new file mode 100644 index 0000000..e279159 --- /dev/null +++ b/tools/install_venv_common.py @@ -0,0 +1,172 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2013 IBM Corp. +# +# 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. + +"""Provides methods needed by installation script for OpenStack development +virtual environments. + +Since this script is used to bootstrap a virtualenv from the system's Python +environment, it should be kept strictly compatible with Python 2.6. + +Synced in from openstack-common +""" + +from __future__ import print_function + +import optparse +import os +import subprocess +import sys + + +class InstallVenv(object): + + def __init__(self, root, venv, requirements, + test_requirements, py_version, + project): + self.root = root + self.venv = venv + self.requirements = requirements + self.test_requirements = test_requirements + self.py_version = py_version + self.project = project + + def die(self, message, *args): + print(message % args, file=sys.stderr) + sys.exit(1) + + def check_python_version(self): + if sys.version_info < (2, 6): + self.die("Need Python Version >= 2.6") + + def run_command_with_code(self, cmd, redirect_output=True, + check_exit_code=True): + """Runs a command in an out-of-process shell. + + Returns the output of that command. Working directory is self.root. + """ + if redirect_output: + stdout = subprocess.PIPE + else: + stdout = None + + proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout) + output = proc.communicate()[0] + if check_exit_code and proc.returncode != 0: + self.die('Command "%s" failed.\n%s', ' '.join(cmd), output) + return (output, proc.returncode) + + def run_command(self, cmd, redirect_output=True, check_exit_code=True): + return self.run_command_with_code(cmd, redirect_output, + check_exit_code)[0] + + def get_distro(self): + if (os.path.exists('/etc/fedora-release') or + os.path.exists('/etc/redhat-release')): + return Fedora( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + else: + return Distro( + self.root, self.venv, self.requirements, + self.test_requirements, self.py_version, self.project) + + def check_dependencies(self): + self.get_distro().install_virtualenv() + + def create_virtualenv(self, no_site_packages=True): + """Creates the virtual environment and installs PIP. + + Creates the virtual environment and installs PIP only into the + virtual environment. + """ + if not os.path.isdir(self.venv): + print('Creating venv...', end=' ') + if no_site_packages: + self.run_command(['virtualenv', '-q', '--no-site-packages', + self.venv]) + else: + self.run_command(['virtualenv', '-q', self.venv]) + print('done.') + else: + print("venv already exists...") + pass + + def pip_install(self, *args): + self.run_command(['tools/with_venv.sh', + 'pip', 'install', '--upgrade'] + list(args), + redirect_output=False) + + def install_dependencies(self): + print('Installing dependencies with pip (this can take a while)...') + + # First things first, make sure our venv has the latest pip and + # setuptools and pbr + self.pip_install('pip>=1.4') + self.pip_install('setuptools') + self.pip_install('pbr') + + self.pip_install('-r', self.requirements, '-r', self.test_requirements) + + def parse_args(self, argv): + """Parses command-line arguments.""" + parser = optparse.OptionParser() + parser.add_option('-n', '--no-site-packages', + action='store_true', + help="Do not inherit packages from global Python " + "install.") + return parser.parse_args(argv[1:])[0] + + +class Distro(InstallVenv): + + def check_cmd(self, cmd): + return bool(self.run_command(['which', cmd], + check_exit_code=False).strip()) + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if self.check_cmd('easy_install'): + print('Installing virtualenv via easy_install...', end=' ') + if self.run_command(['easy_install', 'virtualenv']): + print('Succeeded') + return + else: + print('Failed') + + self.die('ERROR: virtualenv not found.\n\n%s development' + ' requires virtualenv, please install it using your' + ' favorite package management tool' % self.project) + + +class Fedora(Distro): + """This covers all Fedora-based distributions. + + Includes: Fedora, RHEL, CentOS, Scientific Linux + """ + + def check_pkg(self, pkg): + return self.run_command_with_code(['rpm', '-q', pkg], + check_exit_code=False)[1] == 0 + + def install_virtualenv(self): + if self.check_cmd('virtualenv'): + return + + if not self.check_pkg('python-virtualenv'): + self.die("Please install 'python-virtualenv'.") + + super(Fedora, self).install_virtualenv() diff --git a/tools/with_venv.sh b/tools/with_venv.sh new file mode 100755 index 0000000..f4170c9 --- /dev/null +++ b/tools/with_venv.sh @@ -0,0 +1,13 @@ +#!/bin/bash +TOOLS_PATH=${TOOLS_PATH:-$(dirname $0)} +VENV_PATH=${VENV_PATH:-${TOOLS_PATH}} +VENV_DIR=${VENV_NAME:-/../.venv} +TOOLS=${TOOLS_PATH} +VENV=${VENV:-${VENV_PATH}/${VENV_DIR}} +HORIZON_DIR=${TOOLS%/tools} + +# This horrible mangling of the PYTHONPATH is required to get the +# babel-angular-gettext extractor to work. To fix this the extractor needs to +# be packaged on pypi and added to global requirements. That work is in progress. +export PYTHONPATH="$HORIZON_DIR" +source ${VENV}/bin/activate && "$@" diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..164ca42 --- /dev/null +++ b/tox.ini @@ -0,0 +1,81 @@ +[tox] +envlist = py27,py27dj18,pep8,py35 +minversion = 1.6 +skipsdist = True + +[testenv] +usedevelop = True +setenv = VIRTUAL_ENV={envdir} + NOSE_WITH_OPENSTACK=1 + NOSE_OPENSTACK_COLOR=1 + NOSE_OPENSTACK_RED=0.05 + NOSE_OPENSTACK_YELLOW=0.025 + NOSE_OPENSTACK_SHOW_ELAPSED=1 + DJANGO_SETTINGS_MODULE=stacktask_ui.test.settings +install_command = pip install -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} -U {opts} {packages} +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = python manage.py test {posargs} + +[testenv:pep8] +commands = flake8 {posargs} + +[testenv:venv] +commands = {posargs} + +[testenv:cover] +commands = + coverage erase + coverage run {toxinidir}/manage.py test stacktask_ui + coverage xml --omit '.tox/cover/*' -o 'cover/coverage.xml' + coverage html --omit '.tox/cover/*' -d 'cover/htmlcov' + +[testenv:py27dj18] +basepython = python2.7 +commands = + pip install django>=1.8,<1.9 + python manage.py test {posargs} + +[testenv:eslint] +whitelist_externals = npm +commands = + npm install + npm run {posargs:postinstall} + npm run {posargs:lint} + +[testenv:karma] +whitelist_externals = npm +commands = + npm install + npm run {posargs:postinstall} + npm run {posargs:test} + +[testenv:docs] +commands = python setup.py build_sphinx + +[flake8] +exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,panel_template,dash_template,local_settings.py,*/local/*,*/test/test_plugins/*,.ropeproject,node_modules +max-complexity = 20 + +[hacking] +import_exceptions = collections.defaultdict, + django.conf.settings, + django.conf.urls.include, + django.conf.urls.patterns, + django.conf.urls.url, + django.core.urlresolvers.reverse, + django.core.urlresolvers.reverse_lazy, + django.template.loader.render_to_string, + django.test.utils.override_settings, + django.utils.datastructures.SortedDict, + django.utils.encoding.force_text, + django.utils.html.conditional_escape, + django.utils.html.escape, + django.utils.http.urlencode, + django.utils.safestring.mark_safe, + django.utils.translation.npgettext_lazy, + django.utils.translation.pgettext_lazy, + django.utils.translation.ugettext_lazy, + django.utils.translation.ungettext_lazy, + operator.attrgetter, + StringIO.StringIO