From 751057898e95f6110ed45a670923b68d794e4c26 Mon Sep 17 00:00:00 2001 From: Carmelo Romeo Date: Mon, 7 May 2018 19:08:21 +0200 Subject: [PATCH] start Change-Id: I5c224a7d6de3858ea6046c1b6d7a17c3eaf2380b --- CONTRIBUTING.rst | 14 + HACKING.rst | 4 + LICENSE | 176 +++++++ MANIFEST.in | 9 + README.rst | 64 +++ babel-django.cfg | 5 + babel-djangojs.cfg | 14 + doc/Makefile | 152 ++++++ doc/source/conf.py | 441 ++++++++++++++++ doc/source/configuration/index.rst | 10 + doc/source/contributor/api.rst | 9 + doc/source/contributor/index.rst | 16 + doc/source/index.rst | 33 ++ doc/source/install/index.rst | 53 ++ iotronic_ui.egg-info/PKG-INFO | 85 +++ iotronic_ui.egg-info/SOURCES.txt | 88 ++++ iotronic_ui.egg-info/dependency_links.txt | 1 + iotronic_ui.egg-info/not-zip-safe | 1 + iotronic_ui.egg-info/pbr.json | 1 + iotronic_ui.egg-info/requires.txt | 6 + iotronic_ui.egg-info/top_level.txt | 1 + iotronic_ui/__init__.py | 1 + iotronic_ui/api/iotronic.py | 148 ++++++ iotronic_ui/enabled/_6000_iot.py | 29 + iotronic_ui/enabled/_6010_iot_boards_panel.py | 23 + .../enabled/_6020_iot_plugins_panel.py | 23 + iotronic_ui/iot/__init__.py | 1 + iotronic_ui/iot/boards/__init__.py | 0 iotronic_ui/iot/boards/forms.py | 197 +++++++ iotronic_ui/iot/boards/panel.py | 33 ++ iotronic_ui/iot/boards/tables.py | 123 +++++ iotronic_ui/iot/boards/tabs.py | 47 ++ .../iot/boards/templates/boards/_create.html | 8 + .../templates/boards/_detail_overview.html | 51 ++ .../templates/boards/_removeplugins.html | 8 + .../iot/boards/templates/boards/_update.html | 7 + .../iot/boards/templates/boards/create.html | 7 + .../iot/boards/templates/boards/index.html | 7 + .../templates/boards/removeplugins.html | 7 + .../iot/boards/templates/boards/update.html | 7 + iotronic_ui/iot/boards/tests.py | 19 + iotronic_ui/iot/boards/urls.py | 27 + iotronic_ui/iot/boards/views.py | 329 ++++++++++++ iotronic_ui/iot/dashboard.py | 26 + iotronic_ui/iot/plugins/__init__.py | 0 iotronic_ui/iot/plugins/forms.py | 495 ++++++++++++++++++ iotronic_ui/iot/plugins/panel.py | 22 + iotronic_ui/iot/plugins/tables.py | 176 +++++++ iotronic_ui/iot/plugins/tabs.py | 36 ++ .../iot/plugins/templates/plugins/_call.html | 7 + .../plugins/templates/plugins/_create.html | 7 + .../templates/plugins/_detail_overview.html | 16 + .../plugins/templates/plugins/_inject.html | 7 + .../plugins/templates/plugins/_remove.html | 7 + .../iot/plugins/templates/plugins/_start.html | 7 + .../iot/plugins/templates/plugins/_stop.html | 7 + .../plugins/templates/plugins/_update.html | 7 + .../iot/plugins/templates/plugins/call.html | 7 + .../iot/plugins/templates/plugins/create.html | 7 + .../iot/plugins/templates/plugins/index.html | 12 + .../iot/plugins/templates/plugins/inject.html | 7 + .../iot/plugins/templates/plugins/remove.html | 7 + .../iot/plugins/templates/plugins/start.html | 7 + .../iot/plugins/templates/plugins/stop.html | 7 + .../iot/plugins/templates/plugins/update.html | 7 + iotronic_ui/iot/plugins/tests.py | 19 + iotronic_ui/iot/plugins/urls.py | 35 ++ iotronic_ui/iot/plugins/views.py | 413 +++++++++++++++ .../iot/static/iot/images/blue-circle.png | Bin 0 -> 16224 bytes .../iot/static/iot/images/green-circle.png | Bin 0 -> 16582 bytes .../static/iot/images/marker-icon-green.png | Bin 0 -> 1822 bytes .../iot/static/iot/images/marker-icon-red.png | Bin 0 -> 4518 bytes .../iot/static/iot/images/marker-icon.png | Bin 0 -> 1747 bytes .../iot/static/iot/images/marker-shadow.png | Bin 0 -> 797 bytes .../iot/static/iot/images/red-circle.png | Bin 0 -> 16267 bytes iotronic_ui/iot/static/iot/js/iot.js | 122 +++++ iotronic_ui/iot/static/iot/scss/iot.scss | 7 + iotronic_ui/iot/templates/iot/base.html | 10 + manage.py | 23 + package.json | 32 ++ requirements.txt | 15 + setup.cfg | 29 + setup.py | 27 + test-requirements.txt | 15 + test-shim.js | 96 ++++ tools/tox_install.sh | 64 +++ tools/tox_install.sh_ORIG | 88 ++++ tox.ini | 39 ++ 88 files changed, 4200 insertions(+) create mode 100644 CONTRIBUTING.rst create mode 100644 HACKING.rst 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/configuration/index.rst create mode 100644 doc/source/contributor/api.rst create mode 100644 doc/source/contributor/index.rst create mode 100644 doc/source/index.rst create mode 100644 doc/source/install/index.rst create mode 100644 iotronic_ui.egg-info/PKG-INFO create mode 100644 iotronic_ui.egg-info/SOURCES.txt create mode 100644 iotronic_ui.egg-info/dependency_links.txt create mode 100644 iotronic_ui.egg-info/not-zip-safe create mode 100644 iotronic_ui.egg-info/pbr.json create mode 100644 iotronic_ui.egg-info/requires.txt create mode 100644 iotronic_ui.egg-info/top_level.txt create mode 100644 iotronic_ui/__init__.py create mode 100644 iotronic_ui/api/iotronic.py create mode 100644 iotronic_ui/enabled/_6000_iot.py create mode 100644 iotronic_ui/enabled/_6010_iot_boards_panel.py create mode 100644 iotronic_ui/enabled/_6020_iot_plugins_panel.py create mode 100644 iotronic_ui/iot/__init__.py create mode 100644 iotronic_ui/iot/boards/__init__.py create mode 100644 iotronic_ui/iot/boards/forms.py create mode 100644 iotronic_ui/iot/boards/panel.py create mode 100644 iotronic_ui/iot/boards/tables.py create mode 100644 iotronic_ui/iot/boards/tabs.py create mode 100644 iotronic_ui/iot/boards/templates/boards/_create.html create mode 100644 iotronic_ui/iot/boards/templates/boards/_detail_overview.html create mode 100644 iotronic_ui/iot/boards/templates/boards/_removeplugins.html create mode 100644 iotronic_ui/iot/boards/templates/boards/_update.html create mode 100644 iotronic_ui/iot/boards/templates/boards/create.html create mode 100644 iotronic_ui/iot/boards/templates/boards/index.html create mode 100644 iotronic_ui/iot/boards/templates/boards/removeplugins.html create mode 100644 iotronic_ui/iot/boards/templates/boards/update.html create mode 100644 iotronic_ui/iot/boards/tests.py create mode 100644 iotronic_ui/iot/boards/urls.py create mode 100644 iotronic_ui/iot/boards/views.py create mode 100644 iotronic_ui/iot/dashboard.py create mode 100644 iotronic_ui/iot/plugins/__init__.py create mode 100644 iotronic_ui/iot/plugins/forms.py create mode 100644 iotronic_ui/iot/plugins/panel.py create mode 100644 iotronic_ui/iot/plugins/tables.py create mode 100644 iotronic_ui/iot/plugins/tabs.py create mode 100644 iotronic_ui/iot/plugins/templates/plugins/_call.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/_create.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/_inject.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/_remove.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/_start.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/_stop.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/_update.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/call.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/create.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/index.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/inject.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/remove.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/start.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/stop.html create mode 100644 iotronic_ui/iot/plugins/templates/plugins/update.html create mode 100644 iotronic_ui/iot/plugins/tests.py create mode 100644 iotronic_ui/iot/plugins/urls.py create mode 100644 iotronic_ui/iot/plugins/views.py create mode 100644 iotronic_ui/iot/static/iot/images/blue-circle.png create mode 100644 iotronic_ui/iot/static/iot/images/green-circle.png create mode 100644 iotronic_ui/iot/static/iot/images/marker-icon-green.png create mode 100644 iotronic_ui/iot/static/iot/images/marker-icon-red.png create mode 100644 iotronic_ui/iot/static/iot/images/marker-icon.png create mode 100644 iotronic_ui/iot/static/iot/images/marker-shadow.png create mode 100644 iotronic_ui/iot/static/iot/images/red-circle.png create mode 100644 iotronic_ui/iot/static/iot/js/iot.js create mode 100644 iotronic_ui/iot/static/iot/scss/iot.scss create mode 100644 iotronic_ui/iot/templates/iot/base.html create mode 100755 manage.py create mode 100644 package.json create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 test-requirements.txt create mode 100644 test-shim.js create mode 100755 tools/tox_install.sh create mode 100755 tools/tox_install.sh_ORIG create mode 100644 tox.ini diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..b361ea3 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,14 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: +https://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: +https://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: +https://bugs.launchpad.net/iotronic_ui diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..3d43f6e --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +iotronic_ui Style Commandments +=============================================== + +Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ 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..7ef006d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,9 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview +include setup.py + +recursive-include iotronic_ui *.js *.html *.scss + +global-exclude *.pyc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..b0de1eb --- /dev/null +++ b/README.rst @@ -0,0 +1,64 @@ +=============================== +IoTronic Panels +=============================== + +Iotronic plugin for the OpenStack Dashboard + +* Free software: Apache license +* Source: http://git.openstack.org/cgit/openstack/iotronic_ui +* Bugs: http://bugs.launchpad.net/None + +Features +-------- + +* TODO + +Enabling in DevStack +-------------------- + +Add this repo as an external repository into your ``local.conf`` file:: + + [[local|localrc]] + enable_plugin iotronic_ui https://github.com/openstack/iotronic_ui + +Manual Installation +------------------- + +Begin by cloning the Horizon and IoTronic Panels repositories:: + + git clone https://github.com/openstack/horizon + git clone https://github.com/openstack/iotronic_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.) + +Install IoTronic Panels with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -e ../iotronic_ui/ + +And enable it in Horizon:: + + ln -s ../iotronic_ui/iotronic_ui/enabled/_90_project_iot_panelgroup.py openstack_dashboard/local/enabled + ln -s ../iotronic_ui/iotronic_ui/enabled/_91_project_iot_boardss_panel.py openstack_dashboard/local/enabled + +To run horizon with the newly enabled IoTronic Panels 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..e78d6c0 --- /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..283be3b --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,152 @@ +# 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/iotronic_ui.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/iotronic_ui.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/iotronic_ui" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/iotronic_ui" + @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..e672ab5 --- /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 sys + +import django + +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', + 'iotronic_ui.test.settings') + +# Starting in Django 1.7, standalone scripts, such as a sphinx build +# require that django.setup() be called first. +# https://docs.djangoproject.com/en/1.8/releases/1.7/#standalone-scripts +django.setup() + + +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, "contributor/api")) + SRCS = [('iotronic_ui', 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', + 'openstackdocstheme', + ] + +# 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'IoTronic Panels' +copyright = u'2017, OpenStack Foundation' + +# Release notes are version independent. +# The full version, including alpha/beta/rc tags. +release = '' +# The short X.Y version. +version = '' + +# 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 = 'openstackdocs' + +# openstackdocstheme options +repository_name = 'openstack/iotronic_ui' +bug_project = 'None' +bug_tag = '' + +# 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 = {} + +# 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'] + +# Must set this variable to include year, month, day, hours, and minutes. +html_last_updated_fmt = '%Y-%m-%d %H:%M' + +# 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 = 'iotronic_uidoc' + + +# -- 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', 'iotronic_ui.tex', + u'IoTronic Panels 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'IoTronic Panels Documentation', + 'Documentation for the IoTronic Panels 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', 'IoTronic Panels', + u'IoTronic Panels Documentation', u'OpenStack', + 'IoTronic Panels', + 'Iotronic plugin for the OpenStack Dashboard', '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'IoTronic Panels' +epub_author = u'OpenStack' +epub_publisher = u'OpenStack' +epub_copyright = u'2017, 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/configuration/index.rst b/doc/source/configuration/index.rst new file mode 100644 index 0000000..e9132e9 --- /dev/null +++ b/doc/source/configuration/index.rst @@ -0,0 +1,10 @@ +============= +Configuration +============= + +IoTronic Panels has no configuration option. + +For more configurations, see +`Deployment & Configuration +`__ +in the Horizon documentation. diff --git a/doc/source/contributor/api.rst b/doc/source/contributor/api.rst new file mode 100644 index 0000000..dd3be26 --- /dev/null +++ b/doc/source/contributor/api.rst @@ -0,0 +1,9 @@ +===================== +Source Code Reference +===================== + +.. toctree:: + :maxdepth: 1 + :glob: + + api/* diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst new file mode 100644 index 0000000..4c2b48b --- /dev/null +++ b/doc/source/contributor/index.rst @@ -0,0 +1,16 @@ +================= +Contributor Guide +================= + +There is no topic specific to IoTronic Panels now. + +See `Horizon Contributor Documentation +`__ +for general topic on developing a dashboard on horizon. + +---- + +.. toctree:: + :maxdepth: 1 + + api diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..a90a3df --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,33 @@ +=============================== +IoTronic Panels +=============================== + +Iotronic plugin for the OpenStack Dashboard + +* Free software: Apache license +* Source: http://git.openstack.org/cgit/openstack/iotronic_ui +* Bugs: http://bugs.launchpad.net/None + +Features +-------- + +* TODO + +User Documentation +------------------ + +.. toctree:: + :maxdepth: 2 + + install/index + configuration/index + Release Notes + +Contributor Guide +----------------- + +.. toctree:: + :glob: + :maxdepth: 2 + + contributor/index diff --git a/doc/source/install/index.rst b/doc/source/install/index.rst new file mode 100644 index 0000000..155f82c --- /dev/null +++ b/doc/source/install/index.rst @@ -0,0 +1,53 @@ +============ +Installation +============ + +Enabling in DevStack +-------------------- + +Add this repo as an external repository into your ``local.conf`` file:: + + [[local|localrc]] + enable_plugin iotronic_ui https://github.com/openstack/iotronic_ui + +Manual Installation +------------------- + +Begin by cloning the Horizon and IoTronic Panels repositories:: + + git clone https://github.com/openstack/horizon + git clone https://github.com/openstack/iotronic_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.) + +Install IoTronic Panels with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -e ../iotronic_ui/ + +And enable it in Horizon:: + + ln -s ../iotronic_ui/iotronic_ui/enabled/_90_project_iot_panelgroup.py openstack_dashboard/local/enabled + ln -s ../iotronic_ui/iotronic_ui/enabled/_91_project_iot_boardss_panel.py openstack_dashboard/local/enabled + +To run horizon with the newly enabled IoTronic Panels 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/iotronic_ui.egg-info/PKG-INFO b/iotronic_ui.egg-info/PKG-INFO new file mode 100644 index 0000000..df40467 --- /dev/null +++ b/iotronic_ui.egg-info/PKG-INFO @@ -0,0 +1,85 @@ +Metadata-Version: 1.1 +Name: iotronic-ui +Version: 0.0.0 +Summary: Iotronic plugin for the OpenStack Dashboard +Home-page: http://www.openstack.org/ +Author: OpenStack +Author-email: openstack-dev@lists.openstack.org +License: UNKNOWN +Description: =============================== + IoTronic Panels + =============================== + + Iotronic plugin for the OpenStack Dashboard + + * Free software: Apache license + * Source: http://git.openstack.org/cgit/openstack/iotronic_ui + * Bugs: http://bugs.launchpad.net/None + + Features + -------- + + * TODO + + Enabling in DevStack + -------------------- + + Add this repo as an external repository into your ``local.conf`` file:: + + [[local|localrc]] + enable_plugin iotronic_ui https://github.com/openstack/iotronic_ui + + Manual Installation + ------------------- + + Begin by cloning the Horizon and IoTronic Panels repositories:: + + git clone https://github.com/openstack/horizon + git clone https://github.com/openstack/iotronic_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.) + + Install IoTronic Panels with all dependencies in your virtual environment:: + + tools/with_venv.sh pip install -e ../iotronic_ui/ + + And enable it in Horizon:: + + ln -s ../iotronic_ui/iotronic_ui/enabled/_90_project_iot_panelgroup.py openstack_dashboard/local/enabled + ln -s ../iotronic_ui/iotronic_ui/enabled/_91_project_iot_boardss_panel.py openstack_dashboard/local/enabled + + To run horizon with the newly enabled IoTronic Panels 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/ + + +Platform: UNKNOWN +Classifier: Environment :: OpenStack +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Operating System :: POSIX :: Linux +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.5 diff --git a/iotronic_ui.egg-info/SOURCES.txt b/iotronic_ui.egg-info/SOURCES.txt new file mode 100644 index 0000000..1e94740 --- /dev/null +++ b/iotronic_ui.egg-info/SOURCES.txt @@ -0,0 +1,88 @@ +CONTRIBUTING.rst +HACKING.rst +LICENSE +MANIFEST.in +README.rst +babel-django.cfg +babel-djangojs.cfg +manage.py +package.json +requirements.txt +setup.cfg +setup.py +test-requirements.txt +test-shim.js +tox.ini +doc/Makefile +doc/source/conf.py +doc/source/index.rst +doc/source/configuration/index.rst +doc/source/contributor/api.rst +doc/source/contributor/index.rst +doc/source/install/index.rst +iotronic_ui/__init__.py +iotronic_ui.egg-info/PKG-INFO +iotronic_ui.egg-info/SOURCES.txt +iotronic_ui.egg-info/dependency_links.txt +iotronic_ui.egg-info/not-zip-safe +iotronic_ui.egg-info/pbr.json +iotronic_ui.egg-info/requires.txt +iotronic_ui.egg-info/top_level.txt +iotronic_ui/api/iotronic.py +iotronic_ui/enabled/_6000_iot.py +iotronic_ui/enabled/_6010_iot_boards_panel.py +iotronic_ui/enabled/_6020_iot_plugins_panel.py +iotronic_ui/iot/__init__.py +iotronic_ui/iot/dashboard.py +iotronic_ui/iot/boards/__init__.py +iotronic_ui/iot/boards/forms.py +iotronic_ui/iot/boards/panel.py +iotronic_ui/iot/boards/tables.py +iotronic_ui/iot/boards/tabs.py +iotronic_ui/iot/boards/tests.py +iotronic_ui/iot/boards/urls.py +iotronic_ui/iot/boards/views.py +iotronic_ui/iot/boards/templates/boards/_create.html +iotronic_ui/iot/boards/templates/boards/_detail_overview.html +iotronic_ui/iot/boards/templates/boards/_removeplugins.html +iotronic_ui/iot/boards/templates/boards/_update.html +iotronic_ui/iot/boards/templates/boards/create.html +iotronic_ui/iot/boards/templates/boards/index.html +iotronic_ui/iot/boards/templates/boards/removeplugins.html +iotronic_ui/iot/boards/templates/boards/update.html +iotronic_ui/iot/plugins/__init__.py +iotronic_ui/iot/plugins/forms.py +iotronic_ui/iot/plugins/panel.py +iotronic_ui/iot/plugins/tables.py +iotronic_ui/iot/plugins/tabs.py +iotronic_ui/iot/plugins/tests.py +iotronic_ui/iot/plugins/urls.py +iotronic_ui/iot/plugins/views.py +iotronic_ui/iot/plugins/templates/plugins/_call.html +iotronic_ui/iot/plugins/templates/plugins/_create.html +iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html +iotronic_ui/iot/plugins/templates/plugins/_inject.html +iotronic_ui/iot/plugins/templates/plugins/_remove.html +iotronic_ui/iot/plugins/templates/plugins/_start.html +iotronic_ui/iot/plugins/templates/plugins/_stop.html +iotronic_ui/iot/plugins/templates/plugins/_update.html +iotronic_ui/iot/plugins/templates/plugins/call.html +iotronic_ui/iot/plugins/templates/plugins/create.html +iotronic_ui/iot/plugins/templates/plugins/index.html +iotronic_ui/iot/plugins/templates/plugins/inject.html +iotronic_ui/iot/plugins/templates/plugins/remove.html +iotronic_ui/iot/plugins/templates/plugins/start.html +iotronic_ui/iot/plugins/templates/plugins/stop.html +iotronic_ui/iot/plugins/templates/plugins/update.html +iotronic_ui/iot/static/iot/images/blue-circle.png +iotronic_ui/iot/static/iot/images/green-circle.png +iotronic_ui/iot/static/iot/images/marker-icon-green.png +iotronic_ui/iot/static/iot/images/marker-icon-red.png +iotronic_ui/iot/static/iot/images/marker-icon.png +iotronic_ui/iot/static/iot/images/marker-shadow.png +iotronic_ui/iot/static/iot/images/red-circle.png +iotronic_ui/iot/static/iot/js/iot.js +iotronic_ui/iot/static/iot/scss/iot.scss +iotronic_ui/iot/templates/iot/base.html +tools/tox_install.sh +tools/tox_install.sh_ORIG \ No newline at end of file diff --git a/iotronic_ui.egg-info/dependency_links.txt b/iotronic_ui.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/iotronic_ui.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/iotronic_ui.egg-info/not-zip-safe b/iotronic_ui.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/iotronic_ui.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/iotronic_ui.egg-info/pbr.json b/iotronic_ui.egg-info/pbr.json new file mode 100644 index 0000000..34d2e6e --- /dev/null +++ b/iotronic_ui.egg-info/pbr.json @@ -0,0 +1 @@ +{"git_version": "4902c1d", "is_release": false} \ No newline at end of file diff --git a/iotronic_ui.egg-info/requires.txt b/iotronic_ui.egg-info/requires.txt new file mode 100644 index 0000000..1725ef9 --- /dev/null +++ b/iotronic_ui.egg-info/requires.txt @@ -0,0 +1,6 @@ +pbr!=2.1.0,>=2.0.0 +Babel!=2.4.0,>=2.3.4 +Django<2.0,>=1.8 +django-babel>=0.5.1 +django-compressor>=2.0 +django-pyscss>=2.0.2 diff --git a/iotronic_ui.egg-info/top_level.txt b/iotronic_ui.egg-info/top_level.txt new file mode 100644 index 0000000..7d4fe56 --- /dev/null +++ b/iotronic_ui.egg-info/top_level.txt @@ -0,0 +1 @@ +iotronic_ui diff --git a/iotronic_ui/__init__.py b/iotronic_ui/__init__.py new file mode 100644 index 0000000..b8023d8 --- /dev/null +++ b/iotronic_ui/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.1' diff --git a/iotronic_ui/api/iotronic.py b/iotronic_ui/api/iotronic.py new file mode 100644 index 0000000..9f69941 --- /dev/null +++ b/iotronic_ui/api/iotronic.py @@ -0,0 +1,148 @@ +# 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 OrderedDict +# import threading + +from iotronicclient import client as iotronic_client +# from django.conf import settings +# from django.utils.translation import ugettext_lazy as _ + +# from horizon import exceptions +from horizon.utils.memoized import memoized # noqa + +from openstack_dashboard.api import base +# from openstack_dashboard.api import keystone + + +# TESTING +import logging +LOG = logging.getLogger(__name__) + + +@memoized +def iotronicclient(request): + """Initialization of Iotronic client.""" + + endpoint = base.url_for(request, 'iot') + # insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) + # cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None) + + return iotronic_client.Client('1', endpoint, token=request.user.token.id) + + +# BOARD MANAGEMENT +def board_list(request, status=None, detail=None, project=None): + """List boards.""" + boards = iotronicclient(request).board.list(status, detail, project) + return boards + + +def board_get(request, board_id, fields): + """Get board info.""" + board = iotronicclient(request).board.get(board_id, fields) + return board + + +def board_create(request, code, mobile, location, type, name): + """Create board.""" + params = {"code": code, + "mobile": mobile, + "location": location, + "type": type, + "name": name} + board = iotronicclient(request).board.create(**params) + return board + + +def board_update(request, board_id, patch): + """Update board.""" + board = iotronicclient(request).board.update(board_id, patch) + return board + + +def board_delete(request, board_id): + """Delete board.""" + board = iotronicclient(request).board.delete(board_id) + return board + + +# PLUGIN MANAGEMENT (Cloud Side) +def plugin_list(request, detail=None, project=None, with_public=False, + all_plugins=False): + """List plugins.""" + plugin = iotronicclient(request).plugin() + plugins = plugin.list(detail, project, + with_public=with_public, + all_plugins=all_plugins) + return plugins + + +def plugin_get(request, plugin_id, fields): + """Get plugin info.""" + plugin = iotronicclient(request).plugin.get(plugin_id, fields) + return plugin + + +def plugin_create(request, name, public, callable, code, parameters): + """Create plugin.""" + params = {"name": name, + "public": public, + "callable": callable, + "code": code, + "parameters": parameters} + plugin = iotronicclient(request).plugin.create(**params) + return plugin + + +def plugin_update(request, plugin_id, patch): + """Update plugin.""" + plugin = iotronicclient(request).plugin.update(plugin_id, patch) + return plugin + + +def plugin_delete(request, plugin_id): + """Delete plugin.""" + plugin = iotronicclient(request).plugin.delete(plugin_id) + return plugin + + +# PLUGIN MANAGEMENT (Board Side) +def plugin_inject(request, board_id, plugin_id, onboot): + """Inject plugin on board(s).""" + plugin_injection = iotronicclient(request).plugin_injection() + plugin = plugin_injection.plugin_inject(board_id, plugin_id, onboot) + return plugin + + +def plugin_action(request, board_id, plugin_id, action, params={}): + """Start/Stop/Call actions on board(s).""" + plugin_injection = iotronicclient(request).plugin_injection() + plugin = plugin_injection.plugin_action(board_id, + plugin_id, + action, + params) + return plugin + + +def plugin_remove(request, board_id, plugin_id): + """Inject plugin on board(s).""" + plugin_injection = iotronicclient(request).plugin_injection() + plugin = plugin_injection.plugin_remove(board_id, plugin_id) + return plugin + + +def plugins_on_board(request, board_id): + """Plugins on board.""" + plugin_injection = iotronicclient(request).plugin_injection() + plugins = plugin_injection.plugins_on_board(board_id) + return plugins diff --git a/iotronic_ui/enabled/_6000_iot.py b/iotronic_ui/enabled/_6000_iot.py new file mode 100644 index 0000000..ccbc62e --- /dev/null +++ b/iotronic_ui/enabled/_6000_iot.py @@ -0,0 +1,29 @@ +# Copyright 2015, Rackspace, US, Inc. +# +# 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. + +# The slug of the dashboard to be added to HORIZON['dashboards']. Required. +DASHBOARD = 'iot' + +# A list of applications to be added to INSTALLED_APPS. +ADD_INSTALLED_APPS = [ + 'iotronic_ui.iot', +] + +""" +ADD_ANGULAR_MODULES = [ + 'horizon.dashboard.iot', +] +""" + +AUTO_DISCOVER_STATIC_FILES = True diff --git a/iotronic_ui/enabled/_6010_iot_boards_panel.py b/iotronic_ui/enabled/_6010_iot_boards_panel.py new file mode 100644 index 0000000..cf377c6 --- /dev/null +++ b/iotronic_ui/enabled/_6010_iot_boards_panel.py @@ -0,0 +1,23 @@ +# 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. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'boards' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'iot' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'iot' +# If set, it will update the default panel of the PANEL_DASHBOARD. +DEFAULT_PANEL = '' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'iotronic_ui.iot.boards.panel.Boards' diff --git a/iotronic_ui/enabled/_6020_iot_plugins_panel.py b/iotronic_ui/enabled/_6020_iot_plugins_panel.py new file mode 100644 index 0000000..57b5198 --- /dev/null +++ b/iotronic_ui/enabled/_6020_iot_plugins_panel.py @@ -0,0 +1,23 @@ +# 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. + +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'plugins' +# The slug of the dashboard the PANEL associated with. Required. +PANEL_DASHBOARD = 'iot' +# The slug of the panel group the PANEL is associated with. +PANEL_GROUP = 'iot' +# If set, it will update the default panel of the PANEL_DASHBOARD. +DEFAULT_PANEL = '' + +# Python panel class of the PANEL to be added. +ADD_PANEL = 'iotronic_ui.iot.plugins.panel.Plugins' diff --git a/iotronic_ui/iot/__init__.py b/iotronic_ui/iot/__init__.py new file mode 100644 index 0000000..b8023d8 --- /dev/null +++ b/iotronic_ui/iot/__init__.py @@ -0,0 +1 @@ +__version__ = '0.0.1' diff --git a/iotronic_ui/iot/boards/__init__.py b/iotronic_ui/iot/boards/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_ui/iot/boards/forms.py b/iotronic_ui/iot/boards/forms.py new file mode 100644 index 0000000..8e4ba45 --- /dev/null +++ b/iotronic_ui/iot/boards/forms.py @@ -0,0 +1,197 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard.api import iotronic +from openstack_dashboard import policy + +LOG = logging.getLogger(__name__) + + +class CreateBoardForm(forms.SelfHandlingForm): + name = forms.CharField(label=_("Board Name")) + code = forms.CharField( + label=_("Registration Code"), + help_text=_("Registration code") + ) + + # MODIFY ---> options: yun, server + type = forms.ChoiceField( + label=_("Type"), + choices=[('yun', _('YUN')), ('server', _('Server'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-type'}, + ) + ) + + """ + mobile = forms.ChoiceField( + label=_("Mobile"), + choices =[('false', _('False')), ('true', _('True'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-mobile'}, + ) + ) + """ + mobile = forms.BooleanField(label=_("Mobile"), required=False) + + latitude = forms.FloatField(label=_("Latitude")) + longitude = forms.FloatField(label=_("Longitude")) + altitude = forms.FloatField(label=_("Altitude")) + + def handle(self, request, data): + try: + + # Float + # data["location"] = [{"latitude": data["latitude"], + # "longitude": data["longitude"], + # "altitude": data["altitude"]}] + # String + data["location"] = [{"latitude": str(data["latitude"]), + "longitude": str(data["longitude"]), + "altitude": str(data["altitude"])}] + + board = iotronic.board_create(request, data["code"], + data["mobile"], data["location"], + data["type"], data["name"]) + messages.success(request, _("Board created successfully.")) + + return board + except Exception: + exceptions.handle(request, _('Unable to create board.')) + + +class UpdateBoardForm(forms.SelfHandlingForm): + uuid = forms.CharField(label=_("Board ID"), widget=forms.HiddenInput) + name = forms.CharField(label=_("Board Name")) + mobile = forms.BooleanField(label=_("Mobile"), required=False) + + latitude = forms.FloatField(label=_("Latitude")) + longitude = forms.FloatField(label=_("Longitude")) + altitude = forms.FloatField(label=_("Altitude")) + + def __init__(self, *args, **kwargs): + + super(UpdateBoardForm, self).__init__(*args, **kwargs) + + # LOG.debug("MELO INITIAL: %s", kwargs["initial"]) + + LOG.debug("MELO Manager: %s", policy.check((("iot", "iot_manager"),), + self.request)) + LOG.debug("MELO Admin: %s", policy.check((("iot", "iot_admin"),), + self.request)) + + # Admin + if policy.check((("iot", "iot:update_boards"),), self.request): + # LOG.debug("MELO ADMIN") + pass + + # Manager or Admin of the iot project + elif (policy.check((("iot", "iot_manager"),), self.request) or + policy.check((("iot", "iot_admin"),), self.request)): + # LOG.debug("MELO NO-edit IOT ADMIN") + pass + + # Other users + else: + if self.request.user.id != kwargs["initial"]["owner"]: + # LOG.debug("MELO IMMUTABLE FIELDS") + self.fields["name"].widget.attrs = {'readonly': 'readonly'} + self.fields["mobile"].widget.attrs = {'disabled': 'disabled'} + + self.fields["latitude"].widget.attrs = {'readonly': + 'readonly'} + self.fields["longitude"].widget.attrs = {'readonly': + 'readonly'} + self.fields["altitude"].widget.attrs = {'readonly': + 'readonly'} + + def handle(self, request, data): + try: + + data["location"] = [{"latitude": str(data["latitude"]), + "longitude": str(data["longitude"]), + "altitude": str(data["altitude"])}] + iotronic.board_update(request, data["uuid"], + {"name": data["name"], + "mobile": data["mobile"], + "location": data["location"]}) + + messages.success(request, _("Board updated successfully.")) + return True + except Exception: + exceptions.handle(request, _('Unable to update board.')) + + +class RemovePluginsForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Board ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Board Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + plugin_list = forms.MultipleChoiceField( + label=_("Plugins List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-remove-plugins'}), + help_text=_("Select plugins in this pool ") + ) + + def __init__(self, *args, **kwargs): + + super(RemovePluginsForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["plugin_list"].choices = kwargs["initial"]["plugin_list"] + self.fields["plugin_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + for plugin in data["plugin_list"]: + for key, value in self.fields["plugin_list"].choices: + if key == plugin: + + try: + board = None + + # LOG.debug('INJECT: %s %s', plugin, value) + # board = iotronic.plugin_create(request, data["name"], + # data["public"], + # data["callable"], + # data["code"]) + message_text = "Plugin " + str(value) + \ + " removed successfully." + messages.success(request, _(message_text)) + + if counter != len(data["plugin_list"]) - 1: + counter += 1 + else: + return board + except Exception: + message_text = "Unable to remove plugin " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break diff --git a/iotronic_ui/iot/boards/panel.py b/iotronic_ui/iot/boards/panel.py new file mode 100644 index 0000000..beb3ef9 --- /dev/null +++ b/iotronic_ui/iot/boards/panel.py @@ -0,0 +1,33 @@ +# 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 openstack_dashboard.api import keystone + + +class Boards(horizon.Panel): + name = _("Boards") + slug = "boards" + permissions = ('openstack.services.iot', ) + # policy_rules = (("iot", "iot:list_all_boards"),) + + # TO BE REMOVED + """ + def can_access(self, context): + if keystone.is_multi_domain_enabled() \ + and not keystone.is_domain_admin(context['request']): + return False + return super(Roles, self).can_access(context) + """ diff --git a/iotronic_ui/iot/boards/tables.py b/iotronic_ui/iot/boards/tables.py new file mode 100644 index 0000000..c2a4bf9 --- /dev/null +++ b/iotronic_ui/iot/boards/tables.py @@ -0,0 +1,123 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class CreateBoardLink(tables.LinkAction): + name = "create" + verbose_name = _("Create Board") + url = "horizon:iot:boards:create" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class EditBoardLink(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = "horizon:iot:boards:update" + classes = ("ajax-modal",) + icon = "pencil" + # policy_rules = (("iot", "iot:update_board"),) + + """ + def allowed(self, request, role): + return api.keystone.keystone_can_edit_role() + """ + + +class RemovePluginsLink(tables.LinkAction): + name = "removeplugins" + verbose_name = _("Remove Plugin(s)") + url = "horizon:iot:boards:removeplugins" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class DeleteBoardsAction(tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Board", + u"Delete Boards", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Board", + u"Deleted Boards", + count + ) + # policy_rules = (("iot", "iot:delete_board"),) + + """ + def allowed(self, request, role): + return api.keystone.keystone_can_edit_role() + """ + + def delete(self, request, board_id): + api.iotronic.board_delete(request, board_id) + + +class BoardFilterAction(tables.FilterAction): + + # If uncommented it will appear the select menu list of fields + # and filter button + """ + filter_type = "server" + filter_choices = (("name", _("Board Name ="), True), + ("type", _("Type ="), True), + ("status", _("Status ="), True)) + """ + + def filter(self, table, boards, filter_string): + """Naive case-insensitive search.""" + q = filter_string.lower() + return [board for board in boards + if q in board.name.lower()] + + +class BoardsTable(tables.DataTable): + name = tables.WrappingColumn('name', link="horizon:iot:boards:detail", + verbose_name=_('Board Name')) + type = tables.Column('type', verbose_name=_('Type')) + # mobile = tables.Column('mobile', verbose_name=_('Mobile')) + uuid = tables.Column('uuid', verbose_name=_('Board ID')) + # code = tables.Column('code', verbose_name=_('Code')) + status = tables.Column('status', verbose_name=_('Status')) + location = tables.Column('location', verbose_name=_('Geo')) + # extra = tables.Column('extra', verbose_name=_('Extra')) + + # Overriding get_object_id method because in IoT service the "id" is + # identified by the field UUID + def get_object_id(self, datum): + return datum.uuid + + class Meta(object): + name = "boards" + verbose_name = _("boards") + row_actions = (EditBoardLink, RemovePluginsLink, DeleteBoardsAction) + table_actions = (BoardFilterAction, CreateBoardLink, + DeleteBoardsAction) diff --git a/iotronic_ui/iot/boards/tabs.py b/iotronic_ui/iot/boards/tabs.py new file mode 100644 index 0000000..f99d752 --- /dev/null +++ b/iotronic_ui/iot/boards/tabs.py @@ -0,0 +1,47 @@ +# 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 logging + +# from django.core.urlresolvers import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import tabs + +LOG = logging.getLogger(__name__) + +""" +import inspect +LOG.debug('MELO CLASSES: %s', + inspect.getmembers(tabs, predicate=inspect.isclass)) +""" + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("iot/boards/_detail_overview.html") + + def get_context_data(self, request): + coordinates = self.tab_group.kwargs['board'].__dict__["location"][0] + # LOG.debug('IOT INFO: %s', coordinates) + + return {"board": self.tab_group.kwargs['board'], + "coordinates": coordinates, + "is_superuser": request.user.is_superuser} + + +class BoardDetailTabs(tabs.TabGroup): + slug = "board_details" + # tabs = (OverviewTab, LogTab, ConsoleTab, AuditTab) + tabs = (OverviewTab,) + sticky = True diff --git a/iotronic_ui/iot/boards/templates/boards/_create.html b/iotronic_ui/iot/boards/templates/boards/_create.html new file mode 100644 index 0000000..2d20be4 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_create.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Add a new board." %}

