From 2dbe655587ca98b67c1a3e3798c63fd47229adc0 Mon Sep 17 00:00:00 2001 From: Thomas Duval Date: Tue, 19 Jun 2018 16:13:31 +0200 Subject: Update code to 4.5 official version Change-Id: I5075da0e2a3247ae1564f21b358748f482b75aa4 --- moon_dashboard/.gitignore | 1 + moon_dashboard/.gitlab-ci.yml | 64 ++ moon_dashboard/Dockerfile | 34 + moon_dashboard/LICENSE | 0 moon_dashboard/MANIFEST.in | 3 + moon_dashboard/README.md | 40 + moon_dashboard/README.rst | 39 + moon_dashboard/babel-django.cfg | 5 + moon_dashboard/babel-djangojs.cfg | 14 + moon_dashboard/moon/__init__.py | 0 moon_dashboard/moon/dashboard.py | 13 + moon_dashboard/moon/enabled/_32000_moon.py | 19 + moon_dashboard/moon/model/__init__.py | 0 moon_dashboard/moon/model/panel.py | 23 + .../moon/model/templates/model/index.html | 16 + moon_dashboard/moon/model/tests.py | 19 + moon_dashboard/moon/model/urls.py | 20 + moon_dashboard/moon/model/views.py | 22 + moon_dashboard/moon/pdp/__init__.py | 0 moon_dashboard/moon/pdp/panel.py | 23 + moon_dashboard/moon/pdp/templates/pdp/index.html | 16 + moon_dashboard/moon/pdp/tests.py | 19 + moon_dashboard/moon/pdp/urls.py | 20 + moon_dashboard/moon/pdp/views.py | 22 + moon_dashboard/moon/policy/__init__.py | 0 moon_dashboard/moon/policy/panel.py | 23 + .../moon/policy/templates/policy/index.html | 16 + moon_dashboard/moon/policy/tests.py | 19 + moon_dashboard/moon/policy/urls.py | 20 + moon_dashboard/moon/policy/views.py | 22 + .../moon/static/moon/js/angular-resource.js | 863 +++++++++++++++++++++ .../moon/static/moon/js/import.service.js | 27 + moon_dashboard/moon/static/moon/js/moon.module.js | 29 + moon_dashboard/moon/static/moon/js/util.service.js | 136 ++++ .../moon/static/moon/js/util.service.spec.js | 86 ++ .../moon/static/moon/model/model.controller.js | 244 ++++++ moon_dashboard/moon/static/moon/model/model.html | 143 ++++ .../moon/static/moon/model/model.service.js | 286 +++++++ .../moon/static/moon/model/model.service.spec.js | 288 +++++++ .../moon/static/moon/pdp/pdp.controller.js | 121 +++ moon_dashboard/moon/static/moon/pdp/pdp.html | 41 + moon_dashboard/moon/static/moon/pdp/pdp.service.js | 123 +++ .../moon/static/moon/pdp/pdp.service.spec.js | 143 ++++ .../moon/static/moon/policy/policy.controller.js | 295 +++++++ moon_dashboard/moon/static/moon/policy/policy.html | 158 ++++ .../moon/static/moon/policy/policy.service.js | 330 ++++++++ .../moon/static/moon/policy/policy.service.spec.js | 336 ++++++++ moon_dashboard/moon/static/moon/scss/moon.scss | 54 ++ moon_dashboard/moon/templates/moon/base.html | 11 + moon_dashboard/run.sh | 26 + moon_dashboard/setup.cfg | 24 + moon_dashboard/setup.py | 14 + 52 files changed, 4310 insertions(+) create mode 100644 moon_dashboard/.gitignore create mode 100644 moon_dashboard/.gitlab-ci.yml create mode 100644 moon_dashboard/Dockerfile create mode 100644 moon_dashboard/LICENSE create mode 100644 moon_dashboard/MANIFEST.in create mode 100644 moon_dashboard/README.md create mode 100644 moon_dashboard/README.rst create mode 100644 moon_dashboard/babel-django.cfg create mode 100644 moon_dashboard/babel-djangojs.cfg create mode 100644 moon_dashboard/moon/__init__.py create mode 100644 moon_dashboard/moon/dashboard.py create mode 100644 moon_dashboard/moon/enabled/_32000_moon.py create mode 100644 moon_dashboard/moon/model/__init__.py create mode 100644 moon_dashboard/moon/model/panel.py create mode 100644 moon_dashboard/moon/model/templates/model/index.html create mode 100644 moon_dashboard/moon/model/tests.py create mode 100644 moon_dashboard/moon/model/urls.py create mode 100644 moon_dashboard/moon/model/views.py create mode 100644 moon_dashboard/moon/pdp/__init__.py create mode 100644 moon_dashboard/moon/pdp/panel.py create mode 100644 moon_dashboard/moon/pdp/templates/pdp/index.html create mode 100644 moon_dashboard/moon/pdp/tests.py create mode 100644 moon_dashboard/moon/pdp/urls.py create mode 100644 moon_dashboard/moon/pdp/views.py create mode 100644 moon_dashboard/moon/policy/__init__.py create mode 100644 moon_dashboard/moon/policy/panel.py create mode 100644 moon_dashboard/moon/policy/templates/policy/index.html create mode 100644 moon_dashboard/moon/policy/tests.py create mode 100644 moon_dashboard/moon/policy/urls.py create mode 100644 moon_dashboard/moon/policy/views.py create mode 100644 moon_dashboard/moon/static/moon/js/angular-resource.js create mode 100755 moon_dashboard/moon/static/moon/js/import.service.js create mode 100755 moon_dashboard/moon/static/moon/js/moon.module.js create mode 100755 moon_dashboard/moon/static/moon/js/util.service.js create mode 100755 moon_dashboard/moon/static/moon/js/util.service.spec.js create mode 100644 moon_dashboard/moon/static/moon/model/model.controller.js create mode 100644 moon_dashboard/moon/static/moon/model/model.html create mode 100755 moon_dashboard/moon/static/moon/model/model.service.js create mode 100755 moon_dashboard/moon/static/moon/model/model.service.spec.js create mode 100644 moon_dashboard/moon/static/moon/pdp/pdp.controller.js create mode 100644 moon_dashboard/moon/static/moon/pdp/pdp.html create mode 100755 moon_dashboard/moon/static/moon/pdp/pdp.service.js create mode 100755 moon_dashboard/moon/static/moon/pdp/pdp.service.spec.js create mode 100644 moon_dashboard/moon/static/moon/policy/policy.controller.js create mode 100644 moon_dashboard/moon/static/moon/policy/policy.html create mode 100755 moon_dashboard/moon/static/moon/policy/policy.service.js create mode 100755 moon_dashboard/moon/static/moon/policy/policy.service.spec.js create mode 100644 moon_dashboard/moon/static/moon/scss/moon.scss create mode 100644 moon_dashboard/moon/templates/moon/base.html create mode 100644 moon_dashboard/run.sh create mode 100644 moon_dashboard/setup.cfg create mode 100644 moon_dashboard/setup.py (limited to 'moon_dashboard') diff --git a/moon_dashboard/.gitignore b/moon_dashboard/.gitignore new file mode 100644 index 00000000..61f2dc9f --- /dev/null +++ b/moon_dashboard/.gitignore @@ -0,0 +1 @@ +**/__pycache__/ diff --git a/moon_dashboard/.gitlab-ci.yml b/moon_dashboard/.gitlab-ci.yml new file mode 100644 index 00000000..50fd8a4e --- /dev/null +++ b/moon_dashboard/.gitlab-ci.yml @@ -0,0 +1,64 @@ +stages: + - lint + - build + - test + - publish + +variables: + http_proxy: "http://devwatt-proxy.si.fr.intraorange:8080" + https_proxy: "http://devwatt-proxy.si.fr.intraorange:8080" + no_proxy: dind, gitlab.forge.orange-labs.fr + DOCKER_DRIVER: overlay + DOCKER_HOST: tcp://dind:2375 + CONTAINER_RELEASE_IMAGE: moonplatform/$CI_PROJECT_NAME + CONTAINER_TAG: dev + DOCKER_VERSION: "17.12" + +services: + - name: dockerproxy-iva.si.francetelecom.fr/docker:$DOCKER_VERSION-dind + alias: dind +image: dockerproxy-iva.si.francetelecom.fr/docker:$DOCKER_VERSION + +lint-job: + image: dockerfactory-iva.si.francetelecom.fr/docker/orange-dockerfile-lint:0.2.7-alpine3.6-2 + tags: + - rsc + - docker + - shared + stage: lint + script: + - dockerfile_lint -f Dockerfile + +build-job: + stage: build + tags: + - rsc + - docker-privileged + script: + - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + - docker build -t $CONTAINER_RELEASE_IMAGE:$CONTAINER_TAG --build-arg http_proxy=$http_proxy --build-arg https_proxy=$http_proxy . + - docker push $CONTAINER_RELEASE_IMAGE:$CONTAINER_TAG + +test-job: + stage: test + tags: + - rsc + - docker-privileged + script: + - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + - docker run -e http_proxy=$http_proxy -e https_proxy=$http_proxy $CONTAINER_RELEASE_IMAGE:$CONTAINER_TAG curl http://localhost:8000 + +publish-job: + stage: publish + tags: + - rsc + - docker-privileged + script: + - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD + - FINAL_TAG=$(grep version setup.cfg | cut -d "=" -f 2) + - echo FINAL_TAG=$FINAL_TAG + - docker pull $CONTAINER_RELEASE_IMAGE:$CONTAINER_TAG + - docker tag $CONTAINER_RELEASE_IMAGE:$CONTAINER_TAG $CONTAINER_RELEASE_IMAGE:$FINAL_TAG + - docker push $CONTAINER_RELEASE_IMAGE:$FINAL_TAG + only: + - master diff --git a/moon_dashboard/Dockerfile b/moon_dashboard/Dockerfile new file mode 100644 index 00000000..8f997fe1 --- /dev/null +++ b/moon_dashboard/Dockerfile @@ -0,0 +1,34 @@ +FROM python:3.5 + +LABEL Name=Dashboard +LABEL Description="User interface for the Moon platform" +LABEL Maintainer="Thomas Duval" +LABEL Url="https://wiki.opnfv.org/display/moon/Moon+Project+Proposal" + +ENV MANAGER_HOST="127.0.0.1" +ENV MANAGER_PORT=30001 +ENV KEYSTONE_HOST="127.0.0.1" +ENV KEYSTONE_PORT=30005 +ENV OPENSTACK_HOST="127.0.0.1" +ENV OPENSTACK_KEYSTONE_URL="http://${KEYSTONE_HOST}:${KEYSTONE_PORT}/v2.0" + +USER root + +WORKDIR /root/ +ADD . /root + +RUN git clone https://git.openstack.org/openstack/horizon + +WORKDIR /root/horizon + +RUN pip install --no-cache-dir -c http://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt . + +RUN cp openstack_dashboard/local/local_settings.py.example openstack_dashboard/local/local_settings.py +RUN pip install --no-cache-dir tox + +WORKDIR /root/ + +RUN cp -v moon/enabled/_32000_moon.py horizon/openstack_dashboard/local/enabled/_32000_moon.py +RUN cp -rv moon/ horizon/openstack_dashboard/dashboards/ + +CMD ["/bin/sh", "/root/run.sh"] \ No newline at end of file diff --git a/moon_dashboard/LICENSE b/moon_dashboard/LICENSE new file mode 100644 index 00000000..e69de29b diff --git a/moon_dashboard/MANIFEST.in b/moon_dashboard/MANIFEST.in new file mode 100644 index 00000000..1f077b06 --- /dev/null +++ b/moon_dashboard/MANIFEST.in @@ -0,0 +1,3 @@ +include setup.py + +recursive-include myplugin *.js *.html *.scss \ No newline at end of file diff --git a/moon_dashboard/README.md b/moon_dashboard/README.md new file mode 100644 index 00000000..fca52b2d --- /dev/null +++ b/moon_dashboard/README.md @@ -0,0 +1,40 @@ +# Moon plugin for Horizon (OpenStack Dashboard) + +## Install Horizon + +https://docs.openstack.org/horizon/latest/install/index.html + +or for developper quick start: + +https://docs.openstack.org/horizon/latest/contributor/quickstart.html + + +## Moon plugin + +Clone the plugin: + +```bash +git clone https://gitlab.forge.orange-labs.fr/moon/dashboard.git +``` + +* ``$plugin`` is the location of moon plugin +* ``$horizon`` is the location of horizon + +Make symbolic link to enabled file: + +```bash +ln -s $plugin/moon/enabled/_32000_moon.py $horizon/openstack_dashboard/local/enabled/_32000_moon.py +``` + +Make symbolic link to dashboard folder: + +```bash +ln -s $plugin/moon/ $horizon/openstack_dashboard/dashboards/moon +``` + +Finish by restarting the Horizon server. + +## Set Moon API endpoint + +Set the endpoint in $plugin/moon/moon/static/moon/js/moon.module.js file + diff --git a/moon_dashboard/README.rst b/moon_dashboard/README.rst new file mode 100644 index 00000000..de9c4058 --- /dev/null +++ b/moon_dashboard/README.rst @@ -0,0 +1,39 @@ +============================================= +Moon plugin for Horizon (OpenStack Dashboard) +============================================= + +Install Horizon +=============== + +https://docs.openstack.org/horizon/latest/install/index.html + +or for developper quick start: + +https://docs.openstack.org/horizon/latest/contributor/quickstart.html + + +Moon plugin +=========== + +Clone the plugin: + +"git clone https://gitlab.forge.orange-labs.fr/moon/dashboard.git" + +* ``plugin`` is the location of moon plugin +* ``horizon`` is the location of horizon + +Make symbolic link to enabled file: + +"ln -s ``plugin`̀`/moon/enabled/_32000_moon.py ``horizon``/openstack_dashboard/local/enabled/_32000_moon.py" + +Make symbolic link to dashboard folder: + +"ln -s ``plugin`̀`/moon/ ``horizon``/openstack_dashboard/dashboards/moon" + +Finish by restarting the Horizon server. + + +Set Moon API endpoint +=========== + +Set the endpoint in ``plugin``/moon/moon/static/moon/js/moon.module.js file \ No newline at end of file diff --git a/moon_dashboard/babel-django.cfg b/moon_dashboard/babel-django.cfg new file mode 100644 index 00000000..fa906ad8 --- /dev/null +++ b/moon_dashboard/babel-django.cfg @@ -0,0 +1,5 @@ +[extractors] +django = django_babel.extract:extract_django + +[python: **.py] +[django: **/templates/**.html] \ No newline at end of file diff --git a/moon_dashboard/babel-djangojs.cfg b/moon_dashboard/babel-djangojs.cfg new file mode 100644 index 00000000..1c07ba6a --- /dev/null +++ b/moon_dashboard/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] \ No newline at end of file diff --git a/moon_dashboard/moon/__init__.py b/moon_dashboard/moon/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/moon_dashboard/moon/dashboard.py b/moon_dashboard/moon/dashboard.py new file mode 100644 index 00000000..0e3e491e --- /dev/null +++ b/moon_dashboard/moon/dashboard.py @@ -0,0 +1,13 @@ +from django.utils.translation import ugettext_lazy as _ + +import horizon + + +class Moon(horizon.Dashboard): + name = _("Moon") + slug = "moon" + panels = ('model','policy','pdp',) # Add your panels here. + default_panel = 'model' # Specify the slug of the default panel. + + +horizon.register(Moon) diff --git a/moon_dashboard/moon/enabled/_32000_moon.py b/moon_dashboard/moon/enabled/_32000_moon.py new file mode 100644 index 00000000..73198de6 --- /dev/null +++ b/moon_dashboard/moon/enabled/_32000_moon.py @@ -0,0 +1,19 @@ +# The name of the dashboard to be added to HORIZON['dashboards']. Required. +DASHBOARD = 'moon' + +# If set to True, this dashboard will not be added to the settings. +DISABLED = False + +# A list of AngularJS modules to be loaded when Angular bootstraps. +ADD_ANGULAR_MODULES = ['moon'] + +# Automatically discover static resources in installed apps +AUTO_DISCOVER_STATIC_FILES = True + +# A list of applications to be added to INSTALLED_APPS. +ADD_INSTALLED_APPS = [ + 'openstack_dashboard.dashboards.moon', +] + +# A list of scss files to be included in the compressed set of files +ADD_SCSS_FILES = ['moon/scss/moon.scss'] diff --git a/moon_dashboard/moon/model/__init__.py b/moon_dashboard/moon/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/moon_dashboard/moon/model/panel.py b/moon_dashboard/moon/model/panel.py new file mode 100644 index 00000000..9cb65ef0 --- /dev/null +++ b/moon_dashboard/moon/model/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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon +from openstack_dashboard.dashboards.moon import dashboard + +class Model(horizon.Panel): + name = _("Models") + slug = "model" + + +dashboard.Moon.register(Model) diff --git a/moon_dashboard/moon/model/templates/model/index.html b/moon_dashboard/moon/model/templates/model/index.html new file mode 100644 index 00000000..db372a02 --- /dev/null +++ b/moon_dashboard/moon/model/templates/model/index.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Models" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Models") %} +{% endblock page_header %} + + + +{% block main %} + + +{% endblock %} + diff --git a/moon_dashboard/moon/model/tests.py b/moon_dashboard/moon/model/tests.py new file mode 100644 index 00000000..ec988636 --- /dev/null +++ b/moon_dashboard/moon/model/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 MypanelTests(test.TestCase): + # Unit tests for mypanel. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/moon_dashboard/moon/model/urls.py b/moon_dashboard/moon/model/urls.py new file mode 100644 index 00000000..ca9507fb --- /dev/null +++ b/moon_dashboard/moon/model/urls.py @@ -0,0 +1,20 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls import url + +from openstack_dashboard.dashboards.moon.model import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), +] diff --git a/moon_dashboard/moon/model/views.py b/moon_dashboard/moon/model/views.py new file mode 100644 index 00000000..73509537 --- /dev/null +++ b/moon_dashboard/moon/model/views.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 horizon import views + + +class IndexView(views.APIView): + # A very simple class-based view... + template_name = 'moon/model/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context diff --git a/moon_dashboard/moon/pdp/__init__.py b/moon_dashboard/moon/pdp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/moon_dashboard/moon/pdp/panel.py b/moon_dashboard/moon/pdp/panel.py new file mode 100644 index 00000000..9c4b3fa3 --- /dev/null +++ b/moon_dashboard/moon/pdp/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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon +from openstack_dashboard.dashboards.moon import dashboard + +class Pdp(horizon.Panel): + name = _("PDP") + slug = "pdp" + + +dashboard.Moon.register(Pdp) diff --git a/moon_dashboard/moon/pdp/templates/pdp/index.html b/moon_dashboard/moon/pdp/templates/pdp/index.html new file mode 100644 index 00000000..30ac5f93 --- /dev/null +++ b/moon_dashboard/moon/pdp/templates/pdp/index.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "PDP" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("PDP") %} +{% endblock page_header %} + + + +{% block main %} + + +{% endblock %} + diff --git a/moon_dashboard/moon/pdp/tests.py b/moon_dashboard/moon/pdp/tests.py new file mode 100644 index 00000000..ec988636 --- /dev/null +++ b/moon_dashboard/moon/pdp/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 MypanelTests(test.TestCase): + # Unit tests for mypanel. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/moon_dashboard/moon/pdp/urls.py b/moon_dashboard/moon/pdp/urls.py new file mode 100644 index 00000000..a66c8e0c --- /dev/null +++ b/moon_dashboard/moon/pdp/urls.py @@ -0,0 +1,20 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls import url + +from openstack_dashboard.dashboards.moon.pdp import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), +] diff --git a/moon_dashboard/moon/pdp/views.py b/moon_dashboard/moon/pdp/views.py new file mode 100644 index 00000000..8355a5d5 --- /dev/null +++ b/moon_dashboard/moon/pdp/views.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 horizon import views + + +class IndexView(views.APIView): + # A very simple class-based view... + template_name = 'moon/pdp/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context diff --git a/moon_dashboard/moon/policy/__init__.py b/moon_dashboard/moon/policy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/moon_dashboard/moon/policy/panel.py b/moon_dashboard/moon/policy/panel.py new file mode 100644 index 00000000..875a2d76 --- /dev/null +++ b/moon_dashboard/moon/policy/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. + +from django.utils.translation import ugettext_lazy as _ + +import horizon +from openstack_dashboard.dashboards.moon import dashboard + +class Policy(horizon.Panel): + name = _("Policies") + slug = "policy" + + +dashboard.Moon.register(Policy) diff --git a/moon_dashboard/moon/policy/templates/policy/index.html b/moon_dashboard/moon/policy/templates/policy/index.html new file mode 100644 index 00000000..67cd9c3d --- /dev/null +++ b/moon_dashboard/moon/policy/templates/policy/index.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Policies" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Policies") %} +{% endblock page_header %} + + + +{% block main %} + + +{% endblock %} + diff --git a/moon_dashboard/moon/policy/tests.py b/moon_dashboard/moon/policy/tests.py new file mode 100644 index 00000000..ec988636 --- /dev/null +++ b/moon_dashboard/moon/policy/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 MypanelTests(test.TestCase): + # Unit tests for mypanel. + def test_me(self): + self.assertTrue(1 + 1 == 2) diff --git a/moon_dashboard/moon/policy/urls.py b/moon_dashboard/moon/policy/urls.py new file mode 100644 index 00000000..81bde0ca --- /dev/null +++ b/moon_dashboard/moon/policy/urls.py @@ -0,0 +1,20 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.conf.urls import url + +from openstack_dashboard.dashboards.moon.policy import views + + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), +] diff --git a/moon_dashboard/moon/policy/views.py b/moon_dashboard/moon/policy/views.py new file mode 100644 index 00000000..826c833b --- /dev/null +++ b/moon_dashboard/moon/policy/views.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 horizon import views + + +class IndexView(views.APIView): + # A very simple class-based view... + template_name = 'moon/policy/index.html' + + def get_data(self, request, context, *args, **kwargs): + # Add data to the context here... + return context diff --git a/moon_dashboard/moon/static/moon/js/angular-resource.js b/moon_dashboard/moon/static/moon/js/angular-resource.js new file mode 100644 index 00000000..e8bb3014 --- /dev/null +++ b/moon_dashboard/moon/static/moon/js/angular-resource.js @@ -0,0 +1,863 @@ +/** + * @license AngularJS v1.5.8 + * (c) 2010-2016 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular) {'use strict'; + +var $resourceMinErr = angular.$$minErr('$resource'); + +// Helper functions and regex to lookup a dotted path on an object +// stopping at undefined/null. The path must be composed of ASCII +// identifiers (just like $parse) +var MEMBER_NAME_REGEX = /^(\.[a-zA-Z_$@][0-9a-zA-Z_$@]*)+$/; + +function isValidDottedPath(path) { + return (path != null && path !== '' && path !== 'hasOwnProperty' && + MEMBER_NAME_REGEX.test('.' + path)); +} + +function lookupDottedPath(obj, path) { + if (!isValidDottedPath(path)) { + throw $resourceMinErr('badmember', 'Dotted member path "@{0}" is invalid.', path); + } + var keys = path.split('.'); + for (var i = 0, ii = keys.length; i < ii && angular.isDefined(obj); i++) { + var key = keys[i]; + obj = (obj !== null) ? obj[key] : undefined; + } + return obj; +} + +/** + * Create a shallow copy of an object and clear other fields from the destination + */ +function shallowClearAndCopy(src, dst) { + dst = dst || {}; + + angular.forEach(dst, function(value, key) { + delete dst[key]; + }); + + for (var key in src) { + if (src.hasOwnProperty(key) && !(key.charAt(0) === '$' && key.charAt(1) === '$')) { + dst[key] = src[key]; + } + } + + return dst; +} + +/** + * @ngdoc module + * @name ngResource + * @description + * + * # ngResource + * + * The `ngResource` module provides interaction support with RESTful services + * via the $resource service. + * + * + *
+ * + * See {@link ngResource.$resourceProvider} and {@link ngResource.$resource} for usage. + */ + +/** + * @ngdoc provider + * @name $resourceProvider + * + * @description + * + * Use `$resourceProvider` to change the default behavior of the {@link ngResource.$resource} + * service. + * + * ## Dependencies + * Requires the {@link ngResource } module to be installed. + * + */ + +/** + * @ngdoc service + * @name $resource + * @requires $http + * @requires ng.$log + * @requires $q + * @requires ng.$timeout + * + * @description + * A factory which creates a resource object that lets you interact with + * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources. + * + * The returned resource object has action methods which provide high-level behaviors without + * the need to interact with the low level {@link ng.$http $http} service. + * + * Requires the {@link ngResource `ngResource`} module to be installed. + * + * By default, trailing slashes will be stripped from the calculated URLs, + * which can pose problems with server backends that do not expect that + * behavior. This can be disabled by configuring the `$resourceProvider` like + * this: + * + * ```js + app.config(['$resourceProvider', function($resourceProvider) { + // Don't strip trailing slashes from calculated URLs + $resourceProvider.defaults.stripTrailingSlashes = false; + }]); + * ``` + * + * @param {string} url A parameterized URL template with parameters prefixed by `:` as in + * `/user/:username`. If you are using a URL with a port number (e.g. + * `http://example.com:8080/api`), it will be respected. + * + * If you are using a url with a suffix, just add the suffix, like this: + * `$resource('http://example.com/resource.json')` or `$resource('http://example.com/:id.json')` + * or even `$resource('http://example.com/resource/:resource_id.:format')` + * If the parameter before the suffix is empty, :resource_id in this case, then the `/.` will be + * collapsed down to a single `.`. If you need this sequence to appear and not collapse then you + * can escape it with `/\.`. + * + * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in + * `actions` methods. If a parameter value is a function, it will be called every time + * a param value needs to be obtained for a request (unless the param was overridden). The function + * will be passed the current data value as an argument. + * + * Each key value in the parameter object is first bound to url template if present and then any + * excess keys are appended to the url search query after the `?`. + * + * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in + * URL `/path/greet?salutation=Hello`. + * + * If the parameter value is prefixed with `@`, then the value for that parameter will be + * extracted from the corresponding property on the `data` object (provided when calling a + * "non-GET" action method). + * For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of + * `someParam` will be `data.someProp`. + * Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action + * method that does not accept a request body) + * + * @param {Object.=} actions Hash with declaration of custom actions that should extend + * the default set of resource actions. The declaration should be created in the format of {@link + * ng.$http#usage $http.config}: + * + * {action1: {method:?, params:?, isArray:?, headers:?, ...}, + * action2: {method:?, params:?, isArray:?, headers:?, ...}, + * ...} + * + * Where: + * + * - **`action`** – {string} – The name of action. This name becomes the name of the method on + * your resource object. + * - **`method`** – {string} – Case insensitive HTTP method (e.g. `GET`, `POST`, `PUT`, + * `DELETE`, `JSONP`, etc). + * - **`params`** – {Object=} – Optional set of pre-bound parameters for this action. If any of + * the parameter value is a function, it will be called every time when a param value needs to + * be obtained for a request (unless the param was overridden). The function will be passed the + * current data value as an argument. + * - **`url`** – {string} – action specific `url` override. The url templating is supported just + * like for the resource-level urls. + * - **`isArray`** – {boolean=} – If true then the returned object for this action is an array, + * see `returns` section. + * - **`transformRequest`** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * request body and headers and returns its transformed (typically serialized) version. + * By default, transformRequest will contain one function that checks if the request data is + * an object and serializes to using `angular.toJson`. To prevent this behavior, set + * `transformRequest` to an empty array: `transformRequest: []` + * - **`transformResponse`** – + * `{function(data, headersGetter)|Array.}` – + * transform function or an array of such functions. The transform function takes the http + * response body and headers and returns its transformed (typically deserialized) version. + * By default, transformResponse will contain one function that checks if the response looks + * like a JSON string and deserializes it using `angular.fromJson`. To prevent this behavior, + * set `transformResponse` to an empty array: `transformResponse: []` + * - **`cache`** – `{boolean|Cache}` – If true, a default $http cache will be used to cache the + * GET request, otherwise if a cache instance built with + * {@link ng.$cacheFactory $cacheFactory}, this cache will be used for + * caching. + * - **`timeout`** – `{number}` – timeout in milliseconds.
+ * **Note:** In contrast to {@link ng.$http#usage $http.config}, {@link ng.$q promises} are + * **not** supported in $resource, because the same value would be used for multiple requests. + * If you are looking for a way to cancel requests, you should use the `cancellable` option. + * - **`cancellable`** – `{boolean}` – if set to true, the request made by a "non-instance" call + * will be cancelled (if not already completed) by calling `$cancelRequest()` on the call's + * return value. Calling `$cancelRequest()` for a non-cancellable or an already + * completed/cancelled request will have no effect.
+ * - **`withCredentials`** - `{boolean}` - whether to set the `withCredentials` flag on the + * XHR object. See + * [requests with credentials](https://developer.mozilla.org/en/http_access_control#section_5) + * for more information. + * - **`responseType`** - `{string}` - see + * [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType). + * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods - + * `response` and `responseError`. Both `response` and `responseError` interceptors get called + * with `http response` object. See {@link ng.$http $http interceptors}. + * + * @param {Object} options Hash with custom settings that should extend the + * default `$resourceProvider` behavior. The supported options are: + * + * - **`stripTrailingSlashes`** – {boolean} – If true then the trailing + * slashes from any calculated URL will be stripped. (Defaults to true.) + * - **`cancellable`** – {boolean} – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return value. + * This can be overwritten per action. (Defaults to false.) + * + * @returns {Object} A resource "class" object with methods for the default set of resource actions + * optionally extended with custom `actions`. The default set contains these actions: + * ```js + * { 'get': {method:'GET'}, + * 'save': {method:'POST'}, + * 'query': {method:'GET', isArray:true}, + * 'remove': {method:'DELETE'}, + * 'delete': {method:'DELETE'} }; + * ``` + * + * Calling these methods invoke an {@link ng.$http} with the specified http method, + * destination and parameters. When the data is returned from the server then the object is an + * instance of the resource class. The actions `save`, `remove` and `delete` are available on it + * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create, + * read, update, delete) on server-side data like this: + * ```js + * var User = $resource('/user/:userId', {userId:'@id'}); + * var user = User.get({userId:123}, function() { + * user.abc = true; + * user.$save(); + * }); + * ``` + * + * It is important to realize that invoking a $resource object method immediately returns an + * empty reference (object or array depending on `isArray`). Once the data is returned from the + * server the existing reference is populated with the actual data. This is a useful trick since + * usually the resource is assigned to a model which is then rendered by the view. Having an empty + * object results in no rendering, once the data arrives from the server then the object is + * populated with the data and the view automatically re-renders itself showing the new data. This + * means that in most cases one never has to write a callback function for the action methods. + * + * The action methods on the class object or instance object can be invoked with the following + * parameters: + * + * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])` + * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])` + * - non-GET instance actions: `instance.$action([parameters], [success], [error])` + * + * + * Success callback is called with (value, responseHeaders) arguments, where the value is + * the populated resource instance or collection object. The error callback is called + * with (httpResponse) argument. + * + * Class actions return empty instance (with additional properties below). + * Instance actions return promise of the action. + * + * The Resource instances and collections have these additional properties: + * + * - `$promise`: the {@link ng.$q promise} of the original server interaction that created this + * instance or collection. + * + * On success, the promise is resolved with the same resource instance or collection object, + * updated with data from server. This makes it easy to use in + * {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view + * rendering until the resource(s) are loaded. + * + * On failure, the promise is rejected with the {@link ng.$http http response} object, without + * the `resource` property. + * + * If an interceptor object was provided, the promise will instead be resolved with the value + * returned by the interceptor. + * + * - `$resolved`: `true` after first server interaction is completed (either with success or + * rejection), `false` before that. Knowing if the Resource has been resolved is useful in + * data-binding. + * + * The Resource instances and collections have these additional methods: + * + * - `$cancelRequest`: If there is a cancellable, pending request related to the instance or + * collection, calling this method will abort the request. + * + * The Resource instances have these additional methods: + * + * - `toJSON`: It returns a simple object without any of the extra properties added as part of + * the Resource API. This object can be serialized through {@link angular.toJson} safely + * without attaching Angular-specific fields. Notice that `JSON.stringify` (and + * `angular.toJson`) automatically use this method when serializing a Resource instance + * (see [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#toJSON()_behavior)). + * + * @example + * + * # Credit card resource + * + * ```js + // Define CreditCard class + var CreditCard = $resource('/user/:userId/card/:cardId', + {userId:123, cardId:'@id'}, { + charge: {method:'POST', params:{charge:true}} + }); + + // We can retrieve a collection from the server + var cards = CreditCard.query(function() { + // GET: /user/123/card + // server returns: [ {id:456, number:'1234', name:'Smith'} ]; + + var card = cards[0]; + // each item is an instance of CreditCard + expect(card instanceof CreditCard).toEqual(true); + card.name = "J. Smith"; + // non GET methods are mapped onto the instances + card.$save(); + // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'} + // server returns: {id:456, number:'1234', name: 'J. Smith'}; + + // our custom method is mapped as well. + card.$charge({amount:9.99}); + // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'} + }); + + // we can create an instance as well + var newCard = new CreditCard({number:'0123'}); + newCard.name = "Mike Smith"; + newCard.$save(); + // POST: /user/123/card {number:'0123', name:'Mike Smith'} + // server returns: {id:789, number:'0123', name: 'Mike Smith'}; + expect(newCard.id).toEqual(789); + * ``` + * + * The object returned from this function execution is a resource "class" which has "static" method + * for each action in the definition. + * + * Calling these methods invoke `$http` on the `url` template with the given `method`, `params` and + * `headers`. + * + * @example + * + * # User resource + * + * When the data is returned from the server then the object is an instance of the resource type and + * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD + * operations (create, read, update, delete) on server-side data. + + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(user) { + user.abc = true; + user.$save(); + }); + ``` + * + * It's worth noting that the success callback for `get`, `query` and other methods gets passed + * in the response that came from the server as well as $http header getter function, so one + * could rewrite the above example and get access to http headers as: + * + ```js + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}, function(user, getResponseHeaders){ + user.abc = true; + user.$save(function(user, putResponseHeaders) { + //user => saved user object + //putResponseHeaders => $http header getter + }); + }); + ``` + * + * You can also access the raw `$http` promise via the `$promise` property on the object returned + * + ``` + var User = $resource('/user/:userId', {userId:'@id'}); + User.get({userId:123}) + .$promise.then(function(user) { + $scope.user = user; + }); + ``` + * + * @example + * + * # Creating a custom 'PUT' request + * + * In this example we create a custom method on our resource to make a PUT request + * ```js + * var app = angular.module('app', ['ngResource', 'ngRoute']); + * + * // Some APIs expect a PUT request in the format URL/object/ID + * // Here we are creating an 'update' method + * app.factory('Notes', ['$resource', function($resource) { + * return $resource('/notes/:id', null, + * { + * 'update': { method:'PUT' } + * }); + * }]); + * + * // In our controller we get the ID from the URL using ngRoute and $routeParams + * // We pass in $routeParams and our Notes factory along with $scope + * app.controller('NotesCtrl', ['$scope', '$routeParams', 'Notes', + function($scope, $routeParams, Notes) { + * // First get a note object from the factory + * var note = Notes.get({ id:$routeParams.id }); + * $id = note.id; + * + * // Now call update passing in the ID first then the object you are updating + * Notes.update({ id:$id }, note); + * + * // This will PUT /notes/ID with the note object in the request payload + * }]); + * ``` + * + * @example + * + * # Cancelling requests + * + * If an action's configuration specifies that it is cancellable, you can cancel the request related + * to an instance or collection (as long as it is a result of a "non-instance" call): + * + ```js + // ...defining the `Hotel` resource... + var Hotel = $resource('/api/hotel/:id', {id: '@id'}, { + // Let's make the `query()` method cancellable + query: {method: 'get', isArray: true, cancellable: true} + }); + + // ...somewhere in the PlanVacationController... + ... + this.onDestinationChanged = function onDestinationChanged(destination) { + // We don't care about any pending request for hotels + // in a different destination any more + this.availableHotels.$cancelRequest(); + + // Let's query for hotels in '' + // (calls: /api/hotel?location=) + this.availableHotels = Hotel.query({location: destination}); + }; + ``` + * + */ +angular.module('ngResource', ['ng']). + provider('$resource', function() { + var PROTOCOL_AND_DOMAIN_REGEX = /^https?:\/\/[^\/]*/; + var provider = this; + + /** + * @ngdoc property + * @name $resourceProvider#defaults + * @description + * Object containing default options used when creating `$resource` instances. + * + * The default values satisfy a wide range of usecases, but you may choose to overwrite any of + * them to further customize your instances. The available properties are: + * + * - **stripTrailingSlashes** – `{boolean}` – If true, then the trailing slashes from any + * calculated URL will be stripped.
+ * (Defaults to true.) + * - **cancellable** – `{boolean}` – If true, the request made by a "non-instance" call will be + * cancelled (if not already completed) by calling `$cancelRequest()` on the call's return + * value. For more details, see {@link ngResource.$resource}. This can be overwritten per + * resource class or action.
+ * (Defaults to false.) + * - **actions** - `{Object.}` - A hash with default actions declarations. Actions are + * high-level methods corresponding to RESTful actions/methods on resources. An action may + * specify what HTTP method to use, what URL to hit, if the return value will be a single + * object or a collection (array) of objects etc. For more details, see + * {@link ngResource.$resource}. The actions can also be enhanced or overwritten per resource + * class.
+ * The default actions are: + * ```js + * { + * get: {method: 'GET'}, + * save: {method: 'POST'}, + * query: {method: 'GET', isArray: true}, + * remove: {method: 'DELETE'}, + * delete: {method: 'DELETE'} + * } + * ``` + * + * #### Example + * + * For example, you can specify a new `update` action that uses the `PUT` HTTP verb: + * + * ```js + * angular. + * module('myApp'). + * config(['resourceProvider', function ($resourceProvider) { + * $resourceProvider.defaults.actions.update = { + * method: 'PUT' + * }; + * }); + * ``` + * + * Or you can even overwrite the whole `actions` list and specify your own: + * + * ```js + * angular. + * module('myApp'). + * config(['resourceProvider', function ($resourceProvider) { + * $resourceProvider.defaults.actions = { + * create: {method: 'POST'} + * get: {method: 'GET'}, + * getAll: {method: 'GET', isArray:true}, + * update: {method: 'PUT'}, + * delete: {method: 'DELETE'} + * }; + * }); + * ``` + * + */ + this.defaults = { + // Strip slashes by default + stripTrailingSlashes: true, + + // Make non-instance requests cancellable (via `$cancelRequest()`) + cancellable: false, + + // Default actions configuration + actions: { + 'get': {method: 'GET'}, + 'save': {method: 'POST'}, + 'query': {method: 'GET', isArray: true}, + 'remove': {method: 'DELETE'}, + 'delete': {method: 'DELETE'} + } + }; + + this.$get = ['$http', '$log', '$q', '$timeout', function($http, $log, $q, $timeout) { + + var noop = angular.noop, + forEach = angular.forEach, + extend = angular.extend, + copy = angular.copy, + isFunction = angular.isFunction; + + /** + * We need our custom method because encodeURIComponent is too aggressive and doesn't follow + * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set + * (pchar) allowed in path segments: + * segment = *pchar + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * pct-encoded = "%" HEXDIG HEXDIG + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriSegment(val) { + return encodeUriQuery(val, true). + replace(/%26/gi, '&'). + replace(/%3D/gi, '='). + replace(/%2B/gi, '+'); + } + + + /** + * This method is intended for encoding *key* or *value* parts of query component. We need a + * custom method because encodeURIComponent is too aggressive and encodes stuff that doesn't + * have to be encoded per http://tools.ietf.org/html/rfc3986: + * query = *( pchar / "/" / "?" ) + * pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + * pct-encoded = "%" HEXDIG HEXDIG + * sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + * / "*" / "+" / "," / ";" / "=" + */ + function encodeUriQuery(val, pctEncodeSpaces) { + return encodeURIComponent(val). + replace(/%40/gi, '@'). + replace(/%3A/gi, ':'). + replace(/%24/g, '$'). + replace(/%2C/gi, ','). + replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); + } + + function Route(template, defaults) { + this.template = template; + this.defaults = extend({}, provider.defaults, defaults); + this.urlParams = {}; + } + + Route.prototype = { + setUrlParams: function(config, params, actionUrl) { + var self = this, + url = actionUrl || self.template, + val, + encodedVal, + protocolAndDomain = ''; + + var urlParams = self.urlParams = {}; + forEach(url.split(/\W/), function(param) { + if (param === 'hasOwnProperty') { + throw $resourceMinErr('badname', "hasOwnProperty is not a valid parameter name."); + } + if (!(new RegExp("^\\d+$").test(param)) && param && + (new RegExp("(^|[^\\\\]):" + param + "(\\W|$)").test(url))) { + urlParams[param] = { + isQueryParamValue: (new RegExp("\\?.*=:" + param + "(?:\\W|$)")).test(url) + }; + } + }); + url = url.replace(/\\:/g, ':'); + url = url.replace(PROTOCOL_AND_DOMAIN_REGEX, function(match) { + protocolAndDomain = match; + return ''; + }); + + params = params || {}; + forEach(self.urlParams, function(paramInfo, urlParam) { + val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam]; + if (angular.isDefined(val) && val !== null) { + if (paramInfo.isQueryParamValue) { + encodedVal = encodeUriQuery(val, true); + } else { + encodedVal = encodeUriSegment(val); + } + url = url.replace(new RegExp(":" + urlParam + "(\\W|$)", "g"), function(match, p1) { + return encodedVal + p1; + }); + } else { + url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W|$)", "g"), function(match, + leadingSlashes, tail) { + if (tail.charAt(0) == '/') { + return tail; + } else { + return leadingSlashes + tail; + } + }); + } + }); + + // strip trailing slashes and set the url (unless this behavior is specifically disabled) + if (self.defaults.stripTrailingSlashes) { + url = url.replace(/\/+$/, '') || '/'; + } + + // then replace collapse `/.` if found in the last URL path segment before the query + // E.g. `http://url.com/id./format?q=x` becomes `http://url.com/id.format?q=x` + url = url.replace(/\/\.(?=\w+($|\?))/, '.'); + // replace escaped `/\.` with `/.` + config.url = protocolAndDomain + url.replace(/\/\\\./, '/.'); + + + // set params - delegate param encoding to $http + forEach(params, function(value, key) { + if (!self.urlParams[key]) { + config.params = config.params || {}; + config.params[key] = value; + } + }); + } + }; + + + function resourceFactory(url, paramDefaults, actions, options) { + var route = new Route(url, options); + + actions = extend({}, provider.defaults.actions, actions); + + function extractParams(data, actionParams) { + var ids = {}; + actionParams = extend({}, paramDefaults, actionParams); + forEach(actionParams, function(value, key) { + if (isFunction(value)) { value = value(data); } + ids[key] = value && value.charAt && value.charAt(0) == '@' ? + lookupDottedPath(data, value.substr(1)) : value; + }); + return ids; + } + + function defaultResponseInterceptor(response) { + return response.resource; + } + + function Resource(value) { + shallowClearAndCopy(value || {}, this); + } + + Resource.prototype.toJSON = function() { + var data = extend({}, this); + delete data.$promise; + delete data.$resolved; + return data; + }; + + forEach(actions, function(action, name) { + var hasBody = /^(POST|PUT|PATCH)$/i.test(action.method); + var numericTimeout = action.timeout; + var cancellable = angular.isDefined(action.cancellable) ? action.cancellable : + (options && angular.isDefined(options.cancellable)) ? options.cancellable : + provider.defaults.cancellable; + + if (numericTimeout && !angular.isNumber(numericTimeout)) { + $log.debug('ngResource:\n' + + ' Only numeric values are allowed as `timeout`.\n' + + ' Promises are not supported in $resource, because the same value would ' + + 'be used for multiple requests. If you are looking for a way to cancel ' + + 'requests, you should use the `cancellable` option.'); + delete action.timeout; + numericTimeout = null; + } + + Resource[name] = function(a1, a2, a3, a4) { + var params = {}, data, success, error; + + /* jshint -W086 */ /* (purposefully fall through case statements) */ + switch (arguments.length) { + case 4: + error = a4; + success = a3; + //fallthrough + case 3: + case 2: + if (isFunction(a2)) { + if (isFunction(a1)) { + success = a1; + error = a2; + break; + } + + success = a2; + error = a3; + //fallthrough + } else { + params = a1; + data = a2; + success = a3; + break; + } + case 1: + if (isFunction(a1)) success = a1; + else if (hasBody) data = a1; + else params = a1; + break; + case 0: break; + default: + throw $resourceMinErr('badargs', + "Expected up to 4 arguments [params, data, success, error], got {0} arguments", + arguments.length); + } + /* jshint +W086 */ /* (purposefully fall through case statements) */ + + var isInstanceCall = this instanceof Resource; + var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data)); + var httpConfig = {}; + var responseInterceptor = action.interceptor && action.interceptor.response || + defaultResponseInterceptor; + var responseErrorInterceptor = action.interceptor && action.interceptor.responseError || + undefined; + var timeoutDeferred; + var numericTimeoutPromise; + + forEach(action, function(value, key) { + switch (key) { + default: + httpConfig[key] = copy(value); + break; + case 'params': + case 'isArray': + case 'interceptor': + case 'cancellable': + break; + } + }); + + if (!isInstanceCall && cancellable) { + timeoutDeferred = $q.defer(); + httpConfig.timeout = timeoutDeferred.promise; + + if (numericTimeout) { + numericTimeoutPromise = $timeout(timeoutDeferred.resolve, numericTimeout); + } + } + + if (hasBody) httpConfig.data = data; + route.setUrlParams(httpConfig, + extend({}, extractParams(data, action.params || {}), params), + action.url); + + var promise = $http(httpConfig).then(function(response) { + var data = response.data; + + if (data) { + // Need to convert action.isArray to boolean in case it is undefined + // jshint -W018 + if (angular.isArray(data) !== (!!action.isArray)) { + throw $resourceMinErr('badcfg', + 'Error in resource configuration for action `{0}`. Expected response to ' + + 'contain an {1} but got an {2} (Request: {3} {4})', name, action.isArray ? 'array' : 'object', + angular.isArray(data) ? 'array' : 'object', httpConfig.method, httpConfig.url); + } + // jshint +W018 + if (action.isArray) { + value.length = 0; + forEach(data, function(item) { + if (typeof item === "object") { + value.push(new Resource(item)); + } else { + // Valid JSON values may be string literals, and these should not be converted + // into objects. These items will not have access to the Resource prototype + // methods, but unfortunately there + value.push(item); + } + }); + } else { + var promise = value.$promise; // Save the promise + shallowClearAndCopy(data, value); + value.$promise = promise; // Restore the promise + } + } + response.resource = value; + + return response; + }, function(response) { + (error || noop)(response); + return $q.reject(response); + }); + + promise['finally'](function() { + value.$resolved = true; + if (!isInstanceCall && cancellable) { + value.$cancelRequest = angular.noop; + $timeout.cancel(numericTimeoutPromise); + timeoutDeferred = numericTimeoutPromise = httpConfig.timeout = null; + } + }); + + promise = promise.then( + function(response) { + var value = responseInterceptor(response); + (success || noop)(value, response.headers); + return value; + }, + responseErrorInterceptor); + + if (!isInstanceCall) { + // we are creating instance / collection + // - set the initial promise + // - return the instance / collection + value.$promise = promise; + value.$resolved = false; + if (cancellable) value.$cancelRequest = timeoutDeferred.resolve; + + return value; + } + + // instance call + return promise; + }; + + + Resource.prototype['$' + name] = function(params, success, error) { + if (isFunction(params)) { + error = success; success = params; params = {}; + } + var result = Resource[name].call(this, params, this, success, error); + return result.$promise || result; + }; + }); + + Resource.bind = function(additionalParamDefaults) { + return resourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions); + }; + + return Resource; + } + + return resourceFactory; + }]; + }); + + +})(window, window.angular); diff --git a/moon_dashboard/moon/static/moon/js/import.service.js b/moon_dashboard/moon/static/moon/js/import.service.js new file mode 100755 index 00000000..d55c8a19 --- /dev/null +++ b/moon_dashboard/moon/static/moon/js/import.service.js @@ -0,0 +1,27 @@ +(function () { + + 'use strict'; + + angular + .module('moon') + .factory('moon.import.service', importService); + + importService.$inject = ['moon.util.service', '$resource', 'moon.URI']; + + function importService(util, $resource, URI) { + var host = URI.API; + var importResource = $resource(host + '/import/', {}, { + create: { method: 'POST' }, + }); + + return { + importData: function importData(data) { + return importResource.create(null, data).$promise.then(success, util.displayErrorFunction('Unable to import data')); + + function success(data) { + util.displaySuccess('Data imported'); + } + } + } + } +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/js/moon.module.js b/moon_dashboard/moon/static/moon/js/moon.module.js new file mode 100755 index 00000000..ed56ec2a --- /dev/null +++ b/moon_dashboard/moon/static/moon/js/moon.module.js @@ -0,0 +1,29 @@ +/** +# Copyright 2015 Orange +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + */ + +(function () { + + 'use strict'; + + var moon = angular + + .module('moon', [ + 'ngResource', + ]).constant('moon.URI', { + API: 'http://{{MANAGER_HOST}}:{{MANAGER_PORT}}', + }) + +})(); diff --git a/moon_dashboard/moon/static/moon/js/util.service.js b/moon_dashboard/moon/static/moon/js/util.service.js new file mode 100755 index 00000000..18ae901d --- /dev/null +++ b/moon_dashboard/moon/static/moon/js/util.service.js @@ -0,0 +1,136 @@ +(function () { + + 'use strict'; + + angular + .module('moon') + .factory('moon.util.service', utilService); + + utilService.$inject = ['horizon.framework.widgets.toast.service']; + + function utilService(toast) { + + + return { + mapToArray: function mapToArray(map, action) { + var result = [] + for (var key in map) { + if (map.hasOwnProperty(key)) { + var item = map[key]; + item.id = key; + if (action != null) { + action(item); + } + result.push(item); + } + } + return result; + }, + + mapIdToItem: function mapIdToItem(array, map) { + if (array) { + for (var index = 0; index < array.length; index++) { + var id = array[index]; + array[index] = map[id]; + } + } + }, + + mapItemToId: function mapItemToId(array) { + if (array) { + for (var index = 0; index < array.length; index++) { + var item = array[index]; + array[index] = item.id; + } + } + }, + + addToMap: function addToMap(array, map) { + if (array) { + for (var index = 0; index < array.length; index++) { + var item = array[index]; + map[item.id] = item; + } + } + }, + + updateObject: function updateObject(object, newObject) { + for (var key in newObject) { + if (newObject.hasOwnProperty(key)) { + object[key] = newObject[key]; + } + } + }, + + cleanObject: function cleanObject(object) { + for (var key in object) { + if (object.hasOwnProperty(key)) { + delete object[key]; + } + } + }, + + pushAll: function pushAll(array, arrayToPush) { + Array.prototype.push.apply(array, arrayToPush); + }, + + indexOf: function indexOf(array, property, value) { + for (var i = 0; i < array.length; i += 1) { + if (array[i][property] === value) { + return i; + } + } + return -1; + }, + + createInternal: function createInternal(data, array, map, action) { + var added = this.mapToArray(data, action) + this.addToMap(added, map); + this.pushAll(array, added); + return added; + }, + + updateInternal: function updateInternal(data, map, action) { + var updated = this.mapToArray(data, action) + var result = [] + for (var index = 0; index < updated.length; index++) { + var item = updated[index]; + this.updateObject(map[item.id], item) + result.push(map[item.id]) + } + return result; + }, + + removeInternal: function removeInternal(id, array, map) { + var old = map[id]; + delete map[old.id]; + array.splice(array.indexOf(old), 1); + return old; + }, + + arrayToTitleMap: function arrayToTitleMap(array) { + return array.map(function (item) { + return { value: item.id, name: item.name } + }).sort(function (itemA, itemB) { + return itemA.name.localeCompare(itemB.name); + }) + }, + + displayErrorFunction: function displayErrorFunction(message) { + return function() { + toast.add('error', gettext(message)); + } + }, + + displaySuccess: function displaySuccess(message) { + toast.add('success', gettext(message)); + }, + + displayError: function displayError(message) { + toast.add('error', gettext(message)); + }, + + } + + } +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/js/util.service.spec.js b/moon_dashboard/moon/static/moon/js/util.service.spec.js new file mode 100755 index 00000000..d8e3ed31 --- /dev/null +++ b/moon_dashboard/moon/static/moon/js/util.service.spec.js @@ -0,0 +1,86 @@ +(function () { + 'use strict'; + + describe('moon.util.service', function () { + var service; + + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.framework')); + beforeEach(module('moon')); + + beforeEach(inject(function ($injector) { + service = $injector.get('moon.util.service'); + })); + + it('should push all', function () { + var a1 = [0, 1, 2]; + var a2 = [3, 4]; + service.pushAll(a1, a2) + + expect(a1.length).toBe(5); + expect(a1).toEqual([0, 1, 2, 3, 4]); + }); + + it('should index of', function () { + var a = [{ name: 'n0' }, { name: 'n1' }, { name: 'n2' }]; + var result = service.indexOf(a, 'name', 'n1'); + + expect(result).toBe(1); + }); + + it('should map to array', function () { + var map = { "a": { name: "a" }, "b": { name: "b" } }; + var result = service.mapToArray(map); + + expect(result.length).toBe(2); + }); + + it('should map ID to item', function () { + var map = { "a": { name: "a" }, "b": { name: "b" } }; + var array = ["a", "b"]; + service.mapIdToItem(array, map); + + expect(array.length).toBe(2); + expect(array[0].name).toBe("a"); + expect(array[1].name).toBe("b"); + }); + + it('should map item to ID', function () { + var array = [{ id: "a" }, { id: "b" }]; + service.mapItemToId(array); + + expect(array.length).toBe(2); + expect(array[0]).toBe("a"); + expect(array[1]).toBe("b"); + }); + + it('should add to map', function () { + var map = { "a": { name: "a" }, "b": { name: "b" } }; + var array = [{ id: "c" }]; + service.addToMap(array, map); + + expect(map.c).toEqual({ id: "c" }); + }); + + it('should update object', function () { + var object = { a: 1, b: "test" }; + var update = { a: 2, c: "test2" }; + service.updateObject(object, update); + + expect(object.a).toBe(2); + expect(object.b).toBe("test"); + expect(object.c).toBe("test2"); + }); + + it('should clean object', function () { + var object = { a: 1, b: "test" }; + service.cleanObject(object); + + expect(object.a).not.toBeDefined(); + expect(object.b).not.toBeDefined(); + expect(object).toEqual({}); + }); + }); + + +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/model/model.controller.js b/moon_dashboard/moon/static/moon/model/model.controller.js new file mode 100644 index 00000000..d6a7503b --- /dev/null +++ b/moon_dashboard/moon/static/moon/model/model.controller.js @@ -0,0 +1,244 @@ +(function () { + 'use strict'; + + angular + .module('moon') + .directive('onReadFile', directive) + .controller('moon.model.controller', controller); + + controller.$inject = ['moon.util.service', 'moon.model.service', 'moon.import.service', 'horizon.framework.widgets.form.ModalFormService']; + + directive.$inject = ['$parse']; + + function directive($parse) { + return { + restrict: 'A', + scope: false, + link: function (scope, element, attrs) { + element.bind('change', function (e) { + + var onFileReadFn = $parse(attrs.onReadFile); + var reader = new FileReader(); + + reader.onload = function () { + var fileContents = reader.result; + scope.$apply(function () { + onFileReadFn(scope, { + 'contents': fileContents + }); + }); + }; + reader.readAsText(element[0].files[0]); + }); + } + }; + } + + var categoryMap = { + 'subject': { + addTitle: 'Add Subject Category', + removeTitleFromMetaRule: 'Are you sure to remove from meta rule this Subject Category?', + removeTitle: 'Are you sure to remove this Subject Category?', + listName: 'subject_categories', + serviceListName: 'subjectCategories' + }, + 'object': { + addTitle: 'Add Object Category', + removeTitleFromMetaRule: 'Are you sure to remove from meta rule this Object Category?', + removeTitle: 'Are you sure to remove this Object Category?', + listName: 'object_categories', + serviceListName: 'objectCategories' + }, + 'action': { + addTitle: 'Add Action Category', + removeTitleFromMetaRule: 'Are you sure to remove from meta rule this Action Category?', + removeTitle: 'Are you sure to remove this Action Category?', + listName: 'action_categories', + serviceListName: 'actionCategories' + }, + } + + function controller(util, modelService, importService, ModalFormService) { + var self = this; + self.model = modelService; + self.showOrphan = false; + modelService.initialize(); + + self.importData = function importData(text) { + importService.importData(JSON.parse(text)).then(function () { + modelService.initialize(); + }) + } + + self.createModel = function createModel() { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") } + } + }; + var model = { name: '', description: '' }; + var config = { + title: gettext('Create Model'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }], + model: model + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + modelService.createModel(form.model); + } + } + + self.updateModel = function updateModel(model) { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") } + } + }; + var config = { + title: gettext('Update Model'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }], + model: angular.copy(model) + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + modelService.updateModel(form.model); + } + } + + self.removeModel = function removeModel(model) { + if (confirm(gettext('Are you sure to delete this Model?'))) + modelService.removeModel(model); + } + + self.addMetaRule = function addMetaRule(model) { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") }, + id: { type: "string", title: gettext("Select a Meta Rule:") } + } + }; + var metaRule = { name: '', description: '' }; + var titleMap = util.arrayToTitleMap(modelService.metaRules) + var config = { + title: gettext('Add Meta Rule'), + schema: schema, + form: [{ key: 'id', type: 'select', titleMap: titleMap }, { type: 'help', helpvalue: gettext("Or create a new one:") }, 'name', { key: 'description', type: 'textarea' }], + model: metaRule + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + function addMetaRuleToModel(metaRule) { + var modelCopy = angular.copy(model); + modelCopy.meta_rules.push(metaRule); + modelService.updateModel(modelCopy); + } + + if (form.model.name) { + modelService.createMetaRule(form.model).then(addMetaRuleToModel) + } else if (form.model.id) { + addMetaRuleToModel(modelService.getMetaRule(form.model.id)); + } + } + } + + self.updateMetaRule = function updateMetaRule(metaRule) { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") } + } + }; + var metaRuleCopy = angular.copy(metaRule); + var config = { + title: gettext('Update Meta Rule'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }], + model: metaRuleCopy + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + modelService.updateMetaRule(form.model); + } + } + + self.removeMetaRuleFromModel = function removeMetaRuleFromModel(model, metaRule) { + if (confirm(gettext('Are you sure to remove this Meta Rule from model?'))) { + var modelCopy = angular.copy(model); + modelCopy.meta_rules.splice(model.meta_rules.indexOf(metaRule), 1); + modelService.updateModel(modelCopy); + } + } + + self.removeMetaRule = function removeMetaRule(metaRule) { + if (confirm(gettext('Are you sure to remove this Meta Rule?'))) { + modelService.removeMetaRule(metaRule); + } + } + + self.addCategory = function addCategory(type, metaRule) { + var typeValue = categoryMap[type]; + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") }, + id: { type: "string", title: gettext("Select a Category:") } + } + }; + var category = { name: '', description: '' }; + var titleMap = util.arrayToTitleMap(modelService[typeValue.serviceListName]) + var config = { + title: gettext(typeValue.addTitle), + schema: schema, + form: [{ key: 'id', type: 'select', titleMap: titleMap }, { type: 'help', helpvalue: gettext("Or create a new one:") }, 'name', { key: 'description', type: 'textarea' }], + model: category + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + function addCategoryToMetaRule(category) { + var metaRuleCopy = angular.copy(metaRule); + metaRuleCopy[typeValue.listName].push(category); + modelService.updateMetaRule(metaRuleCopy) + } + + if (form.model.name) { + modelService.createCategory(type, form.model).then(addCategoryToMetaRule) + } else if (form.model.id) { + addCategoryToMetaRule(modelService.getCategory(type, form.model.id)); + } + } + } + + self.removeCategoryFromMetaRule = function removeCategoryFromMetaRule(type, metaRule, category) { + var typeValue = categoryMap[type]; + if (confirm(gettext(typeValue.removeTitleFromMetaRule))) { + var metaRuleCopy = angular.copy(metaRule); + metaRuleCopy[typeValue.listName].splice(metaRule[typeValue.listName].indexOf(category), 1); + modelService.updateMetaRule(metaRuleCopy); + } + } + + self.removeCategory = function removeCategory(type, category) { + var typeValue = categoryMap[type]; + if (confirm(gettext(typeValue.removeTitle))) { + modelService.removeCategory(type, category); + } + } + + + } +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/model/model.html b/moon_dashboard/moon/static/moon/model/model.html new file mode 100644 index 00000000..98d64c75 --- /dev/null +++ b/moon_dashboard/moon/static/moon/model/model.html @@ -0,0 +1,143 @@ +
+
+ +

Warning!

+

+ Some metarules or categories are orphan, please check them and delete them if necessary. + Show orphans + Hide orphans +

+
+ +
+
+

Orphan Meta rules

+
+

{$ metaRule.name $}

+ +

{$ metaRule.description $}

+
+
+ +
+

Orphan Subject categories

+
+

{$ subject.name $}

+ +

{$ subject.description $}

+
+
+ +
+

Orphan Object categories

+
+

{$ object.name $}

+ +

{$ object.description $}

+
+
+ +
+

Orphan Action categories

+
+

{$ action.name $}

+ +

{$ action.description $}

+
+
+
+ +
+
+ + + + + +
+
+ + +
+
+

{$ model.name $}

+
+ + +
+

{$ model.description $}

+
+ +

{$ model.meta_rules.length $} + meta rule(s) +

+ +
+
+
+

{$ metaRule.name $}

+
+ + +
+

{$ metaRule.description $}

+

+ + + + + + + + + + + + + + + +
+ Subjects + + + Objects + + + Actions + +
+

+ {$ category.name $} + +

+
+

+ {$ category.name $} + +

+
+

+ {$ category.name $} + +

+
+

+
+
+
+
+
+
\ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/model/model.service.js b/moon_dashboard/moon/static/moon/model/model.service.js new file mode 100755 index 00000000..76c3da01 --- /dev/null +++ b/moon_dashboard/moon/static/moon/model/model.service.js @@ -0,0 +1,286 @@ +(function () { + + 'use strict'; + + angular + .module('moon') + .factory('moon.model.service', modelService); + + modelService.$inject = ['moon.util.service', '$resource', 'moon.URI', '$q']; + + function modelService(util, $resource, URI, $q) { + var host = URI.API; + var modelResource = $resource(host + '/models/' + ':id', {}, { + get: { method: 'GET' }, + query: { method: 'GET' }, + create: { method: 'POST' }, + remove: { method: 'DELETE' }, + update: { method: 'PATCH' } + }); + + var metaRuleResource = $resource(host + '/meta_rules/' + ':id', {}, { + query: { method: 'GET' }, + get: { method: 'GET' }, + update: { method: 'PATCH' }, + create: { method: 'POST' }, + remove: { method: 'DELETE' } + }); + + var subjectCategoryResource = $resource(host + '/subject_categories/' + ':id', {}, { + query: { method: 'GET' }, + get: { method: 'GET' }, + create: { method: 'POST' }, + remove: { method: 'DELETE' } + }); + + var objectCategoryResource = $resource(host + '/object_categories/' + ':id', {}, { + query: { method: 'GET' }, + get: { method: 'GET' }, + create: { method: 'POST' }, + remove: { method: 'DELETE' } + }); + + var actionCategoryResource = $resource(host + '/action_categories/' + ':id', {}, { + query: { method: 'GET' }, + get: { method: 'GET' }, + create: { method: 'POST' }, + remove: { method: 'DELETE' } + }); + + var modelsMap = {}; + var metaRulesMap = {}; + var subjectCategoriesMap = {}; + var objectCategoriesMap = {}; + var actionCategoriesMap = {}; + var models = []; + var metaRules = []; + var orphanMetaRules = []; + var subjectCategories = []; + var objectCategories = []; + var actionCategories = []; + var orphanSubjectCategories = []; + var orphanObjectCategories = []; + var orphanActionCategories = []; + + var categoryMap = { + 'subject': { + resource: subjectCategoryResource, + map: subjectCategoriesMap, + list: subjectCategories, + listName: 'subject_categories' + }, + 'object': { + resource: objectCategoryResource, + map: objectCategoriesMap, + list: objectCategories, + listName: 'object_categories' + }, + 'action': { + resource: actionCategoryResource, + map: actionCategoriesMap, + list: actionCategories, + listName: 'action_categories' + } + } + + function loadModels() { + var queries = { + subjectCategories: subjectCategoryResource.query().$promise, + objectCategories: objectCategoryResource.query().$promise, + actionCategories: actionCategoryResource.query().$promise, + metaRules: metaRuleResource.query().$promise, + models: modelResource.query().$promise, + } + + var result = $q.all(queries).then(function (result) { + createModels(result.models, result.metaRules, result.subjectCategories, result.objectCategories, result.actionCategories) + console.log('moon', 'models initialized') + }) + + return result; + } + + function createModels(modelsData, metarulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData) { + util.cleanObject(modelsMap); + util.cleanObject(metaRulesMap); + util.cleanObject(subjectCategoriesMap); + util.cleanObject(objectCategoriesMap); + util.cleanObject(actionCategoriesMap); + models.splice(0, models.length); + metaRules.splice(0, metaRules.length); + subjectCategories.splice(0, subjectCategories.length); + objectCategories.splice(0, objectCategories.length); + actionCategories.splice(0, actionCategories.length); + if (subjectCategoriesData.subject_categories) createCategoryInternal('subject', subjectCategoriesData.subject_categories); + if (objectCategoriesData.object_categories) createCategoryInternal('object', objectCategoriesData.object_categories); + if (actionCategoriesData.action_categories) createCategoryInternal('action', actionCategoriesData.action_categories); + if (metarulesData.meta_rules) createMetaRuleInternal(metarulesData.meta_rules); + if (modelsData.models) createModelInternal(modelsData.models); + updateOrphan(); + } + + function mapModel(model) { + util.mapIdToItem(model.meta_rules, metaRulesMap); + } + + function createModelInternal(data) { + return util.createInternal(data, models, modelsMap, mapModel); + } + + function updateModelInternal(data) { + return util.updateInternal(data, modelsMap, mapModel); + } + + function removeModelInternal(id) { + return util.removeInternal(id, models, modelsMap); + } + + function mapMetaRule(metaRule) { + util.mapIdToItem(metaRule.subject_categories, subjectCategoriesMap); + util.mapIdToItem(metaRule.object_categories, objectCategoriesMap); + util.mapIdToItem(metaRule.action_categories, actionCategoriesMap); + } + + function createMetaRuleInternal(data) { + return util.createInternal(data, metaRules, metaRulesMap, mapMetaRule); + } + + function updateMetaRuleInternal(data) { + return util.updateInternal(data, metaRulesMap, mapMetaRule); + } + + function removeMetaRuleInternal(id) { + return util.removeInternal(id, metaRules, metaRulesMap); + } + + function createCategoryInternal(type, data) { + var categoryValue = categoryMap[type]; + return util.createInternal(data, categoryValue.list, categoryValue.map) + } + + function removeCategoryInternal(type, id) { + var categoryValue = categoryMap[type]; + return util.removeInternal(id, categoryValue.list, categoryValue.map); + } + + function updateOrphan() { + updateOrphanInternal(metaRules, orphanMetaRules, models, "meta_rules"); + updateOrphanInternal(subjectCategories, orphanSubjectCategories, metaRules, "subject_categories"); + updateOrphanInternal(objectCategories, orphanObjectCategories, metaRules, "object_categories"); + updateOrphanInternal(actionCategories, orphanActionCategories, metaRules, "action_categories"); + } + + function updateOrphanInternal(list, orphanList, parentList, childListName) { + orphanList.splice(0, orphanList.length); + util.pushAll(orphanList, list); + for (var i = 0; i < parentList.length; i++) { + var parent = parentList[i]; + var children = parent[childListName]; + if (children) { + for (var j = 0; j < children.length; j++) { + var child = children[j]; + var notOrphanIndex = util.indexOf(orphanList, "id", child.id); + if (notOrphanIndex >= 0) { + orphanList.splice(notOrphanIndex, 1); + } + } + } + } + } + + + return { + initialize: loadModels, + createModels: createModels, + models: models, + metaRules: metaRules, + orphanMetaRules: orphanMetaRules, + orphanSubjectCategories: orphanSubjectCategories, + orphanObjectCategories: orphanObjectCategories, + orphanActionCategories: orphanActionCategories, + subjectCategories: subjectCategories, + objectCategories: objectCategories, + actionCategories: actionCategories, + getModel: function getModel(id) { + return modelsMap[id]; + }, + createModel: function createModel(model) { + modelResource.create(null, model, success, util.displayErrorFunction('Unable to create model')); + + function success(data) { + createModelInternal(data.models); + util.displaySuccess('Model created'); + } + }, + removeModel: function removeModel(model) { + modelResource.remove({ id: model.id }, null, success, util.displayErrorFunction('Unable to remove model')); + + function success(data) { + removeModelInternal(model.id); + updateOrphan(); + util.displaySuccess('Model removed'); + } + }, + updateModel: function updateModel(model) { + util.mapItemToId(model.meta_rules) + modelResource.update({ id: model.id }, model, success, util.displayErrorFunction('Unable to update model')); + + function success(data) { + updateModelInternal(data.models) + updateOrphan(); + util.displaySuccess('Model updated'); + } + }, + getMetaRule: function getMetaRule(id) { + return metaRulesMap[id]; + }, + createMetaRule: function createMetaRule(metaRule) { + return metaRuleResource.create(null, metaRule).$promise.then(function (data) { + util.displaySuccess('Meta Rule created'); + return createMetaRuleInternal(data.meta_rules)[0]; + }, util.displayErrorFunction('Unable to create meta rule')) + }, + updateMetaRule: function updateMetaRule(metaRule) { + util.mapItemToId(metaRule.subject_categories); + util.mapItemToId(metaRule.object_categories); + util.mapItemToId(metaRule.action_categories); + metaRuleResource.update({ id: metaRule.id }, metaRule, success, util.displayErrorFunction('Unable to update meta rule')); + + function success(data) { + updateMetaRuleInternal(data.meta_rules); + updateOrphan(); + util.displaySuccess('Meta Rule updated'); + } + }, + removeMetaRule: function removeMetaRule(metaRule) { + metaRuleResource.remove({ id: metaRule.id }, null, success, util.displayErrorFunction('Unable to remove meta rule')); + + function success(data) { + removeMetaRuleInternal(metaRule.id); + updateOrphan(); + util.displaySuccess('Meta Rule removed'); + } + }, + getCategory: function getCategory(type, id) { + return categoryMap[type].map[id]; + }, + createCategory: function createCategory(type, category) { + var categoryValue = categoryMap[type]; + return categoryValue.resource.create({}, category).$promise.then(function (data) { + util.displaySuccess('Category created'); + return createCategoryInternal(type, data[categoryValue.listName])[0]; + }, util.displayErrorFunction('Unable to create category')) + }, + removeCategory: function removeCategory(type, category) { + var categoryValue = categoryMap[type]; + categoryValue.resource.remove({ id: category.id }, null, success, util.displayErrorFunction('Unable to remove category')); + + function success(data) { + removeCategoryInternal(type, category.id); + updateOrphan(); + util.displaySuccess('Category removed'); + } + }, + } + } +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/model/model.service.spec.js b/moon_dashboard/moon/static/moon/model/model.service.spec.js new file mode 100755 index 00000000..04d47793 --- /dev/null +++ b/moon_dashboard/moon/static/moon/model/model.service.spec.js @@ -0,0 +1,288 @@ +(function () { + 'use strict'; + + describe('moon.model.service', function () { + var service, $httpBackend, URI; + var modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData; + + function initData() { + modelsData = { + models: + { 'modelId1': { name: 'model1', description: 'mDescription1', meta_rules: ['metaRuleId1'] } } + }; + + subjectCategoriesData = { + subject_categories: + { + 'subjectCategoryId1': { name: 'subjectCategory1', description: 'scDescription1' }, + 'subjectCategoryId2': { name: 'subjectCategory2', description: 'scDescription2' } + }, + }; + objectCategoriesData = { + object_categories: + { + 'objectCategoryId1': { name: 'objectCategory1', description: 'ocDescription1' }, + 'objectCategoryId2': { name: 'objectCategory2', description: 'ocDescription2' } + } + }; + actionCategoriesData = { + action_categories: + { + 'actionCategoryId1': { name: 'actionCategory1', description: 'acDescription1' }, + 'actionCategoryId2': { name: 'actionCategory2', description: 'acDescription2' } + } + }; + metaRulesData = { + meta_rules: + { + 'metaRuleId1': { name: 'metaRule1', description: 'mrDescription1', subject_categories: ['subjectCategoryId1'], object_categories: ['objectCategoryId1'], action_categories: ['actionCategoryId1'] }, + 'metaRuleId2': { name: 'metaRule2', description: 'mrDescription2', subject_categories: [], object_categories: [], action_categories: [] } + } + }; + } + + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.framework')); + beforeEach(module('moon')); + + beforeEach(inject(function ($injector) { + service = $injector.get('moon.model.service'); + $httpBackend = $injector.get('$httpBackend'); + URI = $injector.get('moon.URI'); + })); + + afterEach(function () { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should initialize', function () { + initData(); + $httpBackend.expectGET(URI.API + '/subject_categories').respond(200, subjectCategoriesData); + $httpBackend.expectGET(URI.API + '/object_categories').respond(200, objectCategoriesData); + $httpBackend.expectGET(URI.API + '/action_categories').respond(200, actionCategoriesData); + $httpBackend.expectGET(URI.API + '/meta_rules').respond(200, metaRulesData); + $httpBackend.expectGET(URI.API + '/models').respond(200, modelsData); + + service.initialize(); + $httpBackend.flush(); + + expect(service.models.length).toBe(1); + var model = service.models[0]; + expect(model.id).toBe('modelId1'); + expect(model.name).toBe('model1'); + expect(model.description).toBe('mDescription1'); + + expect(service.metaRules.length).toBe(2); + expect(model.meta_rules.length).toBe(1); + var metaRule = model.meta_rules[0]; + expect(metaRule.id).toBe('metaRuleId1'); + expect(metaRule.name).toBe('metaRule1'); + expect(metaRule.description).toBe('mrDescription1'); + + expect(service.subjectCategories.length).toBe(2); + expect(metaRule.subject_categories.length).toBe(1); + var subjectCategory = metaRule.subject_categories[0]; + expect(subjectCategory.id).toBe('subjectCategoryId1'); + expect(subjectCategory.name).toBe('subjectCategory1'); + expect(subjectCategory.description).toBe('scDescription1'); + + expect(service.objectCategories.length).toBe(2); + expect(metaRule.object_categories.length).toBe(1); + var objectCategory = metaRule.object_categories[0]; + expect(objectCategory.id).toBe('objectCategoryId1'); + expect(objectCategory.name).toBe('objectCategory1'); + expect(objectCategory.description).toBe('ocDescription1'); + + expect(service.actionCategories.length).toBe(2); + expect(metaRule.action_categories.length).toBe(1); + var actionCategory = metaRule.action_categories[0]; + expect(actionCategory.id).toBe('actionCategoryId1'); + expect(actionCategory.name).toBe('actionCategory1'); + expect(actionCategory.description).toBe('acDescription1'); + + expect(service.orphanMetaRules.length).toBe(1); + metaRule = service.orphanMetaRules[0]; + expect(metaRule.id).toBe('metaRuleId2'); + expect(metaRule.name).toBe('metaRule2'); + expect(metaRule.description).toBe('mrDescription2'); + + expect(service.orphanSubjectCategories.length).toBe(1); + subjectCategory = service.orphanSubjectCategories[0]; + expect(subjectCategory.id).toBe('subjectCategoryId2'); + expect(subjectCategory.name).toBe('subjectCategory2'); + expect(subjectCategory.description).toBe('scDescription2'); + + expect(service.orphanObjectCategories.length).toBe(1); + objectCategory = service.orphanObjectCategories[0]; + expect(objectCategory.id).toBe('objectCategoryId2'); + expect(objectCategory.name).toBe('objectCategory2'); + expect(objectCategory.description).toBe('ocDescription2'); + + expect(service.orphanActionCategories.length).toBe(1); + actionCategory = service.orphanActionCategories[0]; + expect(actionCategory.id).toBe('actionCategoryId2'); + expect(actionCategory.name).toBe('actionCategory2'); + expect(actionCategory.description).toBe('acDescription2'); + + }); + + + + it('should create model', function () { + var modelCreatedData = { + models: + { 'modelId1': { name: 'model1', description: 'mDescription1', meta_rules: [] } } + }; + + $httpBackend.expectPOST(URI.API + '/models').respond(200, modelCreatedData); + + service.createModel({ name: 'model1', description: 'mDescription1' }); + $httpBackend.flush(); + + expect(service.models.length).toBe(1); + var model = service.models[0]; + expect(model.id).toBe('modelId1'); + expect(model.name).toBe('model1'); + expect(model.description).toBe('mDescription1'); + }); + + it('should remove model', function () { + initData(); + service.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + + $httpBackend.expectDELETE(URI.API + '/models/modelId1').respond(200); + + service.removeModel({ id: 'modelId1' }); + $httpBackend.flush(); + + expect(service.models.length).toBe(0); + + expect(service.orphanMetaRules.length).toBe(2); + }); + + it('should update model', function () { + initData(); + var modelUpdatedData = { + models: + { 'modelId1': { name: 'model2', description: 'mDescription2', meta_rules: ['metaRuleId2'] } } + }; + service.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + + $httpBackend.expectPATCH(URI.API + '/models/modelId1').respond(200, modelUpdatedData); + + service.updateModel({ id: 'modelId1', name: 'model2', description: 'mDescription2', meta_rules: service.getMetaRule('metaRuleId2') }); + $httpBackend.flush(); + + expect(service.models.length).toBe(1); + var model = service.models[0]; + expect(model.id).toBe('modelId1'); + expect(model.name).toBe('model2'); + expect(model.description).toBe('mDescription2'); + + expect(model.meta_rules.length).toBe(1); + var metaRule = model.meta_rules[0]; + expect(metaRule.id).toBe('metaRuleId2'); + + expect(service.orphanMetaRules.length).toBe(1); + metaRule = service.orphanMetaRules[0]; + expect(metaRule.id).toBe('metaRuleId1'); + }); + + it('should create meta rule', function () { + var metaRuleCreatedData = { + meta_rules: + { 'metaRuleId1': { name: 'metaRule1', description: 'mrDescription1' } } + }; + + $httpBackend.expectPOST(URI.API + '/meta_rules').respond(200, metaRuleCreatedData); + + service.createMetaRule({ name: 'metaRule1', description: 'mrDescription1' }); + $httpBackend.flush(); + + expect(service.metaRules.length).toBe(1); + var metaRule = service.metaRules[0]; + expect(metaRule.id).toBe('metaRuleId1'); + expect(metaRule.name).toBe('metaRule1'); + expect(metaRule.description).toBe('mrDescription1'); + }); + + it('should update meta rule', function () { + initData(); + var metaRuleUpdatedData = { + meta_rules: + { 'metaRuleId1': { name: 'metaRule2', description: 'mrDescription2', subject_categories: ['subjectCategoryId2'], object_categories: ['objectCategoryId2'], action_categories: ['actionCategoryId2'] } } + }; + service.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + + $httpBackend.expectPATCH(URI.API + '/meta_rules/metaRuleId1').respond(200, metaRuleUpdatedData); + + service.updateMetaRule({ id: 'metaRuleId1', name: 'metaRule2', description: 'mrDescription2', subject_categories: [service.getCategory('subject', 'subjectCategoryId2')], object_categories: [service.getCategory('object', 'objectCategoryId2')], action_categories: [service.getCategory('action','actionCategoryId2')] }); + $httpBackend.flush(); + + var metaRule = service.getMetaRule('metaRuleId1'); + expect(metaRule.id).toBe('metaRuleId1'); + expect(metaRule.name).toBe('metaRule2'); + expect(metaRule.description).toBe('mrDescription2'); + + expect(service.orphanSubjectCategories.length).toBe(1); + var subjectCategory = service.orphanSubjectCategories[0]; + expect(subjectCategory.id).toBe('subjectCategoryId1'); + + expect(service.orphanObjectCategories.length).toBe(1); + var objectCategory = service.orphanObjectCategories[0]; + expect(objectCategory.id).toBe('objectCategoryId1'); + + expect(service.orphanActionCategories.length).toBe(1); + var actionCategory = service.orphanActionCategories[0]; + expect(actionCategory.id).toBe('actionCategoryId1'); + }); + + it('should remove meta rule', function () { + initData(); + service.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + + $httpBackend.expectDELETE(URI.API + '/meta_rules/metaRuleId2').respond(200); + + service.removeMetaRule(service.getMetaRule('metaRuleId2')); + $httpBackend.flush(); + + expect(service.metaRules.length).toBe(1); + expect(service.orphanMetaRules.length).toBe(0); + }); + + it('should create category', function () { + var categoryCreatedData = { + subject_categories: + { 'subjectCategoryId1': { name: 'subjectCategory1', description: 'scDescription1' } } + }; + + $httpBackend.expectPOST(URI.API + '/subject_categories').respond(200, categoryCreatedData); + + service.createCategory('subject', { name: 'subjectCategory1', description: 'scDescription1' }); + $httpBackend.flush(); + + expect(service.subjectCategories.length).toBe(1); + var subjectCategory = service.subjectCategories[0]; + expect(subjectCategory.id).toBe('subjectCategoryId1'); + expect(subjectCategory.name).toBe('subjectCategory1'); + expect(subjectCategory.description).toBe('scDescription1'); + }); + + it('should remove category', function () { + initData(); + service.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + + $httpBackend.expectDELETE(URI.API + '/subject_categories/subjectCategoryId2').respond(200); + + service.removeCategory('subject', service.getCategory('subject', 'subjectCategoryId2')); + $httpBackend.flush(); + + expect(service.subjectCategories.length).toBe(1); + expect(service.orphanSubjectCategories.length).toBe(0); + }); + + }); + + +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/pdp/pdp.controller.js b/moon_dashboard/moon/static/moon/pdp/pdp.controller.js new file mode 100644 index 00000000..c57f3b28 --- /dev/null +++ b/moon_dashboard/moon/static/moon/pdp/pdp.controller.js @@ -0,0 +1,121 @@ +(function () { + 'use strict'; + + angular + .module('moon') + .controller('moon.pdp.controller', + controller); + + controller.$inject = ['moon.util.service', 'moon.pdp.service', 'horizon.framework.widgets.form.ModalFormService']; + + function controller(util, pdpService, ModalFormService) { + var self = this; + self.model = pdpService; + pdpService.initialize(); + + self.createPdp = function createPdp() { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") } + } + }; + var pdp = { name: '', description: '' }; + var config = { + title: gettext('Create PDP'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }], + model: pdp + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + pdpService.createPdp(form.model); + } + } + + self.updatePdp = function updatePdp(pdp) { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") } + } + }; + var config = { + title: gettext('Update PDP'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }], + model: angular.copy(pdp) + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + pdpService.updatePdp(form.model); + } + } + + self.removePdp = function removePdp(pdp) { + if (confirm(gettext('Are you sure to delete this PDP?'))) + pdpService.removePdp(pdp); + } + + self.addPolicy = function addPolicy(pdp) { + var schema = { + type: "object", + properties: { + id: { type: "string", title: gettext("Select a Policy:") } + } + }; + var titleMap = util.arrayToTitleMap(pdpService.policies) + var config = { + title: gettext('Add Policy'), + schema: schema, + form: [{ key: 'id', type: 'select', titleMap: titleMap }], + model: {} + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + var pdpCopy = angular.copy(pdp); + pdpCopy.security_pipeline.push(pdpService.getPolicy(form.model.id)); + pdpService.updatePdp(pdpCopy); + } + } + + self.removePolicyFromPdp = function removePolicyFromPdp(pdp, policy) { + if (confirm(gettext('Are you sure to remove this Policy from PDP?'))) { + var pdpCopy = angular.copy(pdp); + pdpCopy.security_pipeline.splice(pdp.security_pipeline.indexOf(policy), 1); + pdpService.updatePdp(pdpCopy); + } + } + + self.changeProject = function changeProject(pdp) { + var schema = { + type: "object", + properties: { + id: { type: "string", title: gettext("Select a Project:") } + } + }; + var model = {id : pdp.keystone_project_id}; + + var titleMap = util.arrayToTitleMap(pdpService.projects) + var config = { + title: gettext('Change Project'), + schema: schema, + form: [{ key: 'id', type: 'select', titleMap: titleMap }], + model: model + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + var pdpCopy = angular.copy(pdp); + pdpCopy.project = pdpService.getProject(form.model.id); + pdpService.updatePdp(pdpCopy); + } + } + + } +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/pdp/pdp.html b/moon_dashboard/moon/static/moon/pdp/pdp.html new file mode 100644 index 00000000..2456a261 --- /dev/null +++ b/moon_dashboard/moon/static/moon/pdp/pdp.html @@ -0,0 +1,41 @@ +
+
+
+ + +
+
+
+
+

{$ pdp.name $}

+
+ + +
+

{$ pdp.description $}

+

+ Project: {$ pdp.project ? pdp.project.name : 'none' $} + +

+ +
+ +

{$ pdp.security_pipeline.length $} + policy(ies) +

+ +
+
+
+

{$ policy.name $}

+ +

{$ policy.description $}

+
+
+
+
+
+
\ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/pdp/pdp.service.js b/moon_dashboard/moon/static/moon/pdp/pdp.service.js new file mode 100755 index 00000000..e18971be --- /dev/null +++ b/moon_dashboard/moon/static/moon/pdp/pdp.service.js @@ -0,0 +1,123 @@ +(function () { + + 'use strict'; + + angular + .module('moon') + .factory('moon.pdp.service', pdpService); + + pdpService.$inject = ['moon.util.service', '$resource', 'moon.URI', '$q', 'horizon.app.core.openstack-service-api.keystone']; + + function pdpService(util, $resource, URI, $q, keystone) { + var host = URI.API; + + var pdpResource = $resource(host + '/pdp/' + ':id', {}, { + get: { method: 'GET' }, + query: { method: 'GET' }, + create: { method: 'POST' }, + remove: { method: 'DELETE' }, + update: { method: 'PATCH' } + }); + + var policyResource = $resource(host + '/policies/' + ':id', {}, { + query: { method: 'GET' }, + }); + + var pdpsMap = {}; + var pdps = []; + var policiesMap = {}; + var policies = []; + var projectsMap = {}; + var projects = []; + + function loadPdps() { + var queries = { + pdps: pdpResource.query().$promise, + policies: policyResource.query().$promise, + projects: keystone.getProjects() + } + + $q.all(queries).then(function (result) { + createPdps(result.pdps, result.policies, result.projects.data) + console.log('moon', 'pdps initialized', pdps) + }) + } + + function createPdps(pdpsData, policiesData, projectsData) { + pdps.splice(0, pdps.length); + policies.splice(0, policies.length); + projects.splice(0, projects.length); + util.cleanObject(pdpsMap); + util.cleanObject(policiesMap); + util.cleanObject(projectsMap) + + util.createInternal(policiesData.policies, policies, policiesMap); + util.pushAll(projects, projectsData.items); + util.addToMap(projects, projectsMap); + createPdpInternal(pdpsData.pdps); + } + + function mapPdp(pdp) { + util.mapIdToItem(pdp.security_pipeline, policiesMap); + pdp.project = null; + if (pdp.keystone_project_id) { + pdp.project = projectsMap[pdp.keystone_project_id]; + } + } + + function createPdpInternal(data) { + return util.createInternal(data, pdps, pdpsMap, mapPdp); + } + + function updatePdpInternal(data) { + return util.updateInternal(data, pdpsMap, mapPdp); + } + + function removePdpInternal(id) { + return util.removeInternal(id, pdps, pdpsMap); + } + + return { + initialize: loadPdps, + createPdps: createPdps, + pdps: pdps, + policies: policies, + projects: projects, + createPdp: function createPdp(pdp) { + pdp.keystone_project_id = null; + pdp.security_pipeline = []; + pdpResource.create(null, pdp, success, util.displayErrorFunction('Unable to create PDP')); + + function success(data) { + createPdpInternal(data.pdps); + util.displaySuccess('PDP created'); + } + }, + removePdp: function removePdp(pdp) { + pdpResource.remove({ id: pdp.id }, null, success, util.displayErrorFunction('Unable to remove PDP')); + + function success(data) { + removePdpInternal(pdp.id); + util.displaySuccess('PDP removed'); + } + }, + updatePdp: function updatePdp(pdp) { + util.mapItemToId(pdp.security_pipeline); + pdp.keystone_project_id = pdp.project ? pdp.project.id : null; + pdpResource.update({ id: pdp.id }, pdp, success, util.displayErrorFunction('Unable to update PDP')); + + function success(data) { + updatePdpInternal(data.pdps) + util.displaySuccess('PDP updated'); + } + }, + getPolicy: function getPolicy(id) { + return policiesMap[id]; + }, + getProject: function getProject(id) { + return projectsMap[id]; + }, + } + + } +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/pdp/pdp.service.spec.js b/moon_dashboard/moon/static/moon/pdp/pdp.service.spec.js new file mode 100755 index 00000000..4208467f --- /dev/null +++ b/moon_dashboard/moon/static/moon/pdp/pdp.service.spec.js @@ -0,0 +1,143 @@ +(function () { + 'use strict'; + + describe('moon.pdp.service', function () { + var service, $httpBackend, URI; + var pdpsData, policiesData, projectsData; + + + function initData() { + pdpsData = { + pdps: + { 'pdpId1': { name: 'pdp1', description: 'pdpDescription1', security_pipeline: ['policyId1'], keystone_project_id: 'projectId1' } } + }; + + policiesData = { + policies: + { + 'policyId1': { name: 'policy1', description: 'pDescription1' }, + 'policyId2': { name: 'policy2', description: 'pDescription2' } + } + }; + + projectsData = { + items: [ + { name: "project1", id: "projectId1" }, + { name: "project2", id: "projectId2" } + ] + }; + + } + + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.framework')); + beforeEach(module('moon')); + + beforeEach(inject(function ($injector) { + service = $injector.get('moon.pdp.service'); + $httpBackend = $injector.get('$httpBackend'); + URI = $injector.get('moon.URI'); + })); + + afterEach(function () { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should initialize', function () { + initData(); + $httpBackend.expectGET(URI.API + '/pdp').respond(200, pdpsData); + $httpBackend.expectGET(URI.API + '/policies').respond(200, policiesData); + $httpBackend.expectGET('/api/keystone/projects/').respond(200, projectsData); + + + service.initialize(); + $httpBackend.flush(); + + expect(service.pdps.length).toBe(1); + var pdp = service.pdps[0]; + expect(pdp.id).toBe('pdpId1'); + expect(pdp.name).toBe('pdp1'); + expect(pdp.description).toBe('pdpDescription1'); + expect(pdp.security_pipeline.length).toBe(1); + expect(pdp.security_pipeline[0].id).toBe('policyId1'); + expect(pdp.keystone_project_id).toBe('projectId1'); + expect(pdp.project.id).toBe('projectId1'); + + expect(service.policies.length).toBe(2); + var policy = service.policies[0]; + expect(policy.id).toBe('policyId1'); + expect(policy.name).toBe('policy1'); + expect(policy.description).toBe('pDescription1'); + + + expect(service.projects.length).toBe(2); + var project = service.projects[0]; + expect(project.id).toBe('projectId1'); + expect(project.name).toBe('project1'); + + }); + + + + it('should create pdp', function () { + var pdpCreatedData = { + pdps: + { 'pdpId1': { name: 'pdp1', description: 'pdpDescription1', security_pipeline: [], keystone_project_id: null } } + }; + + $httpBackend.expectPOST(URI.API + '/pdp').respond(200, pdpCreatedData); + + service.createPdp({ name: 'pdp1', description: 'pdpDescription1' }); + $httpBackend.flush(); + + expect(service.pdps.length).toBe(1); + var pdp = service.pdps[0]; + expect(pdp.id).toBe('pdpId1'); + expect(pdp.name).toBe('pdp1'); + expect(pdp.description).toBe('pdpDescription1'); + expect(pdp.project).toBe(null); + expect(pdp.security_pipeline.length).toBe(0); + }); + + it('should remove pdp', function () { + initData(); + service.createPdps(pdpsData, policiesData, projectsData); + + $httpBackend.expectDELETE(URI.API + '/pdp/pdpId1').respond(200); + + service.removePdp({ id: 'pdpId1' }); + $httpBackend.flush(); + + expect(service.pdps.length).toBe(0); + }); + + it('should update pdp', function () { + initData(); + var pdpUpdatedData = { + pdps: + { 'pdpId1': { name: 'pdp2', description: 'pdpDescription2', security_pipeline: ['policyId2'], keystone_project_id: 'projectId2' } } + }; + service.createPdps(pdpsData, policiesData, projectsData); + + $httpBackend.expectPATCH(URI.API + '/pdp/pdpId1').respond(200, pdpUpdatedData); + + service.updatePdp({ id: 'pdpId1', name: 'pdp2', description: 'pdpDescription2', security_pipeline: [service.getPolicy('policyId2')], project: service.getProject('projectId2') }); + $httpBackend.flush(); + + expect(service.pdps.length).toBe(1); + var pdp = service.pdps[0]; + expect(pdp.id).toBe('pdpId1'); + expect(pdp.name).toBe('pdp2'); + expect(pdp.description).toBe('pdpDescription2'); + expect(pdp.project.id).toBe('projectId2'); + expect(pdp.security_pipeline.length).toBe(1); + expect(pdp.security_pipeline[0].id).toBe('policyId2'); + + }); + + + }); + + +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/policy/policy.controller.js b/moon_dashboard/moon/static/moon/policy/policy.controller.js new file mode 100644 index 00000000..6c6631cf --- /dev/null +++ b/moon_dashboard/moon/static/moon/policy/policy.controller.js @@ -0,0 +1,295 @@ +(function () { + 'use strict'; + + angular + .module('moon') + .controller('moon.policy.controller', + controller); + + controller.$inject = ['moon.util.service', 'moon.policy.service', 'moon.model.service', 'horizon.framework.widgets.form.ModalFormService']; + + function controller(util, policyService, modelService, ModalFormService) { + var self = this; + var genres = [{ value: 'admin', name: gettext('admin') }, { value: 'authz', name: gettext('authz') }]; + self.model = policyService; + self.selectedRule = null; + self.currentData = null; + policyService.initialize(); + + var dataTitleMaps = {}; + + var categoryMap = { + subject: { + perimeterId: 'subject_id' + }, + object: { + perimeterId: 'object_id' + }, + action: { + perimeterId: 'action_id' + }, + } + + function createAddDataButton(type, index, category, config, policy) { + config.form.push({ + "key": type + index + "Button", + "type": "button", + "title": "Add", + onClick: createDataFunction(type, category, policy) + }) + } + + function createDataFunction(type, category, policy) { + return function () { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") }, + } + }; + var data = { name: '', description: '' }; + var config = { + title: gettext('Create Data of ' + category.name + ' category'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }], + model: data + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + policyService.createData(type, policy, category, form.model).then( + function (data) { + util.pushAll(dataTitleMaps[category.id], util.arrayToTitleMap(data)); + } + ); + } + } + } + + function getOrCreateDataTitleMap(category, data, policy) { + var result = dataTitleMaps[category.id]; + if (!result) { + result = util.arrayToTitleMap(data); + dataTitleMaps[category.id] = result; + } + return result; + } + + function createDataSelect(type, categories, data, config, policy) { + for (var i = 0; i < categories.length; i++) { + var category = categories[i]; + var titleMap = getOrCreateDataTitleMap(category, data, policy); + config.schema.properties[type + i] = { type: "string", title: gettext('Select ' + type + ' data of ' + category.name + ' category') }; + config.form.push({ key: type + i, type: 'select', titleMap: titleMap }); + createAddDataButton(type, i, category, config, policy); + } + } + + function pushData(type, model, array) { + var i = 0; + while ((type + i) in model) { + array.push(model[type + i]); + i++; + } + } + + self.createPolicy = function createPolicy() { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") }, + genre: { type: "string", title: gettext("genre") }, + model_id: { type: "string", title: gettext("Select a Model:") } + } + }; + var policy = { name: '', description: '', model_id: null, genre: '' }; + var titleMap = util.arrayToTitleMap(modelService.models) + var config = { + title: gettext('Create Policy'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }, { key: 'genre', type: 'select', titleMap: genres }, { key: 'model_id', type: 'select', titleMap: titleMap }], + model: policy + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + policyService.createPolicy(form.model); + } + } + + self.updatePolicy = function updatePolicy(policy) { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") }, + genre: { type: "string", title: gettext("Genre") }, + } + }; + var config = { + title: gettext('Update Policy'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }, { key: 'genre', type: 'select', titleMap: genres }], + model: { name: policy.name, description: policy.description, model_id: policy.model_id, id: policy.id, genre: policy.genre } + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + policyService.updatePolicy(form.model); + } + } + + self.addRuleWithMetaRule = function addRuleWithMetaRule(policy, metaRule) { + var schema = { + type: "object", + properties: { + instructions: { type: "string", title: gettext("Instructions") } + } + }; + + var config = { + title: gettext('Add Rule'), + schema: schema, + form: [], + model: { + instructions: '[{"decision": "grant"}]' + } + }; + dataTitleMaps = {}; + createDataSelect('subject', metaRule.subject_categories, policy.subjectData, config, policy); + createDataSelect('object', metaRule.object_categories, policy.objectData, config, policy); + createDataSelect('action', metaRule.action_categories, policy.actionData, config, policy); + config.form.push({ key: 'instructions', type: 'textarea' }) + + ModalFormService.open(config).then(submit); + + function submit(form) { + var rule = { enabled: true }; + rule.instructions = JSON.parse(form.model.instructions); + rule.meta_rule_id = metaRule.id; + rule.policy_id = policy.id; + rule.rule = []; + pushData('subject', form.model, rule.rule); + pushData('object', form.model, rule.rule); + pushData('action', form.model, rule.rule); + policyService.addRuleToPolicy(policy, rule); + } + } + + self.addRule = function addRule(policy) { + var schema = { + type: "object", + properties: { + metaRuleId: { type: "string", title: gettext("Select a Metarule:") } + } + }; + var rule = { metaRuleId: null }; + var titleMap = util.arrayToTitleMap(policy.model.meta_rules); + var config = { + title: gettext('Add Rule'), + schema: schema, + form: [{ key: 'metaRuleId', type: 'select', titleMap: titleMap }], + model: rule + }; + ModalFormService.open(config).then(submit); + + function submit(form) { + self.addRuleWithMetaRule(policy, modelService.getMetaRule(form.model.metaRuleId)); + } + } + + self.removePolicy = function removePolicy(policy) { + if (confirm(gettext('Are you sure to delete this Policy?'))) + policyService.removePolicy(policy); + } + + self.populatePolicy = function populatePolicy(policy) { + policyService.populatePolicy(policy); + } + + self.removeRuleFromPolicy = function removeRuleFromPolicy(policy, rule) { + if (confirm(gettext('Are you sure to delete this Rule?'))) + policyService.removeRuleFromPolicy(policy, rule); + } + + self.showRule = function showRule(rule) { + self.selectedRule = rule; + } + + self.hideRule = function hideRule() { + self.selectedRule = null; + self.currentData = null; + } + + self.assignData = function assignData(type, policy, data) { + self.currentData = { + data: data, + type: type, + loading: true, + perimeters: [], + assignments: [] + } + + policyService.loadPerimetersAndAssignments(type, policy).then(function (values) { + var category = categoryMap[type]; + self.currentData.loading = false; + self.currentData.perimeters = values.perimeters; + for (var index = 0; index < values.assignments.length; index++) { + var assignment = values.assignments[index]; + if (assignment.assignments.indexOf(data.id) >= 0) { + var perimeter = values.perimetersMap[assignment[category.perimeterId]]; + self.currentData.assignments.push(perimeter); + self.currentData.perimeters.splice(self.currentData.perimeters.indexOf(perimeter), 1); + } + } + }) + } + + self.createPerimeter = function createPerimeter(type, policy) { + var schema = { + type: "object", + properties: { + name: { type: "string", minLength: 2, title: gettext("Name") }, + description: { type: "string", minLength: 2, title: gettext("Description") }, + } + }; + if (type == 'subject') { + schema.properties.email = { type: "email", "type": "string", "pattern": "^\\S+@\\S+$", title: gettext("Email") } + } + var perimeter = { name: '', description: '' }; + var config = { + title: gettext('Create Perimeter'), + schema: schema, + form: ['name', { key: 'description', type: 'textarea' }], + model: perimeter + }; + if (type == 'subject') { + config.form.push('email'); + } + + ModalFormService.open(config).then(submit); + + function submit(form) { + policyService.createPerimeter(type, policy, form.model).then(function (perimeters) { + util.pushAll(self.currentData.perimeters, perimeters); + }) + } + } + + self.assign = function assign(type, policy, perimeter, data) { + policyService.createAssignment(type, policy, perimeter, data).then(function () { + self.currentData.assignments.push(perimeter); + self.currentData.perimeters.splice(self.currentData.perimeters.indexOf(perimeter), 1); + }) + } + + self.unassign = function unassign(type, policy, perimeter, data) { + policyService.removeAssignment(type, policy, perimeter, data).then(function () { + self.currentData.perimeters.push(perimeter); + self.currentData.assignments.splice(self.currentData.assignments.indexOf(perimeter), 1); + }) + } + } +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/policy/policy.html b/moon_dashboard/moon/static/moon/policy/policy.html new file mode 100644 index 00000000..70789fbb --- /dev/null +++ b/moon_dashboard/moon/static/moon/policy/policy.html @@ -0,0 +1,158 @@ +
+
+
+ + +
+
+ +
+
+

{$ policy.name $}

+
+ + +
+

{$ policy.description $}

+

+ Model: {$ policy.model ? policy.model.name : 'none' $} +

+

+ Genre: + {$ policy.genre ? policy.genre : 'none' $} +

+
+ +

Rules

+ +
+
+

Loading rules...

+
+
+
+ + Metarule: + {$ rule.metaRule.name $} +
+ + Rule: + + + {$ data.name $}{$ $last ? '' : ', ' $} + | + + {$ data.name $}{$ $last ? '' : ', ' $} + | + + {$ data.name $}{$ $last ? '' : ', ' $} + +
+ + +
+
+ +
+

+ Metarule: {$ rule.metaRule.name $}

+
+ + +
+

+ + + + + + + + + + + + + + + + + +
+ Subjects + + Objects + + Actions + + Instructions +
+

+ {$ data.name $} + + +

+
+

+ {$ data.name $} + + +

+
+

+ {$ data.name $} + + +

+
+

+                      
+

+

Loading...

+
+
+
+

+ Assign perimeters to {$ ctrl.currentData.data.name $}

+ + +
+
+
+

Available perimeters

+
+ +
+

Click to assign

+
+
+

Assigned perimeters

+
+ +
+

Click to unassign

+
+
+
+

+
+
+
+
+
+
+
\ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/policy/policy.service.js b/moon_dashboard/moon/static/moon/policy/policy.service.js new file mode 100755 index 00000000..87250b2e --- /dev/null +++ b/moon_dashboard/moon/static/moon/policy/policy.service.js @@ -0,0 +1,330 @@ +(function () { + + 'use strict'; + + angular + .module('moon') + .factory('moon.policy.service', policyService); + + policyService.$inject = ['moon.util.service', 'moon.model.service', '$resource', 'moon.URI', '$q', 'horizon.framework.widgets.toast.service']; + + function policyService(util, modelService, $resource, URI, $q, toast) { + var host = URI.API; + + var policyResource = $resource(host + '/policies/' + ':id', {}, { + get: { method: 'GET' }, + query: { method: 'GET' }, + create: { method: 'POST' }, + remove: { method: 'DELETE' }, + update: { method: 'PATCH' } + }); + + var policyRulesResource = $resource(host + '/policies/' + ':policy_id' + '/rules/' + ':rule_id', {}, { + get: { method: 'GET' }, + query: { method: 'GET' }, + create: { method: 'POST' }, + remove: { method: 'DELETE' } + }); + + var policySubjectDataResource = $resource(host + '/policies/' + ':policy_id' + '/subject_data/' + ':category_id', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + }) + + var policyObjectDataResource = $resource(host + '/policies/' + ':policy_id' + '/object_data/' + ':category_id', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + }) + + var policyActionDataResource = $resource(host + '/policies/' + ':policy_id' + '/action_data/' + ':category_id', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + }) + + var policySubjectPerimetersResource = $resource(host + '/policies/' + ':policy_id' + '/subjects', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + }) + + var policyObjectPerimetersResource = $resource(host + '/policies/' + ':policy_id' + '/objects', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + }) + + var policyActionPerimetersResource = $resource(host + '/policies/' + ':policy_id' + '/actions', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + }) + + var policySubjectAssignmentsResource = $resource(host + '/policies/' + ':policy_id' + '/subject_assignments/' + ':perimeter_id' + '/' + ':category_id' + '/' + ':data_id', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + remove: { method: 'DELETE' } + }) + + var policyObjectAssignmentsResource = $resource(host + '/policies/' + ':policy_id' + '/object_assignments/' + ':perimeter_id' + '/' + ':category_id' + '/' + ':data_id', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + remove: { method: 'DELETE' } + }) + + var policyActionAssignmentsResource = $resource(host + '/policies/' + ':policy_id' + '/action_assignments/' + ':perimeter_id' + '/' + ':category_id' + '/' + ':data_id', {}, { + query: {method: 'GET'}, + create: { method: 'POST' }, + remove: { method: 'DELETE' } + }) + + + var categoryMap = { + 'subject': { + resource: policySubjectDataResource, + arrayName: "subjectData", + mapName: "subjectDataMap", + responseName: "subject_data", + perimeterResource: policySubjectPerimetersResource, + assignmentResource: policySubjectAssignmentsResource, + perimeterResponseName: "subjects", + assignmentResponseName: "subject_assignments", + }, + 'object': { + resource: policyObjectDataResource, + arrayName: "objectData", + mapName: "objectDataMap", + responseName: "object_data", + perimeterResource: policyObjectPerimetersResource, + assignmentResource: policyObjectAssignmentsResource, + perimeterResponseName: "objects", + assignmentResponseName: "object_assignments", + }, + 'action': { + resource: policyActionDataResource, + arrayName: "actionData", + mapName: "actionDataMap", + responseName: "action_data", + perimeterResource: policyActionPerimetersResource, + assignmentResource: policyActionAssignmentsResource, + perimeterResponseName: "actions", + assignmentResponseName: "action_assignments", + } + } + + var policiesMap = {}; + var policies = []; + + function loadPolicies() { + var queries = { + policies: policyResource.query().$promise, + models: modelService.initialize(), + } + + $q.all(queries).then(function (result) { + createPolicies(result.policies); + console.log('moon', 'policies initialized') + }) + } + + function createPolicies(policiesData) { + policies.splice(0, policies.length); + util.cleanObject(policiesMap); + createPolicyInternal(policiesData.policies); + } + + function mapPolicy(policy) { + if (policy.model_id) { + policy.model = modelService.getModel(policy.model_id); + } + } + + function createPolicyInternal(data) { + return util.createInternal(data, policies, policiesMap, mapPolicy); + } + + function removePolicyInternal(id) { + return util.removeInternal(id, policies, policiesMap); + } + + function updatePolicyInternal(data) { + return util.updateInternal(data, policiesMap, mapPolicy); + } + + function removeRuleInternal(policy, rule) { + policy.rules.splice(policy.rules.indexOf(rule), 1); + } + + function loadPolicyRule(policy) { + if (!policy.rules) { + var queries = { + rules: policyRulesResource.query({ policy_id: policy.id }).$promise, + subjectData: policySubjectDataResource.query({ policy_id: policy.id }).$promise, + objectData: policyObjectDataResource.query({ policy_id: policy.id }).$promise, + actionData: policyActionDataResource.query({ policy_id: policy.id }).$promise, + } + + $q.all(queries).then(function (result) { + createRules(policy, result.rules, result.subjectData, result.objectData, result.actionData) + }, util.displayErrorFunction('Unable to load rules')) + } + } + + function createRules(policy, rulesData, subjectsData, objectsData, actionsData) { + policy.rules = rulesData ? rulesData.rules.rules : []; + policy.subjectDataMap = subjectsData.subject_data.length > 0 ? subjectsData.subject_data[0].data : []; + policy.subjectData = util.mapToArray(policy.subjectDataMap); + policy.objectDataMap = objectsData.object_data.length > 0 ? objectsData.object_data[0].data : []; + policy.objectData = util.mapToArray(policy.objectDataMap); + policy.actionDataMap = actionsData.action_data.length > 0 ? actionsData.action_data[0].data : []; + policy.actionData = util.mapToArray(policy.actionDataMap); + for (var i = 0; i < policy.rules.length; i++) { + var rule = policy.rules[i]; + populateRule(policy, rule); + } + } + + function populateRule(policy, rule) { + if (rule.meta_rule_id) { + rule.metaRule = modelService.getMetaRule(rule.meta_rule_id); + } + if (rule.metaRule) { + var j = 0; + var k, id; + rule.subjectData = []; + rule.objectData = []; + rule.actionData = []; + for (k = 0; k < rule.metaRule.subject_categories.length; k++) { + id = rule.rule[j + k]; + rule.subjectData.push(policy.subjectDataMap[id]); + } + j += k; + for (k = 0; k < rule.metaRule.object_categories.length; k++) { + id = rule.rule[j + k]; + rule.objectData.push(policy.objectDataMap[id]); + } + j += k; + for (k = 0; k < rule.metaRule.action_categories.length; k++) { + id = rule.rule[j + k]; + rule.actionData.push(policy.actionDataMap[id]); + } + } + return rule; + } + + return { + initialize: loadPolicies, + createPolicies: createPolicies, + policies: policies, + getPolicy: function getPolicy(id) { + return policiesMap[id]; + }, + createPolicy: function createPolicy(policy) { + policyResource.create(null, policy, success, util.displayErrorFunction('Unable to create Policy')); + + function success(data) { + createPolicyInternal(data.policies); + util.displaySuccess('Policy created'); + } + }, + removePolicy: function removePolicy(policy) { + policyResource.remove({ id: policy.id }, null, success, util.displayErrorFunction('Unable to remove Policy')); + + function success(data) { + removePolicyInternal(policy.id); + util.displaySuccess('Policy removed'); + } + }, + updatePolicy: function updatePolicy(policy) { + policyResource.update({ id: policy.id }, policy, success, util.displayErrorFunction('Unable to update Policy')); + + function success(data) { + updatePolicyInternal(data.policies) + util.displaySuccess('Policy updated'); + } + }, + populatePolicy: loadPolicyRule, + createRules: createRules, + addRuleToPolicy: function addRuleToPolicy(policy, rule) { + policyRulesResource.create({ policy_id: policy.id }, rule, success, util.displayErrorFunction('Unable to create Rule')); + + function success(data) { + var rules = util.mapToArray(data.rules); + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + policy.rules.push(populateRule(policy, rule)) + } + util.displaySuccess('Rule created'); + } + }, + removeRuleFromPolicy: function removeRuleFromPolicy(policy, rule) { + policyRulesResource.remove({ policy_id: policy.id, rule_id: rule.id }, null, success, util.displayErrorFunction('Unable to remove Rule')); + + function success(data) { + removeRuleInternal(policy, rule); + util.displaySuccess('Rule removed'); + } + }, + createData: function createData(type, policy, category, dataCategory) { + var categoryValue = categoryMap[type]; + return categoryValue.resource.create({ policy_id: policy.id, category_id: category.id }, dataCategory).$promise.then( + function (data) { + var result = util.createInternal(data[categoryValue.responseName].data, policy[categoryValue.arrayName], policy[categoryValue.mapName]); + util.displaySuccess('Data created'); + return result; + }, + util.displayErrorFunction('Unable to create Data') + ); + }, + createPerimeter: function createPerimeter(type, policy, perimeter) { + var categoryValue = categoryMap[type]; + return categoryValue.perimeterResource.create({ policy_id: policy.id }, perimeter).$promise.then( + function (data) { + util.displaySuccess('Perimeter created'); + return util.mapToArray(data[categoryValue.perimeterResponseName]); + }, + util.displayErrorFunction('Unable to create Perimeter') + ); + }, + loadPerimetersAndAssignments: function loadPerimetersAndAssignments(type, policy) { + var categoryValue = categoryMap[type]; + var queries = { + perimeters: categoryValue.perimeterResource.query({ policy_id: policy.id }).$promise, + assignments: categoryValue.assignmentResource.query({ policy_id: policy.id }).$promise, + } + + return $q.all(queries).then(function (data) { + var result = {}; + result.assignments = util.mapToArray(data.assignments[categoryValue.assignmentResponseName]); + result.perimetersMap = data.perimeters[categoryValue.perimeterResponseName]; + result.perimeters = util.mapToArray(result.perimetersMap); + return result; + }, util.displayErrorFunction('Unable to load Perimeters')) + + }, + createAssignment: function createAssignment(type, policy, perimeter, data) { + var categoryValue = categoryMap[type]; + var assignment = { + "id": perimeter.id, + "category_id": data.category_id, + "data_id": data.id, + "policy_id": policy.id + } + return categoryValue.assignmentResource.create({ policy_id: policy.id }, assignment).$promise.then( + function (data) { + util.displaySuccess('Assignment created'); + return util.mapToArray(data[categoryValue.assignmentResponseName]); + }, + util.displayErrorFunction('Unable to create Assignment') + ) + }, + removeAssignment: function removeAssignment(type, policy, perimeter, data) { + var categoryValue = categoryMap[type]; + + return categoryValue.assignmentResource.remove({ policy_id: policy.id, perimeter_id: perimeter.id, category_id: data.category_id, data_id: data.id }, null).$promise.then( + function (data) { + util.displaySuccess('Assignment removed'); + }, + util.displayErrorFunction('Unable to remove Assignment') + ) + }, + } + + } +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/policy/policy.service.spec.js b/moon_dashboard/moon/static/moon/policy/policy.service.spec.js new file mode 100755 index 00000000..045bf9b3 --- /dev/null +++ b/moon_dashboard/moon/static/moon/policy/policy.service.spec.js @@ -0,0 +1,336 @@ +(function () { + 'use strict'; + + describe('moon.policy.service', function () { + var service, modelService, $httpBackend, URI; + var policiesData; + var modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData; + var rulesData, subjectsData, objectsData, actionsData; + + + function initData() { + policiesData = { + policies: + { + 'policyId1': { name: 'policy1', description: 'pDescription1', genre: 'genre1', model_id: 'modelId1' }, + } + }; + + modelsData = { + models: + { 'modelId1': { name: 'model1', description: 'mDescription1', meta_rules: ['metaRuleId1'] } } + }; + + subjectCategoriesData = { + subject_categories: + { + 'subjectCategoryId1': { name: 'subjectCategory1', description: 'scDescription1' }, + 'subjectCategoryId2': { name: 'subjectCategory2', description: 'scDescription2' } + }, + }; + objectCategoriesData = { + object_categories: + { + 'objectCategoryId1': { name: 'objectCategory1', description: 'ocDescription1' }, + 'objectCategoryId2': { name: 'objectCategory2', description: 'ocDescription2' } + } + }; + actionCategoriesData = { + action_categories: + { + 'actionCategoryId1': { name: 'actionCategory1', description: 'acDescription1' }, + 'actionCategoryId2': { name: 'actionCategory2', description: 'acDescription2' } + } + }; + metaRulesData = { + meta_rules: + { + 'metaRuleId1': { name: 'metaRule1', description: 'mrDescription1', subject_categories: ['subjectCategoryId1'], object_categories: ['objectCategoryId1'], action_categories: ['actionCategoryId1'] }, + 'metaRuleId2': { name: 'metaRule2', description: 'mrDescription2', subject_categories: [], object_categories: [], action_categories: [] } + } + }; + } + + function initRuleData() { + rulesData = { + rules: { + rules: [ + { meta_rule_id: 'metaRuleId1', rule: ['subjectId1', 'objectId1', 'actionId1'], id: 'ruleId1', instructions: { test: 'test' } } + ] + } + }; + + subjectsData = { + subject_data: + [ + { + data: { + 'subjectId1': { name: 'subject1', description: 'sDescription1' }, + } + } + ] + }; + objectsData = { + object_data: + [ + { + data: { + 'objectId1': { name: 'object1', description: 'oDescription1' }, + } + } + ] + }; + actionsData = { + action_data: + [ + { + data: { + 'actionId1': { name: 'action1', description: 'aDescription1' }, + } + } + ] + }; + } + + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.framework')); + beforeEach(module('moon')); + + beforeEach(inject(function ($injector) { + service = $injector.get('moon.policy.service'); + modelService = $injector.get('moon.model.service'); + $httpBackend = $injector.get('$httpBackend'); + URI = $injector.get('moon.URI'); + })); + + afterEach(function () { + $httpBackend.verifyNoOutstandingExpectation(); + $httpBackend.verifyNoOutstandingRequest(); + }); + + it('should initialize', function () { + initData(); + $httpBackend.expectGET(URI.API + '/policies').respond(200, policiesData); + $httpBackend.expectGET(URI.API + '/subject_categories').respond(200, subjectCategoriesData); + $httpBackend.expectGET(URI.API + '/object_categories').respond(200, objectCategoriesData); + $httpBackend.expectGET(URI.API + '/action_categories').respond(200, actionCategoriesData); + $httpBackend.expectGET(URI.API + '/meta_rules').respond(200, metaRulesData); + $httpBackend.expectGET(URI.API + '/models').respond(200, modelsData); + + + service.initialize(); + $httpBackend.flush(); + + expect(service.policies.length).toBe(1); + var policy = service.policies[0]; + expect(policy.id).toBe('policyId1'); + expect(policy.name).toBe('policy1'); + expect(policy.description).toBe('pDescription1'); + expect(policy.genre).toBe('genre1'); + expect(policy.model.id).toBe('modelId1'); + + }); + + + + it('should create policy', function () { + initData(); + modelService.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + + var policyCreatedData = { + policies: + { 'policyId1': { name: 'policy1', description: 'pDescription1', genre: 'genre1', model_id: 'modelId1' } } + }; + + $httpBackend.expectPOST(URI.API + '/policies').respond(200, policyCreatedData); + + service.createPolicy({ name: 'policy1', description: 'pDescription1', genre: 'genre1', model: modelService.getModel('modelId1') }); + $httpBackend.flush(); + + expect(service.policies.length).toBe(1); + var policy = service.policies[0]; + expect(policy.id).toBe('policyId1'); + expect(policy.name).toBe('policy1'); + expect(policy.description).toBe('pDescription1'); + expect(policy.genre).toBe('genre1'); + expect(policy.model.id).toBe('modelId1'); + }); + + it('should remove policy', function () { + initData(); + modelService.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + service.createPolicies(policiesData); + + $httpBackend.expectDELETE(URI.API + '/policies/policyId1').respond(200); + + service.removePolicy({ id: 'policyId1' }); + $httpBackend.flush(); + + expect(service.policies.length).toBe(0); + }); + + it('should update policy', function () { + initData(); + var policyUpdatedData = { + policies: + { 'policyId1': { name: 'policy2', description: 'pDescription2', genre: 'genre2', model_id: 'modelId1' } } + }; + modelService.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + service.createPolicies(policiesData); + + $httpBackend.expectPATCH(URI.API + '/policies/policyId1').respond(200, policyUpdatedData); + + service.updatePolicy({ id: 'policyId1', name: 'policy2', description: 'pDescription2', genre: 'genre2', model: modelService.getModel('modelId1') }); + $httpBackend.flush(); + + expect(service.policies.length).toBe(1); + var policy = service.policies[0]; + expect(policy.id).toBe('policyId1'); + expect(policy.name).toBe('policy2'); + expect(policy.description).toBe('pDescription2'); + expect(policy.genre).toBe('genre2'); + expect(policy.model.id).toBe('modelId1'); + + }); + + + it('should populate policy', function () { + initData(); + initRuleData(); + modelService.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + service.createPolicies(policiesData); + + var policy = service.getPolicy('policyId1') + + $httpBackend.expectGET(URI.API + '/policies/policyId1/rules').respond(200, rulesData); + $httpBackend.expectGET(URI.API + '/policies/policyId1/subject_data').respond(200, subjectsData); + $httpBackend.expectGET(URI.API + '/policies/policyId1/object_data').respond(200, objectsData); + $httpBackend.expectGET(URI.API + '/policies/policyId1/action_data').respond(200, actionsData); + + service.populatePolicy(policy); + $httpBackend.flush(); + + expect(policy.rules.length).toBe(1); + var rule = policy.rules[0]; + expect(rule.id).toBe('ruleId1'); + expect(rule.metaRule.id).toBe('metaRuleId1'); + expect(rule.instructions.test).toBe('test'); + expect(rule.subjectData.length).toBe(1); + expect(rule.subjectData[0].id).toBe('subjectId1'); + expect(rule.objectData.length).toBe(1); + expect(rule.objectData[0].id).toBe('objectId1'); + expect(rule.actionData.length).toBe(1); + expect(rule.actionData[0].id).toBe('actionId1'); + + expect(policy.subjectData.length).toBe(1); + var subjectData = policy.subjectData[0]; + expect(subjectData.id).toBe('subjectId1'); + expect(subjectData.name).toBe('subject1'); + expect(subjectData.description).toBe('sDescription1'); + + expect(policy.objectData.length).toBe(1); + var objectData = policy.objectData[0]; + expect(objectData.id).toBe('objectId1'); + expect(objectData.name).toBe('object1'); + expect(objectData.description).toBe('oDescription1'); + + expect(policy.actionData.length).toBe(1); + var actionData = policy.actionData[0]; + expect(actionData.id).toBe('actionId1'); + expect(actionData.name).toBe('action1'); + expect(actionData.description).toBe('aDescription1'); + + }); + + + it('should add rule to policy', function () { + initData(); + initRuleData(); + modelService.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + service.createPolicies(policiesData); + + + var ruleCreatedData = { + rules: { + 'ruleId1': { meta_rule_id: 'metaRuleId1', rule: ['subjectId1', 'objectId1', 'actionId1'], instructions: { test: 'test' } } + } + }; + + var policy = service.getPolicy('policyId1'); + + service.createRules(policy, null, subjectsData, objectsData, actionsData); + + $httpBackend.expectPOST(URI.API + '/policies/policyId1/rules').respond(200, ruleCreatedData); + + service.addRuleToPolicy(policy, { meta_rule_id: 'metaRuleId1', rule: ['subjectId1', 'objectId1', 'actionId1'], instructions: { test: 'test' } }); + $httpBackend.flush(); + + expect(policy.rules.length).toBe(1); + var rule = policy.rules[0]; + expect(rule.id).toBe('ruleId1'); + expect(rule.metaRule.id).toBe('metaRuleId1'); + expect(rule.subjectData.length).toBe(1); + expect(rule.subjectData[0].id).toBe('subjectId1'); + expect(rule.objectData.length).toBe(1); + expect(rule.objectData[0].id).toBe('objectId1'); + expect(rule.actionData.length).toBe(1); + expect(rule.actionData[0].id).toBe('actionId1'); + + }); + + it('should remove rule from policy', function () { + initData(); + initRuleData(); + modelService.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + service.createPolicies(policiesData); + + var policy = service.getPolicy('policyId1'); + + service.createRules(policy, rulesData, subjectsData, objectsData, actionsData); + + $httpBackend.expectDELETE(URI.API + '/policies/policyId1/rules/ruleId1').respond(200); + + service.removeRuleFromPolicy(policy, { id: 'ruleId1' }); + $httpBackend.flush(); + + expect(policy.rules.length).toBe(0); + }); + + + it('should create data', function () { + initData(); + initRuleData(); + modelService.createModels(modelsData, metaRulesData, subjectCategoriesData, objectCategoriesData, actionCategoriesData); + service.createPolicies(policiesData); + + + var dataCreatedData = { + subject_data: { + data: { + 'subjectId1': { name: 'subject1', description: 'sDescription1' }, + } + } + }; + + var policy = service.getPolicy('policyId1'); + policy.subjectData = []; + policy.subjectDataMap = {}; + + $httpBackend.expectPOST(URI.API + '/policies/policyId1/subject_data/subjectCategoryId1').respond(200, dataCreatedData); + + service.createData('subject', policy, modelService.getCategory('subject', 'subjectCategoryId1'), { name: 'subject1', description: 'sDescription1' }); + $httpBackend.flush(); + + expect(policy.subjectData.length).toBe(1); + var subjectData = policy.subjectData[0]; + expect(subjectData.id).toBe('subjectId1'); + expect(subjectData.name).toBe('subject1'); + expect(subjectData.description).toBe('sDescription1'); + + }); + + + }); + + +})(); \ No newline at end of file diff --git a/moon_dashboard/moon/static/moon/scss/moon.scss b/moon_dashboard/moon/static/moon/scss/moon.scss new file mode 100644 index 00000000..20bf6c41 --- /dev/null +++ b/moon_dashboard/moon/static/moon/scss/moon.scss @@ -0,0 +1,54 @@ +.inline { + display: inline; +} + +.inline-block { + display: inline-block; +} + +summary{ + outline:none; + margin-bottom: 10px; +} + +details { + cursor: default; +} + +.filter { + display: inline-block; + width: auto; + vertical-align: middle; +} + +.categories td { + width: 33%; +} + +.width-200 { + width: 200px; +} + +.height-200 { + height: 200px; +} + +.border { + border: 1px #DDD solid; +} + +.padding-10 { + padding: 10px; +} + +.scroll { + overflow-y: auto; +} + +.mt-5 { + margin-top: 5px; +} + +.input-file { + display: none !important; +} \ No newline at end of file diff --git a/moon_dashboard/moon/templates/moon/base.html b/moon_dashboard/moon/templates/moon/base.html new file mode 100644 index 00000000..f07a01ba --- /dev/null +++ b/moon_dashboard/moon/templates/moon/base.html @@ -0,0 +1,11 @@ +{% load horizon %}{% jstemplate %}[% extends 'base.html' %] + +[% block sidebar %] + [% include 'horizon/common/_sidebar.html' %] +[% endblock %] + +[% block main %] + [% include "horizon/_messages.html" %] + [% block {{ dash_name }}_main %][% endblock %] +[% endblock %] +{% endjstemplate %} diff --git a/moon_dashboard/run.sh b/moon_dashboard/run.sh new file mode 100644 index 00000000..bf18faa2 --- /dev/null +++ b/moon_dashboard/run.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# sudo docker run -ti --rm -p 8000:8000 -e MANAGER_HOST=localhost -e MANAGER_PORT=30001 -e KEYSTONE_HOST=localhost -e KEYSTONE_PORT=30005 moonplatform/dashboard:dev + +CONSTANT_FILE=/root/horizon/openstack_dashboard/dashboards/moon/static/moon/js/moon.module.js + +sed "s/{{MANAGER_HOST}}/$MANAGER_HOST/g" -i $CONSTANT_FILE +sed "s/{{MANAGER_PORT}}/$MANAGER_PORT/g" -i $CONSTANT_FILE +sed "s/{{KEYSTONE_HOST}}/$KEYSTONE_HOST/g" -i $CONSTANT_FILE +sed "s/{{KEYSTONE_PORT}}/$KEYSTONE_PORT/g" -i $CONSTANT_FILE + +cd /root/horizon + +LOCAL_SETTINGS=/root/horizon/openstack_dashboard/local/local_settings.py +sed "s/OPENSTACK_HOST = \"127.0.0.1\"/OPENSTACK_HOST = \"${OPENSTACK_HOST}\"/" -i $LOCAL_SETTINGS +sed "s#OPENSTACK_KEYSTONE_URL = \"http:\/\/%s:5000\/v2.0\" % OPENSTACK_HOST#OPENSTACK_KEYSTONE_URL = \"${OPENSTACK_KEYSTONE_URL}\"#" -i $LOCAL_SETTINGS + +echo ----------------- +grep OPENSTACK_HOST $LOCAL_SETTINGS +grep OPENSTACK_KEYSTONE_URL LOCAL_SETTINGS +echo ----------------- + +echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ $CONSTANT_FILE" +cat $CONSTANT_FILE +echo "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~" + +tox -e runserver -- 0.0.0.0:8000 \ No newline at end of file diff --git a/moon_dashboard/setup.cfg b/moon_dashboard/setup.cfg new file mode 100644 index 00000000..f68765dd --- /dev/null +++ b/moon_dashboard/setup.cfg @@ -0,0 +1,24 @@ +[metadata] +name = moon +version=1.2.0 +summary = A dashboard plugin for Moon +description-file = + README.rst +author = Jonathan Gourdin +author_email = jonathan.gourdin@orange.com +home-page = https://docs.openstack.org/horizon/latest/ +classifiers = [ + Environment :: OpenStack + Framework :: Django + Intended Audience :: Developers + 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.5 + +[files] +packages = + moon \ No newline at end of file diff --git a/moon_dashboard/setup.py b/moon_dashboard/setup.py new file mode 100644 index 00000000..4794e334 --- /dev/null +++ b/moon_dashboard/setup.py @@ -0,0 +1,14 @@ +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr>=1.8'], + pbr=True) \ No newline at end of file -- cgit 1.2.3-korg