+{% endblock %} + diff --git a/iotronic_ui/iot/boards/templates/boards/_detail_overview.html b/iotronic_ui/iot/boards/templates/boards/_detail_overview.html new file mode 100644 index 0000000..d396f88 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_detail_overview.html @@ -0,0 +1,51 @@ +{% load i18n sizeformat %} + +
+
+
{% trans "Name" %}
+
{{ board.name }}
+
{% trans "Status" %}
+
{{ board.status }}
+
{% trans "Type" %}
+
{{ board.type }}
+
{% trans "ID" %}
+
{{ board.uuid }}
+
{% trans "Code" %}
+
{{ board.code }}
+
{% trans "Creation data" %}
+
{{ board.created_at }}
+
{% trans "Location" %}
+
Latitude: {{ coordinates.latitude }}
+
Longitude: {{ coordinates.longitude }}
+
Altitude: {{ coordinates.altitude }}
+
{% trans "Mobile" %}
+
{{ board.mobile }}
+
{% trans "Extra" %}
+
{{ board.extra }}
+
+
+ + +
+ +
+ diff --git a/iotronic_ui/iot/boards/templates/boards/_removeplugins.html b/iotronic_ui/iot/boards/templates/boards/_removeplugins.html new file mode 100644 index 0000000..2f91899 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_removeplugins.html @@ -0,0 +1,8 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Remove plugin(s) from this board." %}

+{% endblock %} + diff --git a/iotronic_ui/iot/boards/templates/boards/_update.html b/iotronic_ui/iot/boards/templates/boards/_update.html new file mode 100644 index 0000000..f59eae5 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/_update.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Edit the board's details." %}

+{% endblock %} diff --git a/iotronic_ui/iot/boards/templates/boards/create.html b/iotronic_ui/iot/boards/templates/boards/create.html new file mode 100644 index 0000000..df06bf8 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Insert Board" %}{% endblock %} + +{% block main %} + {% include 'iot/boards/_create.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/boards/templates/boards/index.html b/iotronic_ui/iot/boards/templates/boards/index.html new file mode 100644 index 0000000..d62836b --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/index.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Boards" %}{% endblock %} + +{% block main %} + {{ table.render }} +{% endblock %} diff --git a/iotronic_ui/iot/boards/templates/boards/removeplugins.html b/iotronic_ui/iot/boards/templates/boards/removeplugins.html new file mode 100644 index 0000000..0c133d8 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/removeplugins.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove plugin(s) from this board." %}{% endblock %} + +{% block main %} + {% include 'iot/boards/_removeplugins.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/boards/templates/boards/update.html b/iotronic_ui/iot/boards/templates/boards/update.html new file mode 100644 index 0000000..f7ec785 --- /dev/null +++ b/iotronic_ui/iot/boards/templates/boards/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Board" %}{% endblock %} + +{% block main %} + {% include 'iot/boards/_update.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/boards/tests.py b/iotronic_ui/iot/boards/tests.py new file mode 100644 index 0000000..2dcc689 --- /dev/null +++ b/iotronic_ui/iot/boards/tests.py @@ -0,0 +1,19 @@ +# 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 horizon.test import helpers as test + + +class BoardsTests(test.TestCase): + # Unit tests for boards. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/iotronic_ui/iot/boards/urls.py b/iotronic_ui/iot/boards/urls.py new file mode 100644 index 0000000..7349a04 --- /dev/null +++ b/iotronic_ui/iot/boards/urls.py @@ -0,0 +1,27 @@ +# 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 url + +from iotronic_ui.iot.boards import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/update/$', views.UpdateView.as_view(), + name='update'), + url(r'^(?P[^/]+)/removeplugins/$', + views.RemovePluginsView.as_view(), name='removeplugins'), + url(r'^(?P[^/]+)/detail/$', views.BoardDetailView.as_view(), + name='detail'), +] diff --git a/iotronic_ui/iot/boards/views.py b/iotronic_ui/iot/boards/views.py new file mode 100644 index 0000000..961f222 --- /dev/null +++ b/iotronic_ui/iot/boards/views.py @@ -0,0 +1,329 @@ +# 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 logging + +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 messages +from horizon import tables +from horizon import tabs +from horizon.utils import memoized + +from openstack_dashboard.api import iotronic +from openstack_dashboard import policy + +from iotronic_ui.iot.boards import forms as project_forms +from iotronic_ui.iot.boards import tables as project_tables +from iotronic_ui.iot.boards import tabs as project_tabs + + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = project_tables.BoardsTable + template_name = 'iot/boards/index.html' + page_title = _("Boards") + + def get_data(self): + boards = [] + + # FROM + """ + if policy.check((("identity", "identity:list_roles"),), self.request): + try: + boards = iotronic.board_list(self.request, None, None) + # LOG.debug('IOT BOARDS: %s', boards) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve boards list.')) + else: + msg = _("Insufficient privilege level to view boards information.") + messages.info(self.request, msg) + """ + + # TO + # Admin + if policy.check((("iot", "iot:list_all_boards"),), self.request): + try: + boards = iotronic.board_list(self.request, None, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve boards list.')) + + # Admin_iot_project + elif policy.check((("iot", "iot:list_project_boards"),), self.request): + try: + boards = iotronic.board_list(self.request, None, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user boards list.')) + + # Other users + else: + try: + boards = iotronic.board_list(self.request, None, None) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user boards list.')) + + return boards + + +class CreateView(forms.ModalFormView): + template_name = 'iot/boards/create.html' + modal_header = _("Create Board") + form_id = "create_board_form" + form_class = project_forms.CreateBoardForm + submit_label = _("Create Board") + submit_url = reverse_lazy("horizon:iot:boards:create") + success_url = reverse_lazy('horizon:iot:boards:index') + page_title = _("Create Board") + + +class UpdateView(forms.ModalFormView): + template_name = 'iot/boards/update.html' + modal_header = _("Update Board") + form_id = "update_board_form" + form_class = project_forms.UpdateBoardForm + submit_label = _("Update Board") + submit_url = "horizon:iot:boards:update" + success_url = reverse_lazy('horizon:iot:boards:index') + page_title = _("Update Board") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.board_get(self.request, self.kwargs['board_id'], + None) + except Exception: + redirect = reverse("horizon:iot:boards:index") + exceptions.handle(self.request, + _('Unable to update board.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + board = self.get_object() + + # LOG.debug("MELO BOARD INFO: %s", board) + location = board.location[0] + + return {'uuid': board.uuid, + 'name': board.name, + 'mobile': board.mobile, + 'owner': board.owner, + 'latitude': location["latitude"], + 'longitude': location["longitude"], + 'altitude': location["altitude"]} + + +class RemovePluginsView(forms.ModalFormView): + template_name = 'iot/boards/removeplugins.html' + modal_header = _("Remove Plugins from board") + form_id = "remove_boardplugins_form" + form_class = project_forms.RemovePluginsForm + submit_label = _("Remove Plugins from board") + # submit_url = reverse_lazy("horizon:iot:boards:removeplugins") + submit_url = "horizon:iot:boards:removeplugins" + success_url = reverse_lazy('horizon:iot:boards:index') + page_title = _("Remove Plugins from board") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.board_get(self.request, self.kwargs['board_id'], + None) + except Exception: + redirect = reverse("horizon:iot:boards:index") + exceptions.handle(self.request, + _('Unable to remove plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(RemovePluginsView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + board = self.get_object() + + # Populate plugins + # TO BE DONE.....filter by available on this board!!! + # plugins = iotronic.plugin_list(self.request, None, None) + plugins = iotronic.plugins_on_board(self.request, board.uuid) + + plugins.sort(key=lambda b: b.name) + + plugin_list = [] + for plugin in plugins: + plugin_list.append((plugin.uuid, _(plugin.name))) + + return {'uuid': board.uuid, + 'name': board.name, + 'plugin_list': plugin_list} + + +class DetailView(tabs.TabView): + # FROM + """ + tab_group_class = project_tabs.InstanceDetailTabs + template_name = 'horizon/common/_detail.html' + redirect_url = 'horizon:project:instances:index' + page_title = "{{ instance.name|default:instance.id }}" + image_url = 'horizon:project:images:images:detail' + volume_url = 'horizon:project:volumes:volumes:detail' + """ + # TO + tab_group_class = project_tabs.BoardDetailTabs + template_name = 'horizon/common/_detail.html' + page_title = "{{ board.name|default:board.uuid }}" + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + # FROM + """ + instance = self.get_data() + if instance.image: + instance.image_url = reverse(self.image_url, + args=[instance.image['id']]) + instance.volume_url = self.volume_url + context["instance"] = instance + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions(instance) + """ + # TO + board = self.get_data() + context["board"] = board + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions(board) + + return context + + # FROM + """ + def _get_actions(self, instance): + table = project_tables.InstancesTable(self.request) + return table.render_row_actions(instance) + """ + # TO + def _get_actions(self, board): + table = project_tables.BoardsTable(self.request) + return table.render_row_actions(board) + + # FROM + """ + @memoized.memoized_method + def get_data(self): + instance_id = self.kwargs['instance_id'] + + try: + instance = api.nova.server_get(self.request, instance_id) + except Exception: + redirect = reverse(self.redirect_url) + exceptions.handle(self.request, + _('Unable to retrieve details for ' + 'instance "%s".') % instance_id, + redirect=redirect) + # Not all exception types handled above will result in a redirect. + # Need to raise here just in case. + raise exceptions.Http302(redirect) + + choices = project_tables.STATUS_DISPLAY_CHOICES + instance.status_label = ( + filters.get_display_label(choices, instance.status)) + + try: + instance.volumes = api.nova.instance_volumes_list(self.request, + instance_id) + # Sort by device name + instance.volumes.sort(key=lambda vol: vol.device) + except Exception: + msg = _('Unable to retrieve volume list for instance ' + '"%(name)s" (%(id)s).') % {'name': instance.name, + 'id': instance_id} + exceptions.handle(self.request, msg, ignore=True) + + try: + instance.full_flavor = api.nova.flavor_get( + self.request, instance.flavor["id"]) + except Exception: + msg = _('Unable to retrieve flavor information for instance ' + '"%(name)s" (%(id)s).') % {'name': instance.name, + 'id': instance_id} + exceptions.handle(self.request, msg, ignore=True) + + try: + instance.security_groups = api.network.server_security_groups( + self.request, instance_id) + except Exception: + msg = _('Unable to retrieve security groups for instance ' + '"%(name)s" (%(id)s).') % {'name': instance.name, + 'id': instance_id} + exceptions.handle(self.request, msg, ignore=True) + + try: + api.network.servers_update_addresses(self.request, [instance]) + except Exception: + msg = _('Unable to retrieve IP addresses from Neutron for ' + 'instance "%(name)s" (%(id)s).') % {'name': instance.name, + 'id': instance_id} + exceptions.handle(self.request, msg, ignore=True) + + return instance + """ + + # TO + @memoized.memoized_method + def get_data(self): + board_id = self.kwargs['board_id'] + try: + board = iotronic.board_get(self.request, board_id, None) + except Exception: + msg = ('Unable to retrieve board %s information') % {'name': + board.name} + exceptions.handle(self.request, msg, ignore=True) + return board + + # FROM + """ + def get_tabs(self, request, *args, **kwargs): + instance = self.get_data() + return self.tab_group_class(request, instance=instance, **kwargs) + """ + + # TO + def get_tabs(self, request, *args, **kwargs): + board = self.get_data() + return self.tab_group_class(request, board=board, **kwargs) + + +class BoardDetailView(DetailView): + redirect_url = 'horizon:iot:boards:index' + + def _get_actions(self, board): + table = project_tables.BoardsTable(self.request) + return table.render_row_actions(board) diff --git a/iotronic_ui/iot/dashboard.py b/iotronic_ui/iot/dashboard.py new file mode 100644 index 0000000..a84e97e --- /dev/null +++ b/iotronic_ui/iot/dashboard.py @@ -0,0 +1,26 @@ +# 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 Iot(horizon.Dashboard): + name = _("Iot") + slug = "iot" + panels = ('boards', 'plugins') # Add your panels here. + + # Specify the slug of the dashboard's default panel. + default_panel = 'boards' + +horizon.register(Iot) diff --git a/iotronic_ui/iot/plugins/__init__.py b/iotronic_ui/iot/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/iotronic_ui/iot/plugins/forms.py b/iotronic_ui/iot/plugins/forms.py new file mode 100644 index 0000000..8ef14ba --- /dev/null +++ b/iotronic_ui/iot/plugins/forms.py @@ -0,0 +1,495 @@ +# 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 cPickle +import json +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +# START FROM HERE !!!!!!! openstack_dashboard/api/nova.py +# from iotronicclient.common.apiclient import exceptions as iot_exceptions + +from horizon import forms +from horizon import messages + +from openstack_dashboard.api import iotronic +from openstack_dashboard import policy + +LOG = logging.getLogger(__name__) + + +class CreatePluginForm(forms.SelfHandlingForm): + name = forms.CharField(label=_("Plugin Name")) + + """ + public = forms.ChoiceField( + label=_("Public"), + choices =[('false', _('False')), ('true', _('True'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-public'}, + ) + ) + """ + + public = forms.BooleanField(label=_("Public"), required=False) + callable = forms.BooleanField(label=_("Callable"), required=False) + + code = forms.CharField( + label=_("Code"), + widget=forms.Textarea( + attrs={'class': 'switchable', 'data-slug': 'slug-code'}) + ) + + parameters = forms.CharField( + label=_("Parameters"), + required=False, + widget=forms.Textarea( + attrs={'class': 'switchable', + 'data-slug': 'slug-parameters-create'}), + help_text=_("Plugin parameters") + ) + + def handle(self, request, data): + + if not data["parameters"]: + data["parameters"] = {} + else: + data["parameters"] = json.loads(data["parameters"]) + + try: + plugin = iotronic.plugin_create(request, data["name"], + data["public"], data["callable"], + data["code"], data["parameters"]) + LOG.debug("MELO API REQ: %s", request) + + messages.success(request, _("Plugin created successfully.")) + + return plugin + # except iot_exceptions.ClientException: + except Exception: + # LOG.debug("MELO API REQ EXC: %s", request) + # LOG.debug("MELO API REQ (DICT): %s", exceptions.__dict__) + exceptions.handle(request, _('Unable to create plugin.')) + + +class InjectPluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + onboot = forms.BooleanField(label=_("On Boot"), required=False) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-inject-boards'}), + help_text=_("Select boards in this pool ") + ) + + def __init__(self, *args, **kwargs): + + super(InjectPluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_inject(request, key, + data["uuid"], + data["onboot"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin injected successfully on " \ + "board " + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return plugin + except Exception: + message_text = "Unable to inject plugin on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class StartPluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', + 'data-slug': 'slug-start-boards'}), + help_text=_("Select boards in this pool ") + ) + + parameters = forms.CharField( + label=_("Parameters"), + required=False, + widget=forms.Textarea( + attrs={'class': 'switchable', + 'data-slug': 'slug-startplugin-json'}), + help_text=_("Plugin parameters") + ) + + def __init__(self, *args, **kwargs): + + super(StartPluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + if not data["parameters"]: + data["parameters"] = {} + else: + data["parameters"] = json.loads(data["parameters"]) + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_action(request, key, + data["uuid"], + "PluginStart", + data["parameters"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin started successfully on board "\ + + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return plugin + except Exception: + message_text = "Unable to start plugin on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class StopPluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + delay = forms.IntegerField( + label=_("Delay in secs"), + required=False, + help_text=_("OPTIONAL: seconds to wait before stopping the plugin") + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-stop-boards'}), + help_text=_("Select boards in this pool ") + ) + + def __init__(self, *args, **kwargs): + + super(StopPluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + if not data["delay"]: + data["delay"] = {} + else: + data["delay"] = {"delay": data["delay"]} + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_action(request, key, + data["uuid"], + "PluginStop", + data["delay"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin stopped successfully on board "\ + + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return plugin + except Exception: + message_text = "Unable to stop plugin on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class CallPluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-call-boards'}), + help_text=_("Select boards in this pool ") + ) + + parameters = forms.CharField( + label=_("Parameters"), + required=False, + widget=forms.Textarea( + attrs={'class': 'switchable', + 'data-slug': 'slug-callplugin-json'}), + help_text=_("Plugin parameters") + ) + + def __init__(self, *args, **kwargs): + + super(CallPluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + if not data["parameters"]: + data["parameters"] = {} + else: + data["parameters"] = json.loads(data["parameters"]) + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_action(request, key, + data["uuid"], + "PluginCall", + data["parameters"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin called successfully on board " \ + + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 2 + else: + return plugin + except Exception: + message_text = "Unable to call plugin on board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class RemovePluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + + name = forms.CharField( + label=_('Plugin Name'), + widget=forms.TextInput(attrs={'readonly': 'readonly'}) + ) + + board_list = forms.MultipleChoiceField( + label=_("Boards List"), + widget=forms.SelectMultiple( + attrs={'class': 'switchable', 'data-slug': 'slug-remove-boards'}), + help_text=_("Select boards in this pool ") + ) + + def __init__(self, *args, **kwargs): + + super(RemovePluginForm, self).__init__(*args, **kwargs) + # input=kwargs.get('initial',{}) + + boardslist_length = len(kwargs["initial"]["board_list"]) + + self.fields["board_list"].choices = kwargs["initial"]["board_list"] + self.fields["board_list"].max_length = boardslist_length + + def handle(self, request, data): + + counter = 0 + + for board in data["board_list"]: + for key, value in self.fields["board_list"].choices: + if key == board: + + try: + plugin = None + plugin = iotronic.plugin_remove(request, key, + data["uuid"]) + # LOG.debug("MELO API: %s %s", plugin, request) + message_text = "Plugin removed successfully from" \ + + " board " + str(value) + "." + messages.success(request, _(message_text)) + + if counter != len(data["board_list"]) - 1: + counter += 1 + else: + return plugin + except Exception: + message_text = "Unable to remove plugin from board " \ + + str(value) + "." + exceptions.handle(request, _(message_text)) + + break + + +class UpdatePluginForm(forms.SelfHandlingForm): + + uuid = forms.CharField(label=_("Plugin ID"), widget=forms.HiddenInput) + owner = forms.CharField(label=_("Owner"), widget=forms.HiddenInput) + + """ + name = forms.CharField(label=_("Plugin Name")) + public = forms.ChoiceField(label=_("Public")) + callable = forms.ChoiceField(label=_("Callable")) + code = forms.CharField(label=_("Code")) + """ + + name = forms.CharField(label=_("Plugin Name")) + + """ + public = forms.ChoiceField( + label=_("Public"), + choices =[('false', _('False')), ('true', _('True'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-public'}, + ) + ) + + + callable = forms.ChoiceField( + label=_("Callable"), + choices =[('false', _('False')), ('true', _('True'))], + widget=forms.Select( + attrs={'class': 'switchable', 'data-slug': 'slug-callable'}, + ) + ) + """ + public = forms.BooleanField(label=_("Public"), required=False) + callable = forms.BooleanField(label=_("Callable"), required=False) + + code = forms.CharField( + label=_("Code"), + widget=forms.Textarea( + attrs={'class': 'switchable', 'data-slug': 'slug-code'}) + ) + + def __init__(self, *args, **kwargs): + + super(UpdatePluginForm, self).__init__(*args, **kwargs) + + # Admin + if policy.check((("iot", "iot:update_plugins"),), self.request): + # LOG.debug("MELO ADMIN") + pass + + # Admin_iot_project + elif policy.check((("iot", "iot:update_project_plugins"),), + self.request): + # LOG.debug("MELO IOT ADMIN") + + if self.request.user.id != kwargs["initial"]["owner"]: + # LOG.debug("MELO NO-edit IOT ADMIN") + self.fields["name"].widget.attrs = {'readonly': 'readonly'} + self.fields["public"].widget.attrs = {'disabled': 'disabled'} + self.fields["callable"].widget.attrs = {'disabled': 'disabled'} + self.fields["code"].widget.attrs = {'readonly': 'readonly'} + + # Other users + else: + if self.request.user.id != kwargs["initial"]["owner"]: + # LOG.debug("MELO IMMUTABLE FIELDS") + self.fields["name"].widget.attrs = {'readonly': 'readonly'} + self.fields["public"].widget.attrs = {'disabled': 'disabled'} + self.fields["callable"].widget.attrs = {'disabled': 'disabled'} + self.fields["code"].widget.attrs = {'readonly': 'readonly'} + + def handle(self, request, data): + try: + + # LOG.debug("MELO DATA: %s", data) + data["code"] = cPickle.dumps(str(data["code"])) + + iotronic.plugin_update(request, data["uuid"], + {"name": data["name"], + "public": data["public"], + "callable": data["callable"], + "code": data["code"]}) + messages.success(request, _("Plugin updated successfully.")) + return True + except Exception: + exceptions.handle(request, _('Unable to update plugin.')) diff --git a/iotronic_ui/iot/plugins/panel.py b/iotronic_ui/iot/plugins/panel.py new file mode 100644 index 0000000..a099a3d --- /dev/null +++ b/iotronic_ui/iot/plugins/panel.py @@ -0,0 +1,22 @@ +# 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 Plugins(horizon.Panel): + name = _("Plugins") + slug = "plugins" + # policy_rules = (("iot", "iot:list_all_plugins"), + # ("iot", "iot:list_project_plugins")) diff --git a/iotronic_ui/iot/plugins/tables.py b/iotronic_ui/iot/plugins/tables.py new file mode 100644 index 0000000..464ec46 --- /dev/null +++ b/iotronic_ui/iot/plugins/tables.py @@ -0,0 +1,176 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import ungettext_lazy + +from horizon import tables + +from openstack_dashboard import api + +LOG = logging.getLogger(__name__) + + +class CreatePluginLink(tables.LinkAction): + name = "create" + verbose_name = _("Create Plugin") + url = "horizon:iot:plugins:create" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class EditPluginLink(tables.LinkAction): + name = "edit" + verbose_name = _("Edit") + url = "horizon:iot:plugins:update" + classes = ("ajax-modal",) + icon = "pencil" + # policy_rules = (("iot", "iot:update_board"),) + + """ + def allowed(self, request, plugin): + # LOG.debug("MELO ALLOWED: %s %s %s", self, request, plugin) + # LOG.debug("MELO user: %s", request.user.id) + + return True + """ + + +class InjectPluginLink(tables.LinkAction): + name = "inject" + verbose_name = _("Inject Plugin") + url = "horizon:iot:plugins:inject" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class StartPluginLink(tables.LinkAction): + name = "start" + verbose_name = _("Start Plugin") + url = "horizon:iot:plugins:start" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class StopPluginLink(tables.LinkAction): + name = "stop" + verbose_name = _("Stop Plugin") + url = "horizon:iot:plugins:stop" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class CallPluginLink(tables.LinkAction): + name = "call" + verbose_name = _("Call Plugin") + url = "horizon:iot:plugins:call" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class RemovePluginLink(tables.LinkAction): + name = "remove" + verbose_name = _("Remove Plugin from board(s)") + url = "horizon:iot:plugins:remove" + classes = ("ajax-modal",) + icon = "plus" + # policy_rules = (("iot", "iot:create_board"),) + + +class DeletePluginsAction(tables.DeleteAction): + @staticmethod + def action_present(count): + return ungettext_lazy( + u"Delete Plugin", + u"Delete Plugins", + count + ) + + @staticmethod + def action_past(count): + return ungettext_lazy( + u"Deleted Plugin", + u"Deleted Plugins", + count + ) + # policy_rules = (("iot", "iot:delete_board"),) + + """ + def allowed(self, request, role): + return api.keystone.keystone_can_edit_role() + """ + + def delete(self, request, plugin_id): + api.iotronic.plugin_delete(request, plugin_id) + + +class PluginFilterAction(tables.FilterAction): + + def filter(self, table, plugins, filter_string): + """Naive case-insensitive search.""" + q = filter_string.lower() + return [plugin for plugin in plugins + if q in plugin.name.lower()] + + +class PluginsTable(tables.DataTable): + name = tables.WrappingColumn('name', link="horizon:iot:plugins:detail", + verbose_name=_('Plugin Name')) + uuid = tables.Column('uuid', verbose_name=_('Plugin ID')) + owner = tables.Column('owner', verbose_name=_('Owner')) + public = tables.Column('public', verbose_name=_('Public')) + callable = tables.Column('callable', verbose_name=_('Callable')) + + # Overriding get_object_id method because in IoT service the "id" is + # identified by the field UUID + def get_object_id(self, datum): + # LOG.debug("MELO datum %s", datum) + return datum.uuid + + # Overriding get_row_actions method because we need to discriminate + # between Sync and Async plugins + def get_row_actions(self, datum): + actions = super(PluginsTable, self).get_row_actions(datum) + # LOG.debug("MELO ACTIONS: %s %s", actions[0].name, datum.name) + + selected_row_actions = [] + + common_actions = ["edit", "inject", "remove", "delete"] + + for action in actions: + if action.name in common_actions: + selected_row_actions.append(action) + + elif datum.callable == True and action.name == "call": + selected_row_actions.append(action) + + elif datum.callable == False and action.name != "call": + selected_row_actions.append(action) + + return selected_row_actions + + class Meta(object): + name = "plugins" + verbose_name = _("plugins") + + row_actions = (EditPluginLink, InjectPluginLink, StartPluginLink, + StopPluginLink, CallPluginLink, RemovePluginLink, + DeletePluginsAction,) + table_actions = (PluginFilterAction, CreatePluginLink, + DeletePluginsAction) diff --git a/iotronic_ui/iot/plugins/tabs.py b/iotronic_ui/iot/plugins/tabs.py new file mode 100644 index 0000000..de5fec0 --- /dev/null +++ b/iotronic_ui/iot/plugins/tabs.py @@ -0,0 +1,36 @@ +# 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 logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import tabs + +LOG = logging.getLogger(__name__) + + +class OverviewTab(tabs.Tab): + name = _("Overview") + slug = "overview" + template_name = ("iot/plugins/_detail_overview.html") + + def get_context_data(self, request): + return {"plugin": self.tab_group.kwargs['plugin'], + "is_superuser": request.user.is_superuser} + + +class PluginDetailTabs(tabs.TabGroup): + slug = "plugin_details" + # tabs = (OverviewTab, LogTab, ConsoleTab, AuditTab) + tabs = (OverviewTab,) + sticky = True diff --git a/iotronic_ui/iot/plugins/templates/plugins/_call.html b/iotronic_ui/iot/plugins/templates/plugins/_call.html new file mode 100644 index 0000000..ad15c71 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_call.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Call a plugin on board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_create.html b/iotronic_ui/iot/plugins/templates/plugins/_create.html new file mode 100644 index 0000000..c1cdcd2 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_create.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Add a new plugin into the Cloud." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html b/iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html new file mode 100644 index 0000000..8906b44 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_detail_overview.html @@ -0,0 +1,16 @@ +{% load i18n sizeformat %} + +
+
+
{% trans "Name" %}
+
{{ plugin.name }}
+
{% trans "ID" %}
+
{{ plugin.uuid }}
+
{% trans "Public" %}
+
{{ plugin.public }}
+
{% trans "Callable" %}
+
{{ plugin.callable }}
+
{% trans "Code" %}
+
{{ plugin.code }}
+
+
diff --git a/iotronic_ui/iot/plugins/templates/plugins/_inject.html b/iotronic_ui/iot/plugins/templates/plugins/_inject.html new file mode 100644 index 0000000..f4ec1e0 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_inject.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Inject a plugin into board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_remove.html b/iotronic_ui/iot/plugins/templates/plugins/_remove.html new file mode 100644 index 0000000..79aa0be --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_remove.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Remove a plugin from board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_start.html b/iotronic_ui/iot/plugins/templates/plugins/_start.html new file mode 100644 index 0000000..07df81b --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_start.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Start a plugin on board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_stop.html b/iotronic_ui/iot/plugins/templates/plugins/_stop.html new file mode 100644 index 0000000..d72d9f8 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_stop.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Stop a plugin on board(s)." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/_update.html b/iotronic_ui/iot/plugins/templates/plugins/_update.html new file mode 100644 index 0000000..2233fcb --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/_update.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block modal-body-right %} +

{% trans "Description:" %}

+

{% trans "Edit the plugin's details." %}

+{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/call.html b/iotronic_ui/iot/plugins/templates/plugins/call.html new file mode 100644 index 0000000..531077a --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/call.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Call Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_call.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/create.html b/iotronic_ui/iot/plugins/templates/plugins/create.html new file mode 100644 index 0000000..f056540 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/create.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Insert Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_create.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/index.html b/iotronic_ui/iot/plugins/templates/plugins/index.html new file mode 100644 index 0000000..c4c0458 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/index.html @@ -0,0 +1,12 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Plugins" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Plugins") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} + diff --git a/iotronic_ui/iot/plugins/templates/plugins/inject.html b/iotronic_ui/iot/plugins/templates/plugins/inject.html new file mode 100644 index 0000000..247d683 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/inject.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Inject Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_inject.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/remove.html b/iotronic_ui/iot/plugins/templates/plugins/remove.html new file mode 100644 index 0000000..d684294 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/remove.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Remove Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_remove.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/start.html b/iotronic_ui/iot/plugins/templates/plugins/start.html new file mode 100644 index 0000000..0d6b488 --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/start.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Start Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_start.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/stop.html b/iotronic_ui/iot/plugins/templates/plugins/stop.html new file mode 100644 index 0000000..bdf8f3b --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/stop.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Stop Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_stop.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/templates/plugins/update.html b/iotronic_ui/iot/plugins/templates/plugins/update.html new file mode 100644 index 0000000..f0919eb --- /dev/null +++ b/iotronic_ui/iot/plugins/templates/plugins/update.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Update Plugin" %}{% endblock %} + +{% block main %} + {% include 'iot/plugins/_update.html' %} +{% endblock %} diff --git a/iotronic_ui/iot/plugins/tests.py b/iotronic_ui/iot/plugins/tests.py new file mode 100644 index 0000000..a1e1acd --- /dev/null +++ b/iotronic_ui/iot/plugins/tests.py @@ -0,0 +1,19 @@ +# 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 horizon.test import helpers as test + + +class PluginsTests(test.TestCase): + # Unit tests for plugins. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/iotronic_ui/iot/plugins/urls.py b/iotronic_ui/iot/plugins/urls.py new file mode 100644 index 0000000..1894504 --- /dev/null +++ b/iotronic_ui/iot/plugins/urls.py @@ -0,0 +1,35 @@ +# 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 url + +from iotronic_ui.iot.plugins import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(r'^(?P[^/]+)/inject/$', views.InjectView.as_view(), + name='inject'), + url(r'^(?P[^/]+)/start/$', views.StartView.as_view(), + name='start'), + url(r'^(?P[^/]+)/stop/$', views.StopView.as_view(), + name='stop'), + url(r'^(?P[^/]+)/call/$', views.CallView.as_view(), + name='call'), + url(r'^(?P[^/]+)/remove/$', views.RemoveView.as_view(), + name='remove'), + url(r'^(?P[^/]+)/update/$', views.UpdateView.as_view(), + name='update'), + url(r'^(?P[^/]+)/detail/$', views.PluginDetailView.as_view(), + name='detail'), +] diff --git a/iotronic_ui/iot/plugins/views.py b/iotronic_ui/iot/plugins/views.py new file mode 100644 index 0000000..0a1dc6c --- /dev/null +++ b/iotronic_ui/iot/plugins/views.py @@ -0,0 +1,413 @@ +# 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 cPickle +import logging + +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 messages +from horizon import tables +from horizon import tabs +from horizon.utils import memoized + +from openstack_dashboard.api import iotronic, keystone +from openstack_dashboard import policy + +from iotronic_ui.iot.plugins import forms as project_forms +from iotronic_ui.iot.plugins import tables as project_tables +from iotronic_ui.iot.plugins import tabs as project_tabs + +LOG = logging.getLogger(__name__) + + +class IndexView(tables.DataTableView): + table_class = project_tables.PluginsTable + template_name = 'iot/plugins/index.html' + page_title = _("Plugins") + + def get_data(self): + plugins = [] + users = [] + + # Admin + if policy.check((("iot", "iot:list_all_plugins"),), self.request): + try: + plugins = iotronic.plugin_list(self.request, None, None, + all_plugins=True) + users = keystone.user_list(self.request) + + except Exception: + exceptions.handle(self.request, _('Unable to retrieve plugins \ + list.')) + + # Admin_iot_project + elif policy.check((("iot", "iot:list_project_plugins"),), + self.request): + try: + plugins = iotronic.plugin_list(self.request, None, None, + with_public=True) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user plugins list.')) + + # Other users + else: + # FROM + """ + msg = _("Insufficient privilege level to view + plugins information.") + messages.info(self.request, msg) + """ + # TO + try: + plugins = iotronic.plugin_list(self.request, None, None, + with_public=True) + + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve user plugins list.')) + + # Replace owner column values (user.id) with user.name (only Admin + # can see human-readable names) + for plugin in plugins: + for user in users: + if plugin.owner == user.id: + plugin.owner = user.name + break + + return plugins + + +class CreateView(forms.ModalFormView): + template_name = 'iot/plugins/create.html' + modal_header = _("Create Plugin") + form_id = "create_plugin_form" + form_class = project_forms.CreatePluginForm + submit_label = _("Create Plugin") + submit_url = reverse_lazy("horizon:iot:plugins:create") + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Create Plugin") + + +class InjectView(forms.ModalFormView): + template_name = 'iot/plugins/inject.html' + modal_header = _("Inject Plugin") + form_id = "inject_plugin_form" + form_class = project_forms.InjectPluginForm + submit_label = _("Inject Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:inject") + submit_url = "horizon:iot:plugins:inject" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Inject Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to inject plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(InjectView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class StartView(forms.ModalFormView): + template_name = 'iot/plugins/start.html' + modal_header = _("Start Plugin") + form_id = "start_plugin_form" + form_class = project_forms.StartPluginForm + submit_label = _("Start Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:start") + submit_url = "horizon:iot:plugins:start" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Start Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to start plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(StartView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class StopView(forms.ModalFormView): + template_name = 'iot/plugins/stop.html' + modal_header = _("Stop Plugin") + form_id = "stop_plugin_form" + form_class = project_forms.StopPluginForm + submit_label = _("Stop Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:stop") + submit_url = "horizon:iot:plugins:stop" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Stop Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to stop plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(StopView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class CallView(forms.ModalFormView): + template_name = 'iot/plugins/call.html' + modal_header = _("Call Plugin") + form_id = "call_plugin_form" + form_class = project_forms.CallPluginForm + submit_label = _("Call Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:call") + submit_url = "horizon:iot:plugins:call" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Call Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to call plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(CallView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class RemoveView(forms.ModalFormView): + template_name = 'iot/plugins/remove.html' + modal_header = _("Remove Plugin") + form_id = "remove_plugin_form" + form_class = project_forms.RemovePluginForm + submit_label = _("Remove Plugin") + # submit_url = reverse_lazy("horizon:iot:plugins:remove") + submit_url = "horizon:iot:plugins:remove" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Remove Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to remove plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(RemoveView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Populate boards + boards = iotronic.board_list(self.request, "online", None, None) + boards.sort(key=lambda b: b.name) + + board_list = [] + for board in boards: + board_list.append((board.uuid, _(board.name))) + + return {'uuid': plugin.uuid, + 'name': plugin.name, + 'board_list': board_list} + + +class UpdateView(forms.ModalFormView): + template_name = 'iot/plugins/update.html' + modal_header = _("Update Plugin") + form_id = "update_plugin_form" + form_class = project_forms.UpdatePluginForm + submit_label = _("Update Plugin") + submit_url = "horizon:iot:plugins:update" + success_url = reverse_lazy('horizon:iot:plugins:index') + page_title = _("Update Plugin") + + @memoized.memoized_method + def get_object(self): + try: + return iotronic.plugin_get(self.request, self.kwargs['plugin_id'], + None) + except Exception: + redirect = reverse("horizon:iot:plugins:index") + exceptions.handle(self.request, + _('Unable to update plugin.'), + redirect=redirect) + + def get_context_data(self, **kwargs): + context = super(UpdateView, self).get_context_data(**kwargs) + args = (self.get_object().uuid,) + context['submit_url'] = reverse(self.submit_url, args=args) + return context + + def get_initial(self): + plugin = self.get_object() + + # Deserialize the code of the plugin + plugin.code = cPickle.loads(str(plugin.code)) + + return {'uuid': plugin.uuid, + 'owner': plugin.owner, + 'name': plugin.name, + 'public': plugin.public, + 'callable': plugin.callable, + 'code': plugin.code} + + +class DetailView(tabs.TabView): + + tab_group_class = project_tabs.PluginDetailTabs + template_name = 'horizon/common/_detail.html' + page_title = "{{ plugin.name|default:plugin.uuid }}" + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + + plugin = self.get_data() + context["plugin"] = plugin + context["url"] = reverse(self.redirect_url) + context["actions"] = self._get_actions(plugin) + + return context + + def _get_actions(self, plugin): + table = project_tables.PluginsTable(self.request) + return table.render_row_actions(plugin) + + @memoized.memoized_method + def get_data(self): + plugin_id = self.kwargs['plugin_id'] + try: + plugin = iotronic.plugin_get(self.request, plugin_id, None) + except Exception: + msg = ('Unable to retrieve plugin %s information') % {'name': + plugin.name} + exceptions.handle(self.request, msg, ignore=True) + return plugin + + def get_tabs(self, request, *args, **kwargs): + plugin = self.get_data() + return self.tab_group_class(request, plugin=plugin, **kwargs) + + +class PluginDetailView(DetailView): + redirect_url = 'horizon:iot:plugins:index' + + def _get_actions(self, plugin): + + # Deserialize the code of the plugin + plugin.code = cPickle.loads(str(plugin.code)) + + table = project_tables.PluginsTable(self.request) + return table.render_row_actions(plugin) diff --git a/iotronic_ui/iot/static/iot/images/blue-circle.png b/iotronic_ui/iot/static/iot/images/blue-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..3e1f389d121aaadc4b6e238bd516fde82f4d3f83 GIT binary patch literal 16224 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 literal 0 HcmV?d00001 diff --git a/iotronic_ui/iot/static/iot/images/marker-icon-green.png b/iotronic_ui/iot/static/iot/images/marker-icon-green.png new file mode 100644 index 0000000000000000000000000000000000000000..56db5ea9ffa29c2bf85feb5a943e0ab0dc73a3c0 GIT binary patch literal 1822 zcmV+(2jTdMP)P001cn1^@s6z>|W`00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-3$s303JDN(rN$z2CGR# zK~z}7otIr~Tt^kh|L4rydw1{pBaNN5*bk&piW6ETkl+w_sDwfbNIW1Rgb?t6LNhkW6b(AsrluY;4_y?gJ>oWsL9 z#v9wQ-MRW}rK>sTw{y;!Gj~W;v7E8((7RU~K|dgCgi%c-9((EG;}grVrQ1Q?|C6l) z!tyx+KBIzuCd#adybI2#YMB^U1Mq8AJ$(H0r%ta3@}5VC&F0NJ8T>@4GXZ)fK^I`z zf{381U=b8G0j^PzRdZ6+Z=sw%b@a0@`z0aUe!OWNgMVvHv@sqqDM~=_9_j({0tf&G z*fPihCNe-q@Rzhv_+o^;|HR8*I&)!B$hPMA>=^RLRfv|I1=aMJ-;W>K^}5o{O3R3p?bH|oMXaN-Lm zN6}jN%n0UZl}e?gy&i7L13_D%xkW^@;ClpxzZM{?Ju00Q%U*tQ(=`LY;QmdYX7CQb z!6&Zfp{i|yA)wnJx+6lAT10n5u-yg}(Y(2?=Ar9-V#sVC{Qjng0e}X7cwZ`xCOYq} zq`u1lz3yO~G?YaW=56QcX>Hk!{g}l(+=GSu1nxXF7wbmKne)nqrWl z*5;d$iU1{GWOQUf$jyIzZt|Z9iMCqK9D6bZnqsKnB$LQXQuB@sL4jFGuE_5$FgEc9 zetGtAD+D5Kzeg_r4zG^9iqPjFxwhGpA%wasIOyNZogY}!3Bkv>pJJ)WY}nK*C;<{% z!qHd$j%hoM^*!rRHYpSVFAN>Tp}!tP`K}U*t^(mkXzKu`!pN`qOp36d?t6M;pB228 zzOT$h-L;lIC+^WI3;W*~;j%}w*+g$gFQ&38EL+6)Byc?r&1u)R8QC)8N`%Hg8Uc`g zQdMkzX2Y?LzOKRIZTY+%9{?mJ3zITT!-B3ev_lX&Jc_2kH$2QO`Ld}+G#+bx^v1EF z$IoxK0Kks+WpCCG^V%p4jaE|Uj%MaSO#-tAbrRGmWJ+e|`CF>aB!uR8BV$M3o^6nh zq9ehDBDoqPjtmGgPtX7f4xw20=By&E5@V#4o&VwBP=Beu-ecF%at<}u*s>MVAOk$9aW(^g7 zYWT@ZwIv|{Ae!u#JmfsdNKFMwx~O7Kfk^;)XkJSi&oAU}LCEl)(+vo|A)_ucN$bE` zXkQQ!;>18kT?VDyukX6jTs8y%#AOG?g_>Rk5>6Mas0vk}R{?S1da_DhSj^X=kTZL) z0Nyv!2mqY< zgjs5&!@I literal 0 HcmV?d00001 diff --git a/iotronic_ui/iot/static/iot/images/marker-icon-red.png b/iotronic_ui/iot/static/iot/images/marker-icon-red.png new file mode 100644 index 0000000000000000000000000000000000000000..3165a3132a2ec44323faffbbdd9dccef0b14dd9b GIT binary patch literal 4518 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 Eg31n(2><{9 literal 0 HcmV?d00001 diff --git a/iotronic_ui/iot/static/iot/images/marker-icon.png b/iotronic_ui/iot/static/iot/images/marker-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e2e9f757f515ded172e6f72c3ce55bbe15579649 GIT binary patch literal 1747 zcmV;^1}yoBP)P001cn1^@s6z>|W`000J>NklgF+#9zZY7a#;@J(5X0e&McXK2n7+jhR}<0i-1U5t`>D@ zJSJ*^swjdwq0keUf9!BETXZhVyjqS4&z|?2HdJnOU-HYF_xSyu=XsCkdtVv=(53>u zME@3F*5J;OHwJNJdWK(ivQ??rr&t7M)1yRas=d_yYH>g+p#{( zm+NoyW%|8bNfUkAMrabri(FY#Dqr5%zhZA&e^iALHXiJOFYA7Qt##L_a?_z6SW{&J zVeyp#G&snW>SO{*%d9CGVM}xic~V`MU$)*JU1Nbw2YX?ywi}|VZ4g;$g)p^+DoLHR zZ^Zr$S_=f^oU`+!4K^?NsU;H{;bhhex#H7(!s52U&FJ}OHQf-VvVd?Btj2MhF|zQI z%l~jBr~6T7^_WIHC1>8j&bv`c18g|Z3*l-jgeoml1{oh++Y75JI)RgU>LEY<%)C)X zI2rZ2kb>s^4cZuYeq3!A}DS`X~>Nd;+A$4e;ZwyD<1@2!8$ZKJ3w%!6)+sCALy+bKwyk zqKCS6qEGWmJ)97b-QXY|`<0lS5THkEtGgjE>ojOvuftfM&Ugd-rQg9C+|FdGM)HXs z(IxsA$&r(xg_j^4=hC;>s;1UF*Wp1I-2~sEF zZYgb?&`4bM1qjM*hR`yr3qJ(;L>Kj2XpBUy+)t((6z;bIr@-ihFNVAl4<#?{TWIaQ zIi>;YjXS_iIfNb?!N1t-!Y6vZvW5ZXE{%l7imzV9NvV4nf#G`Pcex;#@}?EINwsk7 z>UC=SlJC*b5a>-mgHP%q2+qF;cbNx ztCV@{s&fPsSt#;i@#G-m$as&$gLaG}ebOrtS5*22&glbhMH_fz8(~qVVN!VIVCKzg z1$U9^93(E%Ur9v_R*h#!t)Ce+#)+m(q^zCq%g&L-!E zC&et9WrT(49pl0S`?=DK7=`+;`CB!wP3ta97b)XdJ8K=@`DXOk0Q0};7zNT!`k4t@ z+)=9S)4p(jEGm5!qq)PDTmXiw3qDM19|fiylc@LtiQ=}Kfb#w&ckv^7tBj;cfwtY$q?IdliYlg zqh@4;IyW)OFPQPw4z|JsAEb7`+@yA@Q8SXQT#;upV`QNJ59Bg5m(jci3`0T4X-&^GU6)x7$Vi0XMWB(2hrdK^!hq0 zt!bDg$Hh)-9L62h`&{0PBf%7vM>69o`k4~kx^Wc)?lG!}=WgV2x-rq?HN#jMr^B0; p5zMeb2q5MEX5{g&Aa%N&e*pr!t%ZZ}>w*9P002ovPDHLkV1gpUS8xCT literal 0 HcmV?d00001 diff --git a/iotronic_ui/iot/static/iot/images/marker-shadow.png b/iotronic_ui/iot/static/iot/images/marker-shadow.png new file mode 100644 index 0000000000000000000000000000000000000000..d1e773c715a9b508ebea055c4bb4b0a2ad7f6e52 GIT binary patch literal 797 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
1p=5=wyS1U^c>c(`ZlR%-Gu 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 BBpUz# literal 0 HcmV?d00001 diff --git a/iotronic_ui/iot/static/iot/js/iot.js b/iotronic_ui/iot/static/iot/js/iot.js new file mode 100644 index 0000000..97e3b8d --- /dev/null +++ b/iotronic_ui/iot/static/iot/js/iot.js @@ -0,0 +1,122 @@ +/* Additional JavaScript for iot. */ + +//alert('MELO'); + +//var image_url = 'https://ing-res-17.me.trigrid.it/iotronic/'; +var images_url = 'http://'+location.host+'/dashboard/static/iot/images/'; + +var markers = []; +markers = L.markerClusterGroup({ + spiderfyOnMaxZoom: false, + disableClusteringAtZoom: 17 +}); + + +var marker_red = L.icon({ + iconUrl: images_url+'marker-icon-red.png', + iconAnchor:[12.5, 41], + shadowUrl: images_url+'marker-shadow.png' +}); + + +var marker_green = L.icon({ + iconUrl: images_url+'marker-icon-green.png', + iconAnchor:[12.5, 41], + shadowUrl: images_url+'marker-shadow.png' +}); + + +var marker_blue = L.icon({ + iconUrl: images_url+'marker-icon.png', + iconAnchor:[12.5, 41], + shadowUrl: images_url+'marker-shadow.png' +}); + + +var labels = []; +var latitude = []; +var longitude = []; +var altitude = []; +var statuses = []; +var last_update = []; + + +function render_map(map_id, coordinates){ + var osmUrl='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; + var osm = new L.TileLayer(osmUrl, {}); + + var lat = 38.20523; + var lon = 15.55972; + if(coordinates["coordinates"].length ==1){ + lat = coordinates["coordinates"][0].lat; + lon = coordinates["coordinates"][0].lon; + } + var map = L.map(map_id, {scrollWheelZoom:false, worldCopyJump: true}).setView([lat, lon], 12); + map.addLayer(osm); + + //Copyright + L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap - by MDSLab' + }).addTo(map); + + //var marker = L.marker([lat, lon]); + //marker.setIcon(marker_red); + coord = coordinates["coordinates"]; + for(var i=0;i'; + if(statuses[sel] == "online") + img = ''; + if(statuses[sel] == "offline") + img = ''; + + var open_popup = '
'; + + var default_popup = '
'+img +' '+labels[sel]+'

' + + '
'+last_update[sel]+'

'+ + 'Latitude: '+latitude[sel]+ '
' + + 'Longitude: '+longitude[sel]+'
' + + 'Altitude: '+altitude[sel]+'

'; + + global_popup = open_popup + default_popup +"
"; + var popup = L.popup().setLatLng(e.latlng).setContent(global_popup).openOn(map); + }); + markers.addLayer(marker); + } + map.addLayer(markers); + //return map; +} + + + +function choose_marker(status){ + if(status=="online") return marker_green; + else if(status =="offline") return marker_red; + else return marker_blue; +} diff --git a/iotronic_ui/iot/static/iot/scss/iot.scss b/iotronic_ui/iot/static/iot/scss/iot.scss new file mode 100644 index 0000000..e1bd25d --- /dev/null +++ b/iotronic_ui/iot/static/iot/scss/iot.scss @@ -0,0 +1,7 @@ +/* Additional SCSS for {{ dash_name }}. */ + +/* +#mapdiv { + min-height: 300px; +} +*/ diff --git a/iotronic_ui/iot/templates/iot/base.html b/iotronic_ui/iot/templates/iot/base.html new file mode 100644 index 0000000..41c73e4 --- /dev/null +++ b/iotronic_ui/iot/templates/iot/base.html @@ -0,0 +1,10 @@ +{% extends 'base.html' %} + +{% block sidebar %} + {% include 'horizon/common/_sidebar.html' %} +{% endblock %} + +{% block main %} + {% include "horizon/_messages.html" %} + {% block iot_main %}{% endblock %} +{% endblock %} diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..6cac6ba --- /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 + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", + "iotronic_ui.test.settings") + execute_from_command_line(sys.argv) diff --git a/package.json b/package.json new file mode 100644 index 0000000..1962e2c --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "iotronic_ui", + "description": "IoTronic Panels JavaScript tests", + "repository": { + "type": "git", + "url": "git://git.openstack.org/openstack/iotronic_ui" + }, + "version": "0.0.0", + "private": true, + "license": "Apache 2.0", + "author": "Openstack ", + "devDependencies": { + "eslint": "^1.10.3", + "eslint-config-openstack": "^1.2.4", + "eslint-plugin-angular": "1.0.1", + "jasmine-core": "2.4.1", + "karma": "1.1.2", + "karma-chrome-launcher": "1.0.1", + "karma-cli": "1.0.1", + "karma-coverage": "1.1.1", + "karma-jasmine": "1.0.2", + "karma-ng-html2js-preprocessor": "1.0.0", + "karma-threshold-reporter": "0.1.15" + }, + "dependencies": {}, + "scripts": { + "postinstall": "if [ ! -d .tox ] || [ ! -d .tox/py27 ]; then tox -epy27 --notest; fi", + "lint": "eslint --no-color iotronic_ui/static", + "lintq": "eslint --quiet iotronic_ui/static", + "test": "karma start iotronic_ui/karma.conf.js --single-run" + } +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5f85fca --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +# 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,!=2.1.0 # Apache-2.0 +Babel>=2.3.4,!=2.4.0 # BSD +Django>=1.8,<2.0 # BSD +django-babel>=0.5.1 # BSD +django-compressor>=2.0 # MIT +django-pyscss>=2.0.2 # BSD License (2 clause) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..e8cfe56 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,29 @@ +[metadata] +name = iotronic_ui +summary = Iotronic plugin for the OpenStack Dashboard +description-file = + README.rst +author = OpenStack +author-email = openstack-dev@lists.openstack.org +home-page = http://www.openstack.org/ +classifier = + Environment :: OpenStack + 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.5 + +[files] +packages = + iotronic_ui + +[build_sphinx] +all_files = 1 +build-dir = doc/build +source-dir = doc/source +warning-is-error = 1 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d74ff58 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +# 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>=2.0.0'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..5f85fca --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,15 @@ +# 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,!=2.1.0 # Apache-2.0 +Babel>=2.3.4,!=2.4.0 # BSD +Django>=1.8,<2.0 # BSD +django-babel>=0.5.1 # BSD +django-compressor>=2.0 # MIT +django-pyscss>=2.0.2 # BSD License (2 clause) diff --git a/test-shim.js b/test-shim.js new file mode 100644 index 0000000..5b364ba --- /dev/null +++ b/test-shim.js @@ -0,0 +1,96 @@ +/* + * 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/tox_install.sh b/tools/tox_install.sh new file mode 100755 index 0000000..3a38ee4 --- /dev/null +++ b/tools/tox_install.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash + +# Many of horizon's repos suffer from the problem of depending on horizon, +# but it not existing on pypi. + +# This wrapper for tox's package installer will use the existing package +# if it exists, else use zuul-cloner if that program exists, else grab it +# from horizon master via a hard-coded URL. That last case should only +# happen with devs running unit tests locally. + +# From the tox.ini config page: +# install_command=ARGV +# default: +# pip install {opts} {packages} + +ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner +GIT_BASE=${GIT_BASE:-https://git.openstack.org/} + +install_project() { + local project=$1 + local branch=${2:-$BRANCH_NAME} + local module_name=${project//-/_} + + set +e + project_installed=$(echo "import $module_name" | python 2>/dev/null ; echo $?) + set -e + + if [ $project_installed -eq 0 ]; then + echo "ALREADY INSTALLED" > /tmp/tox_install.txt + echo "$project already installed; using existing package" + elif [ -x "$ZUUL_CLONER" ]; then + echo "ZUUL CLONER" > /tmp/tox_install.txt + # Make this relative to current working directory so that + # git clean can remove it. We cannot remove the directory directly + # since it is referenced after $install_cmd -e + mkdir -p .tmp + PROJECT_DIR=$(/bin/mktemp -d -p $(pwd)/.tmp) + pushd $PROJECT_DIR + $ZUUL_CLONER --cache-dir \ + /opt/git \ + --branch $branch \ + http://git.openstack.org \ + openstack/$project + cd openstack/$project + $install_cmd -e . + popd + else + echo "PIP HARDCODE" > /tmp/tox_install.txt + local GIT_REPO="$GIT_BASE/openstack/$project" + SRC_DIR="$VIRTUAL_ENV/src/$project" + git clone --depth 1 --branch $branch $GIT_REPO $SRC_DIR + $install_cmd -U -e $SRC_DIR + fi +} + +set -e + +install_cmd="pip install -c$1" +shift + +install_project horizon + +$install_cmd -U $* +exit $? diff --git a/tools/tox_install.sh_ORIG b/tools/tox_install.sh_ORIG new file mode 100755 index 0000000..7fd2a59 --- /dev/null +++ b/tools/tox_install.sh_ORIG @@ -0,0 +1,88 @@ +#!/usr/bin/env bash + +# Client constraint file contains this client version pin that is in conflict +# with installing the client from source. We should remove the version pin in +# the constraints file before applying it for from-source installation. +# The script also has a secondary purpose to install certain special +# dependencies directly from git. + +# Wrapper for pip install that always uses constraints. +function pip_install() { + pip install -c"$localfile" -U "$@" +} + +# Grab the library from git using either zuul-cloner or pip. The former is +# there to a take advantage of the setup done by the gate infrastructure +# and honour any/all Depends-On headers in the commit message +function install_from_git() { + ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner + # GIT_HOST=git.openstack.org + GIT_HOST=github.com + PROJ=$1 + EGG=$2 + + edit-constraints "$localfile" -- "$EGG" + if [ -x "$ZUUL_CLONER" ]; then + SRC_DIR="$VIRTUAL_ENV/src" + mkdir -p "$SRC_DIR" + cd "$SRC_DIR" >/dev/null + ZUUL_CACHE_DIR=${ZUUL_CACHE_DIR:-/opt/git} $ZUUL_CLONER \ + --branch "$BRANCH_NAME" \ + "git://$GIT_HOST" "$PROJ" + pip_install -e "$PROJ/." + cd - >/dev/null + else + SRC_DIR="$VIRTUAL_ENV/src/$PROJ" + git clone --depth 1 --branch $BRANCH_NAME https://$GIT_HOST/$PROJ $SRC_DIR + pip_install -e $SRC_DIR + fi +} + + + +CONSTRAINTS_FILE="$1" +shift 1 + +# This script will either complete with a return code of 0 or the return code +# of whatever failed. +set -e + +# NOTE(tonyb): Place this in the tox environment's log dir so it will get +# published to logs.openstack.org for easy debugging. +mkdir -p "$VIRTUAL_ENV/log/" +localfile="$VIRTUAL_ENV/log/upper-constraints.txt" + +if [[ "$CONSTRAINTS_FILE" != http* ]]; then + CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE" +fi +# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep +curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile" + +pip_install openstack-requirements + +# This is the main purpose of the script: Allow local installation of +# the current repo. It is listed in constraints file and thus any +# install will be constrained and we need to unconstrain it. +edit-constraints "$localfile" -- "$CLIENT_NAME" + +declare -a passthrough_args +while [ $# -gt 0 ] ; do + case "$1" in + # If we have any special os: deps then process them + os:*) + declare -a pkg_spec + IFS=: pkg_spec=($1) + install_from_git "${pkg_spec[1]}" "${pkg_spec[2]}" + ;; + # Otherwise just pass the other deps through to the constrained pip install + *) + passthrough_args+=("$1") + ;; + esac + shift 1 +done + +# If *only* had special args then then isn't any need to run pip. +if [ -n "$passthrough_args" ] ; then + pip_install "${passthrough_args[@]}" +fi diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..34cedf7 --- /dev/null +++ b/tox.ini @@ -0,0 +1,39 @@ +[tox] +envlist = py27,py27dj18,pep8 +minversion = 2.3.1 +skipsdist = True + +[testenv] +usedevelop = True +setenv = VIRTUAL_ENV={envdir} + BRANCH_NAME=master + CLIENT_NAME=iotronic-ui +install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages} +deps = -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt +commands = {} +whitelist_externals = flake8 + +[testenv:pep8] +basepython = python2.7 +# commands = flake8 {posargs} +commands = + +[testenv:venv] +commands = {posargs} + +[testenv:py27] +basepython = python2.7 +commands = + +[testenv:py27dj18] +basepython = python2.7 +commands = + pip install django>=1.8,<2.0 + +[flake8] +show-source = True +builtins = _ +ignore = E711,E712,H404,H405,E123,E125,E901,H301,H701,E226 +exclude = .venv,.git,.tox,dist,*lib/python*,*egg,build,node_modules +max-complexity = 20