diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c6864401..5fbb0cd7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,11 @@ variables: ARTIFACT_DOWNLOAD_ATTEMPTS: 3 RESTORE_CACHE_ATTEMPTS: 3 +cache: + paths: + - .tox/ + + stages: - code_standarts - build @@ -17,7 +22,7 @@ stages: .branch_tests_template: &branch_tests stage: test - image: git.vst.lan/vst/tests:tox + image: onegreyonewhite/tox:tox variables: TOX_ENVS: "" script: @@ -39,7 +44,7 @@ stages: .pack_tamplate: &packing-test stage: packaging-test - image: git.vst.lan/vst/tests:tox + image: onegreyonewhite/tox:tox variables: MYSQL_ROOT_PASSWORD: 'polemarch' MYSQL_DATABASE: 'polemarch' @@ -86,13 +91,9 @@ stages: ########################################### code_style: stage: code_standarts - image: git.vst.lan/vst/tests:tox + image: onegreyonewhite/tox:tox script: - make test ENVS=flake,pylint - cache: - paths: - - .tox/pylint - - .tox/flake only: - /^.{0,}issue_.*$/ - developer @@ -121,7 +122,7 @@ default_rpm_tests: default_oracle_tests: <<: *packing-test - image: git.vst.lan/vst/tests:oracle + image: onegreyonewhite/tox:oracle script: - cat /etc/hosts - make rpm RELEASE=${CI_BUILD_ID} @@ -133,7 +134,7 @@ default_oracle_tests: default_deb_tests: <<: *packing-test - image: git.vst.lan/vst/tests:ubuntu + image: onegreyonewhite/tox:ubuntu script: - cat /etc/hosts - make deb RELEASE=${CI_BUILD_ID} @@ -203,7 +204,7 @@ release_deb: stage: release only: - tags - image: git.vst.lan/vst/tests:ubuntu + image: onegreyonewhite/tox:ubuntu script: - make deb allow_failure: false diff --git a/.pep8 b/.pep8 index 9d6692ec..98182122 100644 --- a/.pep8 +++ b/.pep8 @@ -1,7 +1,9 @@ [pep8] ignore = E221,E222,E121,E123,E126,E226,E24,E704,E116,E731,E722,E741 exclude = ./polemarch/*/migrations/*,./polemarch/main/settings*.py,.tox/*,./etc/*,./*/__init__.py,./t_openstack.py +max-line-length=90 [flake8] ignore = E221,E222,E121,E123,E126,E226,E24,E704,E116,E731,E722,E741 exclude = ./polemarch/*/migrations/*,./polemarch/main/settings*.py,.tox/*,./etc/*,./*/__init__.py,./t_openstack.py,./polemarch/projects/* +max-line-length=90 diff --git a/MANIFEST.in b/MANIFEST.in index 11a7fa65..63b9e685 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,13 +8,8 @@ include polemarch/main/tests/test_repo.tar include requirements.txt include requirements-git.txt include requirements-doc.txt -recursive-include etc * recursive-include polemarch/*.c * recursive-include polemarch/doc/html * recursive-include polemarch/*.so * recursive-include polemarch/static * -recursive-include polemarch/*/templates * -recursive-include initbin * -include Makefile -include deb.mk -include rpm.mk \ No newline at end of file +recursive-include polemarch/*/templates * \ No newline at end of file diff --git a/Makefile b/Makefile index ebbf5844..b0839541 100644 --- a/Makefile +++ b/Makefile @@ -21,11 +21,22 @@ COMPOSE = docker-compose-testrun.yml COMPOSE_ARGS = --abort-on-container-exit COMPLEX_TESTS_COMPOSE = docker-compose-tests.yml COMPLEX_TESTS_COMPOSE_ARGS = '--abort-on-container-exit --build' +DEFAULT_PREFIX = /opt +INSTALL_PREFIX = $(shell if [[ ! -z "${prefix}" ]]; then echo -n $(prefix); else echo -n $(DEFAULT_PREFIX); fi) +INSTALL_DIR = $(INSTALL_PREFIX)/${NAME} +INSTALL_BINDIR = $(INSTALL_DIR)/bin +REQUIREMENTS = -r requirements.txt -r requirements-doc.txt +TMPDIR := $(shell mktemp -d) +BUILD_DIR= $(TMPDIR) +PREBUILD_DIR = $(BUILD_DIR)/$(INSTALL_DIR) +PREBUILD_BINDIR = $(BUILD_DIR)/$(INSTALL_BINDIR) +SOURCE_DIR = $(shell pwd) + include rpm.mk include deb.mk -all: compile +all: compile clean_prebuild prebuild docs: @@ -52,11 +63,44 @@ compile: build-clean -rm -rf polemarch/doc/* $(PY) setup.py compile -v +prebuild: + # Create virtualenv + $(PY) -m virtualenv --no-site-packages $(PREBUILD_DIR) + # Install required packages + $(PREBUILD_BINDIR)/pip install -U pip + $(PREBUILD_BINDIR)/pip install -U dist/$(NAME)-$(VER).tar.gz $(REQUIREMENTS) + $(PREBUILD_BINDIR)/pip install -U -r requirements-git.txt + # RECORD files are used by wheels for checksum. They contain path names which + # match the buildroot and must be removed or the package will fail to build. + find $(PREBUILD_DIR) -name "RECORD" -exec rm -rf {} \; + # Change the virtualenv path to the target installation direcotry. + venvctrl-relocate --source=$(PREBUILD_DIR) --destination=$(INSTALL_DIR) + # Remove sources for Clang + find $(PREBUILD_DIR)/lib -type f -name "*.c" -print0 | xargs -0 rm -rf + # Remove broken link + -rm -rf $(PREBUILD_DIR)/local + # Install settings + -install -Dm 755 $(NAME)/main/settings.ini $(BUILD_DIR)/etc/$(USER)/settings.ini.template + # Install systemd services + -install -Dm 755 initbin/$(NAME)web.service $(BUILD_DIR)/etc/systemd/system/$(NAME)web.service + -install -Dm 755 initbin/$(NAME)worker.service $(BUILD_DIR)/etc/systemd/system/$(NAME)worker.service + # Install tmpdirs config + -install -Dm 755 initbin/$(NAME).conf $(BUILD_DIR)/etc/tmpfiles.d/$(NAME).conf + # Create tmpdirs + -mkdir -p $(BUILD_DIR)/var/{log,run,lock}/$(NAME) + install: - $(PIP) install dist/$(ARCHIVE) django\>=1.8,\<1.12 + # Change owner for packages + -chown -R $(USER):$(USER) $(PREBUILD_DIR) $(BUILD_DIR)/var/{log,run,lock}/$(NAME) $(BUILD_DIR)/etc/$(USER) + # Copy build + cp -rf $(BUILD_DIR)/* / + $(MAKE) clean_prebuild uninstall: - $(PIP) uninstall $(NAME) + -rm -rf $(INSTALL_DIR) + +clean_prebuild: + -rm -rf $(BUILD_DIR)/* clean: build-clean find ./polemarch -name "*.c" -print0 | xargs -0 rm -rf @@ -79,13 +123,13 @@ fclean: clean find ./polemarch -name "*.c" -print0 | xargs -0 rm -rf -rm -rf .tox -rpm: compile +rpm: echo "$$RPM_SPEC" > polemarch.spec rm -rf ~/rpmbuild mkdir -p ~/rpmbuild/SOURCES/ ls -la - cp -vf dist/$(ARCHIVE) ~/rpmbuild/SOURCES rpmbuild --verbose -bb polemarch.spec + mkdir -p dist cp -v ~/rpmbuild/RPMS/x86_64/*.rpm dist/ rm polemarch.spec @@ -108,11 +152,14 @@ deb: chmod +x debian/prerm chmod +x debian/postrm # build - dpkg-buildpackage -uc -us + dpkg-buildpackage -d -uc -us mv -v ../$(NAME)_$(VER)*.deb dist/ # cleanup rm -rf debian +compose_down: + docker-compose -f $(COMPOSE) down + compose: docker-compose -f $(COMPOSE) build diff --git a/deb.mk b/deb.mk index 6cab181b..f3875b16 100644 --- a/deb.mk +++ b/deb.mk @@ -42,13 +42,14 @@ export DEBIAN_COPYRIGHT # paths and executables variables BUILDROOT = debian/$(NAME) -INSTALLDIR = opt/$(NAME) define DEBIAN_RULES #!/usr/bin/make -f # maximum verbosity during deb build DH_VERBOSE = 1 export DH_OPTIONS=-v # targets, that we want to override with no actions +override_dh_auto_build: + # don't need becouse all makes in 'override_dh_auto_install' target override_dh_auto_test: # don't want to test during package build override_dh_strip: @@ -65,27 +66,7 @@ override_dh_auto_install: mkdir -p $(BUILDROOT) touch $(BUILDROOT)/dummy rm -rf $(BUILDROOT)/* - # install our package with all required python dependencies in virtualenv - virtualenv --no-site-packages $(BUILDROOT)/$(INSTALLDIR) - rm -rf $(BUILDROOT)/$(INSTALLDIR)/local - $(BUILDROOT)/$(INSTALLDIR)/bin/pip install $(PIPARGS) -r requirements-doc.txt - $(BUILDROOT)/$(INSTALLDIR)/bin/pip install $(PIPARGS) dist/$(NAME)-$(VER).tar.gz - $(BUILDROOT)/$(INSTALLDIR)/bin/pip install $(PIPARGS) -r requirements-git.txt - find $(BUILDROOT)/ -name "RECORD" -exec rm -rf {} \; - venvctrl-relocate --source=$(BUILDROOT)/$(INSTALLDIR) --destination=/$(INSTALLDIR) - find $(BUILDROOT)/$(INSTALLDIR)/lib -type f -name "*.c" -print0 | xargs -0 rm -rf - # system folders which is needed for application to work (lob, lock, etc) - mkdir -p $(BUILDROOT)/var/log/$(NAME) - mkdir -p $(BUILDROOT)/var/run/$(NAME) - mkdir -p $(BUILDROOT)/var/lock/$(NAME) - mkdir -p $(BUILDROOT)/etc/$(NAME) - # systemd services - mkdir -p $(BUILDROOT)/etc/systemd/system/ - mkdir -p $(BUILDROOT)/etc/tmpfiles.d/ - cp initbin/*.service $(BUILDROOT)/etc/systemd/system/ - cp initbin/*.conf $(BUILDROOT)/etc/tmpfiles.d/ - # settings - cp $(NAME)/main/settings.ini $(BUILDROOT)/etc/$(NAME)/ + make BUILD_DIR=$(BUILDROOT) %: dh $$@ endef diff --git a/doc/conf.py b/doc/conf.py index 4cc5ee72..78a90b1b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -64,7 +64,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. diff --git a/doc/gui.rst b/doc/gui.rst index 8766ad36..61969242 100644 --- a/doc/gui.rst +++ b/doc/gui.rst @@ -8,12 +8,12 @@ In this section of our documentation we will tell you about Polemarch GUI's oppo Let's begin with Dashboard page: -.. image:: gui_png/new-dashboard-1.png +.. image:: gui_png/dashboard.png As you can see, Polemarch GUI provides user with 2 menus: * the first one is located in the left sidebar and it is aimed - to provide user with navigation between main system objects, like projects, templates, history records and ect. + to provide user with navigation between main system objects, like projects, inventories, history records and ect. * the second one is located in the top right conner of browser window and it is aimed to navigate user to API section, to user's page and to logout page. @@ -27,41 +27,141 @@ user is able to change widgets' position by Drag and Drop. To collapse or to hide/show some widgets user should click on 'cogwheel' button. After this button has been clicked, Polemarch opens modal window, where user can activate or collapse some widgets. -To see all these features in work look at next gif-image: - -.. image:: gui_gif1/new-dashboard-1.gif - - Before you start ---------------- Before you can do any job with Polemarch you should create at least one -inventory with your servers enumeration and at least one project, because all +inventory with your servers enumeration(groups and hosts) and at least one project, because all Polemarch's functions are linked to the project. -Let's start with creation of inventory: +Inventories +----------- -.. image:: gui_png/new-inventories_page.png +.. image:: gui_png/inventories.png There are 2 ways of inventory's creation: -* the first one is to create inventory manually. To do it user should click on 'Create' button. +* the first one is to create inventory manually. To do it user should click on "Create" button. -* the second on is to import inventory from text file. To do it user should click on 'Import from file' button. +* the second one is to import inventory from text file. To do it user should click on "Import from file" button. By inventory's creation, in this case, we understand creation of inventory, which includes at least one group, which, in it's turn, includes at least one host. In other words, beside inventory user should create host and group. -To understand it better let's look at next gif-images: +To understand it better let's look at next images, which will explain you how to create inventory manually. +Here you can see the form for manual creation of inventory. + +.. image:: gui_png/create-new-inventory.png + +As you can see, this form is rather simple. There are only 2 sections with several fields to input. + +Section "New inventory": + +* **name** - name of your inventory. + +* **notes** - not required field for some user’s notes, for example, + for what purpose this inventory was created or something like this. + +Section "Adding new variable": + +* **name** - name of ansible variable. This field has autocomplete, so you can just start typing + the variable name and Polemarch will suggest you appropriate name values. + +* **value** - value of ansible variable. + +After inventory creation you will see the next page: + +.. image:: gui_png/test-inventory.png +.. image:: gui_png/test-inventory2.png + +There are some new buttons here: + +* **save** - this button saves all changes you have made on this page. +* **history** - this button opens history list of inventory executions. + +As you can see, also new "Sub items" section has appeared. This section has 4 buttons: + +* **edit existing groups** - this button gives you an opportunity to add to inventory groups, that are already created, + or to delete some groups from this inventory. + +* **create new group** - this button gives you an opportunity to create new group and add it to this inventory. + +* **edit existing hosts** - this button gives you an opportunity to add to inventory hosts, that are already created, + or to delete some hosts from this inventory. + +* **create new host** - this button gives you an opportunity to create new host and add it to this inventory. + +Let's look how you can create a group. + +Groups +------ + +.. image:: gui_png/create-new-group.png + +Section "New group": + +* **name** - name of your group. + +* **children** - if this field is true, group can consist of other croups only. + Otherwise, this group can consist of hosts only. + +* **notes** - not required field for some user’s notes, for example, + for what purpose this group was created or something like this. + +Section "Adding new variable": + +* **name** - name of ansible variable. This field has autocomplete, so you can just start typing + the variable name and Polemarch will suggest you appropriate name values. + +* **value** - value of ansible variable. + +After group creation you will see the next page: + +.. image:: gui_png/test-group.png +.. image:: gui_png/test-group2.png + +As you can see, new "Variables" and "Sub item" sections have appeared. + +"Sub item" section has 2 buttons: + +* **edit existing hosts** - this button gives you an opportunity to add to inventory hosts, that are already created, + or to delete some hosts from this inventory. + +* **create new host** - this button gives you an opportunity to create new host and add it to this inventory. + +"Variables" section has a list of variables that user have chosen during group creation. + +Let's look how you can create a host. + +Hosts +----- + +.. image:: gui_png/create-new-host.png + +Section "New host": + +* **name** - name of your host. + Name can be either human-readable(example.com) or hostname/IP (192.168.0.12) or range of them(19[2:7].168.0.12). + +* **notes** - not required field for some user’s notes, for example, + for what purpose this host was created or something like this. + +Section "Adding new variable": + +* **name** - name of ansible variable. This field has autocomplete, so you can just start typing + the variable name and Polemarch will suggest you appropriate name values. + +* **value** - value of ansible variable. -Here you can see how user can create inventory and place his hosts and groups there manually: +After host creation you will see the next page: -.. image:: gui_gif1/new-create_inventory_manually.gif +.. image:: gui_png/test-host.png -And here you can see how user can import Ansible inventory file: +As you can see, new "Variables" section has appeared and it has a list of variables that user have chosen during host creation. -.. image:: gui_gif1/new-import_inventory.gif +Projects +-------- Futher to start your work with Polemarch you should create project. @@ -80,34 +180,145 @@ There are 3 project types in Polemarch: Let's look at the example of GIT project's creation: -.. image:: gui_gif1/new-create_git_project.gif +.. image:: gui_png/create-new-git-project.png -As you can see at the gif-image above for GIT project -it is possible to choose a branch to what user want to sync. In this example user has synced -his GIT project from 'master' branch to 'other' branch. 'Arrow' icon in the branch input field +As you can see, the form of new GIT project creation consist of 5 fields: + +* **name** - name of your project. + +* **repository type** - type of project repository (GIT, TAR, MANUAL). + +* **repository URL** - URL to your repository. + +* **repository password** - repository password if it exist. + +* **branch** - branch of your GIT project, to what your Polemarch project will be synced. + If you stay it empty, Polemarch will sync to "master" branch. + +* **update before execution** - if true, project will be updated before each task + execution from this project. + +* **notes** - not required field for some user’s notes, for example, + for what purpose this project was created or something like this. + +After project creation you will the next page: + +.. image:: gui_png/test-project.png + +As you can see at image above for GIT project +it is possible to choose a branch to what user want to sync. In this example user will sync +his GIT project from 'master' branch to 'other' branch during next synchronization. 'Arrow' icon in the branch input field shows us, that project will be sync from one branch to another. If there is no 'arrow' icon, it means, that next time project will be sync to the same branch as you can see it in 'Branch' input field. +Also there are 2 new fields: + +* **revision** - GIT project revision. + +* **status** - Polemarch project status. + Possible values are: NEW - newly created project, + WAIT_SYNC - repository synchronization has been scheduled, but has not started to perform yet, + SYNC - synchronization is in progress, + ERROR - synchronization failed, + OK - project is synchronized. + +Also there are several buttons on this page: + +* **save** - this button saves all changes you have made on this page. + +* **sync** - this button syncs your Polemarch project with GIT repository. + +* **run playbook** - this button opens a "Run plabook" page. + +* **run module** - this button opens a "Run module" page. + +* **periodic tasks** - this button opens a page with list of periodic tasks of this project. + +* **history** - this button opens a page with list of history records of this project. + +* **import templates** - this button imports a text file with task/module template for this project from your computer. + +* **remove** - this button deletes this project. + If you update something in your GIT repository, don't forget to run sync in Polemarch for pulling your changes. After your project's status has changed into "OK" you can confidently start working with Polemarch. -Execution of playbook and modules ---------------------------------- +Execution of modules +-------------------- Ok, we made all preparations and ready to do some real work. Let's start by executing some command on your servers: -.. image:: gui_gif1/new-run_shell_command.gif +.. image:: gui_png/execute-ansible-module.png + +Here you can see 2 sections: "Execute ansible module" and "Adding new argument". + +"Execute ansible module" consist of next fields: + +* **inventory source** - source of inventory. It can be either "From database" or "From file in project dir". + +* **inventory from project / inventory file** - name of inventory. + +* **group** - name of group to which this module will be executed. + +* **module** - name of ansible module. This field has autocomplete, so you can just start typing + the ansible module name and Polemarch will suggest you appropriate name values. + +* **args** - arguments for ansible module. + +Section "Adding new argument": + +* **name** - name of ansible variable. + +* **value** - value of ansible variable. + +After you completed all necessary fields you should click on "Execute" button to run this ansible module. +After this you will see the next page: + +.. image:: gui_png/module-shell-1.png +.. image:: gui_png/module-shell-2.png + +As you can see there are 3 sections on this page: "Inventory", "stdout", "Task". + +"Inventory" section includes ansible inventory in text format. + +"stdout" section includes what ansible has written to stdout and stderr during execution. +With "Clear" button you can delete this output. + +"Task" sections consist of next fields: + +* **status** - status of task. It indicates different results of execution and can be + DELAY (scheduled for run), OK (successful run), INTERRUPTED (interrupted by user), RUN (currently running), + OFFLINE (can’t connect to node), ERROR (failure). + +* **module** - name of executed module. + +* **start time** - time, when task execution was started. + +* **stop time** - time, when task execution was finished. + +* **execution time** - amount of time the execution took. + +* **initiator** - name of object, who executed this task. + +* **executor** - name of user, who executed this task. + +* **revision** - project revision. + +* **inventory** - name of inventory. -As you can see at the gif-image above -when task has stopped running it become possible to clear ansible stdout. +* **args** - list of args, which were used during task execution. -Also you can run any Ansible modules and any of playbooks in your project. + +Execution of playbooks +---------------------- + +Also you can run any of playbooks in your project. Polemarch will scan project dir root for any .yml file and provide possibility -to run them. So place available playbook targets at root of your Git repository +to run them. So place available playbook targets at root of your GIT repository or tar-archive or folder with your project files. Be aware that your project must have "OK" status, because your @@ -118,28 +329,171 @@ in playbook execution page. Let's look at the example of running some playbook, which Polemarch imported from GIT repository of our project: -.. image:: gui_gif1/new-running_playbook.gif +.. image:: gui_png/execute-playbook.png + +Here you can see 2 sections: "Run playbook" and "Adding new argument". + +"Run playbook" consist of next fields: + +* **playbook** - name of playbook. This field has autocomplete with playbook names from your GIT/TAR/MANUAL project. + +* **inventory source** - source of inventory. It can be either "From database" or "From file in project dir". + +* **inventory from project / inventory file** - name of inventory. + +* **group** - name of group to which this module will be executed. + +Section "Adding new argument": + +* **name** - name of ansible variable. + +* **value** - value of ansible variable. + +After you completed all necessary fields you should click on "Execute" button to run this playbook. +After this you will see the next page: + +.. image:: gui_png/playbook-executed-1.png +.. image:: gui_png/playbook-executed-2.png + +As you can see there are 3 sections on this page: "Inventory", "stdout", "Task". + +"Inventory" section includes ansible inventory in text format. + +"stdout" section includes what ansible has written to stdout and stderr during execution. +With "Clear" button you can delete this output. + +"Task" sections consist of next fields: + +* **status** - status of task. It indicates different results of execution and can be + DELAY (scheduled for run), OK (successful run), INTERRUPTED (interrupted by user), RUN (currently running), + OFFLINE (can’t connect to node), ERROR (failure). + +* **playbook** - name of executed playbook. + +* **start time** - time, when task execution was started. + +* **stop time** - time, when task execution was finished. + +* **execution time** - amount of time the execution took. + +* **initiator** - name of object, who executed this task. + +* **executor** - name of user, who executed this task. +* **revision** - project revision. + +* **inventory** - name of inventory. + +* **args** - list of args, which were used during task execution. Templates --------- If you have many arguments, which you pass to Ansible at every task run (like extra-vars, forks number and so on), you can create template for such action -to minimize hand work (either module run or playbook): +to minimize hand work. Polemarch provides user with 2 kinds of templates: +task template(template for playbook execution) and module template(template for module execution). +Both of this template kinds are similar, that's why we will look at the example of module template creation only. + +.. image:: gui_png/create-module-template.png + +This page has 2 sections: "Run module template" and "Adding new argument". + +"Run module template" section consist of next fields: + +* **template name** - name of template. + +* **project** - name of project, for which this template will be available. + +* **inventory source** - source of inventory. It can be either "From database" or "From file in project dir". + +* **inventory from project / inventory file** - name of inventory. + +* **group** - name of group to which this module will be executed. + +* **module** - name of ansible module. This field has autocomplete, so you can just start typing + the ansible module name and Polemarch will suggest you appropriate name values. + +* **args** - arguments for ansible module. + +* **notes** - not required field for some user’s notes, for example, + for what purpose this template was created or something like this. + +Section "Adding new argument": + +* **name** - name of ansible variable. + +* **value** - value of ansible variable. + +After you completed all necessary fields you should click on "Create" button to save this template. +After this you will see the next page: + +.. image:: gui_png/module-template-page.png + +As you can see, this page has the same sections as the previous page. + +But also there are some new buttons here: + +* **save** - this button saves all changes you have made on this page. + +* **save and execute** - this button saves all changes you have made on this page and executes this template. + +* **create new option** - this button opens the "Create new option" page. -.. image:: gui_gif1/new-create_template.gif +* **history** - this button opens history list of template executions. + +* **copy** - this button creates a copy of this template. + +* **remove** - this button deletes this template. + +Options +------- Sometimes your need to keep some similar templates, which are different by only several parameters. In this case template options will be extremly useful for you. In every template you can create a lot of options which can modify this template by some parameters. Let's look at the example: -.. image:: gui_gif1/new-create_template_option.gif +.. image:: gui_png/create-new-option.png + +As you can see there are 2 section on this page: "New option" and "Adding new argument". + +"New option" section consist of next fields: + +* **name** - name of option. + +* **group** - name of group to which this template will be executed, if this option be selected for execution. + +* **module** - name of ansible module which will be executed, if this option be selected for execution. + This field has autocomplete, so you can just start typing + the ansible module name and Polemarch will suggest you appropriate name values. + +* **args** - ansible module arguments, which will be used, if this option be selected for execution. + +Section "Adding new argument": + +* **name** - name of ansible variable. + +* **value** - value of ansible variable. +After you completed all necessary fields you should click on "Create" button to save this template option. +After this you will see the next page: -Also you can backup/share your templates using import/export mechanism: +.. image:: gui_png/option-page.png -.. image:: gui_gif1/new-export-import-template.gif +There is new section "Additional arguments", that includes list of arguments, which will be added +to template during execution. + +Buttons on this page: + +* **save** - this button saves all changes you have made on this page. + +* **save and execute** - this button saves all changes you have made on this page and executes template with this option. + +* **remove** - this button deletes this template option. + +Also you can backup/share your templates using export mechanism: + +.. image:: gui_png/export-template.png Periodic tasks -------------- @@ -148,16 +502,77 @@ If you want to run some actions by schedule without any control from you, it is possible with Polemarch. You can create periodic tasks, which runs every X seconds (interval based): -.. image:: gui_gif1/new-create-periodic-task-interval.gif +.. image:: gui_png/create-periodic.png + +As you can see there are 2 sections on this page: "New task" and "Adding new argument". + +"New task" section consist of next fields: + +* **name** - name of periodic task. + +* **save in history** - if value is true, the fact of task execution will be saved in history records. + Otherwise, no history records about this periodic task execution will be saved. + +* **inventory source** - source of inventory. It can be either "From database" or "From file in project dir". + +* **inventory from project / inventory file** - name of inventory. + +* **group** - name of group to which this periodic task will be executed. + +* **kind** - kind of task: module or playbook. + +* **playbook** - name of playbook. This field is available for kind=playbook only. + +* **module** - name of ansible module. This field has autocomplete, so you can just start typing + the ansible module name and Polemarch will suggest you appropriate name values. + This field is available for kind=module only. + +* **args** - arguments for ansible module. This field is available for kind=module only. + +* **type** - type of schedule. It can be either "Interval schedule" or "Cron style schedule". + +* **interval schedule / cron style schedule** - value for schedule. + +* **notes** - not required field for some user’s notes, for example, + for what purpose this periodic task was created or something like this. + +Section "Adding new argument": + +* **name** - name of ansible variable. + +* **value** - value of ansible variable. + +After you completed all necessary fields you should click on "Save task" button to save this periodic task. +After this you will see the next page: + +.. image:: gui_png/test-periodic.png + +This page has the same sections as the previous one, but there is a new field: + +* **enabled** - if the value is true, this periodic task will be available and will be working. + +Buttons on this page: + +* **save** - this button saves all changes you have made on this page. + +* **execute** - this button executes this periodic task. + +* **copy** - this button creates a copy of this periodic task. + +* **remove** - this button deletes this periodic task. Also you can create periodic tasks with more advancing scheduling options (days of week, hours, month and so on) by using cron-style periodic tasks: -.. image:: gui_gif1/new-create-periodic-schedule.gif +.. image:: gui_png/cron-schedule.png + +As you can see this task will be executed at 9 o'clock each day of each month. Search ------ Almost everywhere in Polemarch you can filter your data. Let see for example how to filter your execution history records to find result of needed action: -.. image:: gui_gif1/new-search2.gif \ No newline at end of file +.. image:: gui_png/search0.png + +.. image:: gui_png/search.png \ No newline at end of file diff --git a/doc/gui_png/create-module-template.png b/doc/gui_png/create-module-template.png new file mode 100644 index 00000000..9f840c88 Binary files /dev/null and b/doc/gui_png/create-module-template.png differ diff --git a/doc/gui_png/create-new-git-project.png b/doc/gui_png/create-new-git-project.png new file mode 100644 index 00000000..61b73878 Binary files /dev/null and b/doc/gui_png/create-new-git-project.png differ diff --git a/doc/gui_png/create-new-group.png b/doc/gui_png/create-new-group.png new file mode 100644 index 00000000..9d21c351 Binary files /dev/null and b/doc/gui_png/create-new-group.png differ diff --git a/doc/gui_png/create-new-host.png b/doc/gui_png/create-new-host.png new file mode 100644 index 00000000..ae8f99ee Binary files /dev/null and b/doc/gui_png/create-new-host.png differ diff --git a/doc/gui_png/create-new-inventory.png b/doc/gui_png/create-new-inventory.png new file mode 100644 index 00000000..159d434e Binary files /dev/null and b/doc/gui_png/create-new-inventory.png differ diff --git a/doc/gui_png/create-new-option.png b/doc/gui_png/create-new-option.png new file mode 100644 index 00000000..0b909026 Binary files /dev/null and b/doc/gui_png/create-new-option.png differ diff --git a/doc/gui_png/create-periodic.png b/doc/gui_png/create-periodic.png new file mode 100644 index 00000000..0c821e48 Binary files /dev/null and b/doc/gui_png/create-periodic.png differ diff --git a/doc/gui_png/cron-schedule.png b/doc/gui_png/cron-schedule.png new file mode 100644 index 00000000..e04e6ddb Binary files /dev/null and b/doc/gui_png/cron-schedule.png differ diff --git a/doc/gui_png/dashboard.png b/doc/gui_png/dashboard.png new file mode 100644 index 00000000..4041ad76 Binary files /dev/null and b/doc/gui_png/dashboard.png differ diff --git a/doc/gui_png/execute-ansible-module.png b/doc/gui_png/execute-ansible-module.png new file mode 100644 index 00000000..dbe35f15 Binary files /dev/null and b/doc/gui_png/execute-ansible-module.png differ diff --git a/doc/gui_png/execute-playbook.png b/doc/gui_png/execute-playbook.png new file mode 100644 index 00000000..6e583687 Binary files /dev/null and b/doc/gui_png/execute-playbook.png differ diff --git a/doc/gui_png/export-template.png b/doc/gui_png/export-template.png new file mode 100644 index 00000000..53aab4f2 Binary files /dev/null and b/doc/gui_png/export-template.png differ diff --git a/doc/gui_png/inventories.png b/doc/gui_png/inventories.png new file mode 100644 index 00000000..7f3ef89a Binary files /dev/null and b/doc/gui_png/inventories.png differ diff --git a/doc/gui_png/module-shell-1.png b/doc/gui_png/module-shell-1.png new file mode 100644 index 00000000..5d9488bb Binary files /dev/null and b/doc/gui_png/module-shell-1.png differ diff --git a/doc/gui_png/module-shell-2.png b/doc/gui_png/module-shell-2.png new file mode 100644 index 00000000..8046f6e3 Binary files /dev/null and b/doc/gui_png/module-shell-2.png differ diff --git a/doc/gui_png/module-template-page.png b/doc/gui_png/module-template-page.png new file mode 100644 index 00000000..50b02237 Binary files /dev/null and b/doc/gui_png/module-template-page.png differ diff --git a/doc/gui_png/option-page.png b/doc/gui_png/option-page.png new file mode 100644 index 00000000..26b95c4d Binary files /dev/null and b/doc/gui_png/option-page.png differ diff --git a/doc/gui_png/playbook-executed-1.png b/doc/gui_png/playbook-executed-1.png new file mode 100644 index 00000000..f66c8e42 Binary files /dev/null and b/doc/gui_png/playbook-executed-1.png differ diff --git a/doc/gui_png/playbook-executed-2.png b/doc/gui_png/playbook-executed-2.png new file mode 100644 index 00000000..44e0f691 Binary files /dev/null and b/doc/gui_png/playbook-executed-2.png differ diff --git a/doc/gui_png/search.png b/doc/gui_png/search.png new file mode 100644 index 00000000..08bceeac Binary files /dev/null and b/doc/gui_png/search.png differ diff --git a/doc/gui_png/search0.png b/doc/gui_png/search0.png new file mode 100644 index 00000000..56e18d24 Binary files /dev/null and b/doc/gui_png/search0.png differ diff --git a/doc/gui_png/test-group.png b/doc/gui_png/test-group.png new file mode 100644 index 00000000..69418f95 Binary files /dev/null and b/doc/gui_png/test-group.png differ diff --git a/doc/gui_png/test-group2.png b/doc/gui_png/test-group2.png new file mode 100644 index 00000000..a50145ee Binary files /dev/null and b/doc/gui_png/test-group2.png differ diff --git a/doc/gui_png/test-host.png b/doc/gui_png/test-host.png new file mode 100644 index 00000000..e2511056 Binary files /dev/null and b/doc/gui_png/test-host.png differ diff --git a/doc/gui_png/test-inventory.png b/doc/gui_png/test-inventory.png new file mode 100644 index 00000000..5b3a230f Binary files /dev/null and b/doc/gui_png/test-inventory.png differ diff --git a/doc/gui_png/test-inventory2.png b/doc/gui_png/test-inventory2.png new file mode 100644 index 00000000..9d27751f Binary files /dev/null and b/doc/gui_png/test-inventory2.png differ diff --git a/doc/gui_png/test-periodic.png b/doc/gui_png/test-periodic.png new file mode 100644 index 00000000..397a2854 Binary files /dev/null and b/doc/gui_png/test-periodic.png differ diff --git a/doc/gui_png/test-project.png b/doc/gui_png/test-project.png new file mode 100644 index 00000000..9b8586ee Binary files /dev/null and b/doc/gui_png/test-project.png differ diff --git a/doc/polemarch-sphinx-theme/layout.html b/doc/polemarch-sphinx-theme/layout.html index 36114ed5..fa51c1ef 100644 --- a/doc/polemarch-sphinx-theme/layout.html +++ b/doc/polemarch-sphinx-theme/layout.html @@ -1,17 +1,20 @@ +{%- macro css() %} + + + {%- for css in css_files %} + {%- if css|attr("rel") %} + + {%- else %} + + {%- endif %} + {%- endfor %} +{%- endmacro %} + {%- macro script() %} - + {%- for scriptfile in script_files %} {%- endfor %} @@ -20,19 +23,22 @@ {%- endmacro %} - -{%- block extrahead %} + Polemarch documantation + {%- block css %} + {{- css() }} + {%- endblock %} - {%- block scripts %} {{- script() }} {%- endblock %} -{% endblock %} +{%- block extrahead %} {% endblock %} + +
@@ -111,12 +117,14 @@

Polemarch documentation

+ {% block footer %} + {% endblock %} diff --git a/doc/polemarch-sphinx-theme/search.html b/doc/polemarch-sphinx-theme/search.html index d26e8948..79baa38b 100644 --- a/doc/polemarch-sphinx-theme/search.html +++ b/doc/polemarch-sphinx-theme/search.html @@ -11,9 +11,7 @@ {% set title = _('Search') %} {% set script_files = script_files + ['_static/searchtools.js'] %} {% block extrahead %} - {%- for scriptfile in script_files %} - - {%- endfor %} + diff --git a/doc/polemarch-sphinx-theme/static/documentation_options.js_t b/doc/polemarch-sphinx-theme/static/documentation_options.js_t new file mode 100644 index 00000000..363d97d5 --- /dev/null +++ b/doc/polemarch-sphinx-theme/static/documentation_options.js_t @@ -0,0 +1,9 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: '{{ url_root }}', + VERSION: '{{ release|e }}', + LANGUAGE: '{{ language }}', + COLLAPSE_INDEX: false, + FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}', + HAS_SOURCE: {{ has_source|lower }}, + SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}' +}; \ No newline at end of file diff --git a/doc/polemarch-sphinx-theme/static/polemarch-style.css b/doc/polemarch-sphinx-theme/static/polemarch-style.css index e80fa9fd..a77e3a32 100644 --- a/doc/polemarch-sphinx-theme/static/polemarch-style.css +++ b/doc/polemarch-sphinx-theme/static/polemarch-style.css @@ -7,14 +7,7 @@ @import url(css/AdminLTE.css); @import url(css/ansi-colors.css); @import url(css/font-awesome.min.css); -/*@import url(/polemarch/static/css/gui.css);*/ @import url(css/ionicons.min.css); -@import url(bootstrap/fonts/glyphicons-halflings-regular.eot); -@import url(bootstrap/fonts/glyphicons-halflings-regular.svg); -@import url(bootstrap/fonts/glyphicons-halflings-regular.ttf); -@import url(bootstrap/fonts/glyphicons-halflings-regular.woff); -@import url(bootstrap/fonts/glyphicons-halflings-regular.woff2); - @import url(css/skins/_all-skins.css); @import url(css/skins/skin-blue.css); @import url(css/skins/skin-blue-light.css); @@ -36,9 +29,7 @@ { font-weight: 600; display:block; - padding-top:7px; - padding-bottom:7px; - padding-left:15px; + padding: 12px 5px 12px 15px; } .sidebar-menu .caption { @@ -47,7 +38,7 @@ .sidebar-menu ul li a:hover { - background-color: #f4f4f5; + background-color: #E0E0E0; } .sidebar-menu ul li ul { @@ -59,9 +50,7 @@ { font-weight: 500; display:block; - padding-top:7px; - padding-bottom:7px; - padding-left:35px; + padding: 12px 5px 12px 25px; } .sidebar-menu, @@ -114,7 +103,6 @@ img { } .highlight .s2 { - /* color: #4070a0;*/ color: #D14; } @@ -261,6 +249,17 @@ form.search { z-index:2000; } +li.toctree-l1.current { + border-left: 5px solid #00c0ef; + background-color: #EEEEEE; +} + +li.toctree-l1.current>a { + padding: 12px 5px 12px 10px!important; +} + + + @media (max-width: 767px) { .bg-logo-doc { background-image: url(img/logo/horizontal-bw.png); diff --git a/doc/polemarch-sphinx-theme/static/pygments.css b/doc/polemarch-sphinx-theme/static/pygments.css new file mode 100644 index 00000000..fe753368 --- /dev/null +++ b/doc/polemarch-sphinx-theme/static/pygments.css @@ -0,0 +1,32 @@ +.highlight pre { + background-color: #f7f7f9; +} + +.highlight pre p { + color: #333; +} + +.highlight .mi { + color: #208050; +} + +.highlight .s2 { + /* color: #4070a0;*/ + color: #D14; +} + +.highlight .kc { + color: #000081; +} + +.highlight .nf, +.highlight .na { + font-weight: bold; +} + +.highlight .l, +.highlight .nn, +.highlight .kr, +.highlight .m { + color: #195f91; +} \ No newline at end of file diff --git a/doc/quickstart.rst b/doc/quickstart.rst index 9ee87e7c..b76fc481 100644 --- a/doc/quickstart.rst +++ b/doc/quickstart.rst @@ -106,13 +106,13 @@ Uploading of backup in MySQL: Making backup in PostgreSQL: - .. sourcecode:: postgresql + .. sourcecode:: bash pg_dump dbname > dump.sql Uploading of backup in PostgreSQL: - .. sourcecode:: postgresql + .. sourcecode:: bash createdb dbname psql dbname < dump.sql diff --git a/doc/restapi.rst b/doc/restapi.rst index 7f54ff45..bf33290f 100644 --- a/doc/restapi.rst +++ b/doc/restapi.rst @@ -68,6 +68,7 @@ Hosts { "id":12, "name":"038108237241668497-0875926814493907", + "notes":"some notes about this host", "type":"HOST", "vars":{ @@ -84,6 +85,7 @@ Hosts :>json number id: id of host. :>json string name: |host_name_def| + :>json string notes: |host_notes_def| :>json string type: |host_type_def| :>json object vars: |obj_vars_def| :>json object owner: |host_owner_details| @@ -94,6 +96,8 @@ Hosts otherwise is ``HOST``. .. |host_name_def| replace:: either human-readable name or hostname/IP or range of them (it is depends on context of using this host during playbooks running). +.. |host_notes_def| replace:: not required field for some user's notes, for example, + for what purpose this host was created or something like this. .. |host_owner_details| replace:: owner of host. Supported fields could be seen in :http:get:`/api/v1/users/{id}/`. .. |hosts_details_ref| replace:: **Response JSON Object:** response json fields are the @@ -166,6 +170,7 @@ Hosts Creates host. :json number id: id of group. :>json string name: name of group. + :>json string notes: |group_notes_def| :>json array hosts: |group_hosts_def| :>json array groups: |group_groups_def| :>json object vars: |obj_vars_def| @@ -322,6 +332,8 @@ Groups ``false``, otherwise empty. See :ref:`hosts` for fields explanation. .. |group_groups_def| replace:: list of subgroups in group, if ``children`` is ``true``, otherwise empty. +.. |group_notes_def| replace:: not required field for some user's notes, for example, + for what purpose this group was created or something like this. .. |group_children_def| replace:: either this group of subgroups or group of hosts. .. |group_owner_details| replace:: owner of group. Supported fields @@ -383,6 +395,7 @@ Groups Creates group. :json number id: id of inventory. :>json string name: name of inventory. + :>json string notes: |inventory_notes_def| :>json array hosts: |inventory_hosts_def| :>json array all_hosts: |inventory_all_hosts_def| :>json array groups: |inventory_groups_def| @@ -612,6 +630,8 @@ Inventories .. |inventory_hosts_def| replace:: list of hosts in inventory. See :ref:`hosts` for fields explanation. +.. |inventory_notes_def| replace:: not required field for some user's notes, for example, + for what purpose this inventory was created or something like this. .. |inventory_all_hosts_def| replace:: list of all hosts in inventory(includes also hosts from this inventory's groups) . See :ref:`hosts` for fields explanation. .. |inventory_groups_def| replace:: list of groups in inventory. @@ -673,6 +693,7 @@ Inventories Creates inventory. :json number id: id of project. :>json string name: name of project. + :>json string notes: |project_notes_def| :>json string repository: |project_repository_def| :>json string status: current status of project. Possible values are: ``NEW`` - newly created project, ``WAIT_SYNC`` - repository @@ -867,6 +893,8 @@ Projects :>json string branch: current branch of project, to which project has been synced last time. :>json string url: url to this specific inventory. +.. |project_notes_def| replace:: not required field for some user's notes, for example, + for what purpose this project was created or something like this. .. |project_repository_def| replace:: URL of repository (repo-specific URL). For ``TAR`` it is just HTTP-link to archive. .. |project_hosts_def| replace:: list of hosts in project. See :ref:`hosts` @@ -949,6 +977,7 @@ Projects description :http:post:`/api/v1/projects/{id}/sync/` :json number id: id of periodic task. + :>json string name: name of periodic task. + :>json string notes: |ptask_notes_def| :>json string type: |ptask_type_details| :>json string schedule: |ptask_schedule_details| :>json string mode: playbook or module to run periodically. @@ -1346,6 +1381,10 @@ Periodic tasks :>json object vars: |ptask_vars_def| :>json string url: url to this specific periodic task. +.. |ptask_notes_def| replace:: not required field for some user's notes, for example, + for what purpose this periodic task was created or something like this. + + .. |ptask_details_ref| replace:: **Response JSON Object:** response json fields are the same as in :http:get:`/api/v1/periodic-tasks/{id}/`. @@ -1457,6 +1496,7 @@ Periodic tasks { "name":"new-periodic-test", + "notes":"", "type": "INTERVAL", "schedule": "25", "mode": "touch_the_clouds.yml", @@ -1475,6 +1515,7 @@ Periodic tasks { "id": 14, "name":"new-periodic-test", + "notes":"", "type": "INTERVAL", "schedule": "25", "mode": "touch_the_clouds.yml", @@ -1523,6 +1564,7 @@ Periodic tasks { "id": 14, "name":"new-periodic-test", + "notes":"", "type": "INTERVAL", "schedule": "25", "mode": "touch_the_clouds.yml", @@ -1602,6 +1644,7 @@ Templates { "id": 1, "name": "test_tmplt", + "notes": "some notes about this template", "kind": "Task", "owner": { "id": 1, @@ -1637,12 +1680,16 @@ Templates :>json number id: id of template. :>json string name: name of template. + :>json string notes: |template_notes_def| :>json string kind: |template_kind_details| :>json object owner: |template_owner_details| :>json string data: |template_data_details| :>json object options: tepmlate options, which can update some template's settings before new execution. :>json array options_list: list of options' names for this template. +.. |template_notes_def| replace:: not required field for some user's notes, for example, + for what purpose this template was created or something like this. + .. |template_details_ref| replace:: **Response JSON Object:** response json fields are the same as in :http:get:`/api/v1/templates/{id}/`. @@ -1717,9 +1764,10 @@ Templates Creates template + :json string start_time: time, when playbook execution was started. :>json string stop_time: time, when playbook execution was ended (normally or not) + :>json number execution_time: time taken to perform task execution (in seconds). :>json number inventory: id of inventory. :>json string raw_inventory: ansible inventory, which was used for execution. It was generated from Polemarch's :ref:`inventory` @@ -1972,8 +2027,11 @@ History records in :http:get:`/api/v1/history/{id}/raw/`. :>json number initiator: initiator id. :>json string initiator_type: initiator type like in api url. + :>json number executor: id of user, who executed task. :>json object execute_args: arguments, which were used during execution. :>json string revision: project revision. + :>json object options: some additional options, which were used during this task execution. + For example, if you execute some template with option, name of this option will be saved to this object. :>json string url: url to this specific history record. .. |history_details_ref| replace:: **Response JSON Object:** response json fields are the @@ -2160,7 +2218,9 @@ History records "start_time": "2017-07-24T06:39:52.052504Z", "stop_time": "2017-07-24T06:41:06.521813Z", "initiator": 1, - "initiator_type": "users", + "initiator_type": "project", + "executor": 1, + "options": {}, "url": "http://localhost:8000/api/v1/history/121/" }, { @@ -2172,8 +2232,12 @@ History records "status": "OK", "start_time": "2017-07-24T06:27:40.481588Z", "stop_time": "2017-07-24T06:27:42.499873Z", - "initiator": 1, - "initiator_type": "users", + "initiator": 2, + "initiator_type": "template", + "executor": 1, + "options": { + "template_option": "some-vars" + }, "url": "http://localhost:8000/api/v1/history/118/" } ] @@ -2763,6 +2827,13 @@ Hooks ----- Polemarch has his own system of hooks. +Polemarch hooks are synchronous and you can appoint them on different events like "after end task", +"before start task" and so on. + +More information about hooks types and events ou can find here :http:get:`/api/v1/hooks/types/`. + +WARNING: You should be accurate with hooks appointment, because the more hooks you have, +the more time they need for execution and, finally, the more time Ansible needs for task execution. .. http:get:: /api/v1/hooks/ @@ -2859,9 +2930,9 @@ Polemarch has his own system of hooks. { "name": "new hook", - "type": "HTTP", + "type": "SCRIPT", "when": "on_execution", - "recipients": "http://localhost:8000/new_hook_trigger" + "recipients": "new-test.sh" } Results: @@ -3003,7 +3074,6 @@ Users { "id":3, "username":"petya", - "password":"pbkdf2_sha256$36000$usSWH0uGIPZl$+Xzz3KpJrq8ZP3truExYOe3CjsaIWgOxuN6jIvJ5ZO8=", "is_active":true, "is_staff":false, "first_name":"Petya", @@ -3118,7 +3188,6 @@ Users { "id":3, "username":"petya", - "password":"pbkdf2_sha256$36000$usSWH0uGIPZl$+Xzz3KpJrq8ZP3truExYOe3CjsaIWgOxuN6jIvJ5ZO8=", "is_active":true, "is_staff":false, "first_name":"Petya", @@ -3157,7 +3226,6 @@ Users { "id":3, "username":"petrusha", - "password":"pbkdf2_sha256$36000$usSWH0uGIPZl$+Xzz3KpJrq8ZP3truExYOe3CjsaIWgOxuN6jIvJ5ZO8=", "is_active":true, "is_staff":false, "first_name":"Petya", @@ -3429,6 +3497,7 @@ in ACL system. { "id": 1, "name": "myteam", + "notes": "some notes about this team", "users": [ { "id": 1, @@ -3461,6 +3530,7 @@ in ACL system. :>json number id: id of team. :>json string name: name of team. + :>json string notes: |team_notes_def| :>json array users: array of users in team. See :ref:`users` for fields explanation. :>json array users_list: ids of users in team. @@ -3469,6 +3539,8 @@ in ACL system. .. |team_details_ref| replace:: **Response JSON Object:** response json fields are the same as in :http:get:`/api/v1/teams/{id}/`. +.. |team_notes_def| replace:: not required field for some user’s notes, for example, + for what purpose this team was created or something like this. .. http:get:: /api/v1/teams/ @@ -3517,6 +3589,7 @@ in ACL system. Creates team. :=0.25.2' +FROM onegreyonewhite/tox:centos +MAINTAINER sergey.k@vstconsulting.net # Build and install rpm -COPY requirements-doc.txt /tmp/requirements-doc.txt -COPY requirements-git.txt /tmp/requirements-git.txt -COPY requirements.txt /tmp/requirements.txt -RUN pip2 install -U -r /tmp/requirements-doc.txt \ - -r /tmp/requirements-git.txt \ - -r /tmp/requirements.txt +RUN curl https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh | tee /usr/bin/wait-for-it.sh && \ + chmod +x /usr/bin/wait-for-it.sh RUN mkdir /tmp/polemarch ADD . /tmp/polemarch/ RUN cd /tmp/polemarch && make rpm diff --git a/polemarch/__init__.py b/polemarch/__init__.py index 4f19d3a1..8f9d0130 100644 --- a/polemarch/__init__.py +++ b/polemarch/__init__.py @@ -1,6 +1,6 @@ from .environment import prepare_environment -__version__ = "0.1.5" +__version__ = "0.1.6" def _main(**kwargs): # pylint: disable=unused-variable diff --git a/polemarch/api/base.py b/polemarch/api/base.py index 9e2498dd..e34a378d 100644 --- a/polemarch/api/base.py +++ b/polemarch/api/base.py @@ -57,7 +57,9 @@ def get_queryset(self): " or override the `get_queryset()` method." % self.__class__.__name__ ) - self.queryset = self.model.objects.all() + self.queryset = getattr( + self.model.objects, 'cleared', self.model.objects.all + )() if self.kwargs.get("pk", None) is None: self.queryset = self.get_extra_queryset() return self._base_get_queryset() @@ -153,6 +155,7 @@ def get_queryset(self): class ListNonModelViewSet(NonModelsViewSet, viewsets.mixins.ListModelMixin): # pylint: disable=abstract-method + schema = None @property def methods(self): diff --git a/polemarch/api/handlers.py b/polemarch/api/handlers.py index f79728d2..63200b33 100644 --- a/polemarch/api/handlers.py +++ b/polemarch/api/handlers.py @@ -4,6 +4,7 @@ from django.core.exceptions import PermissionDenied from django.core import exceptions as djexcs +from django.http.response import Http404 from rest_framework import exceptions, status, views from rest_framework.response import Response @@ -25,17 +26,17 @@ def polemarch_exception_handler(exc, context): return Response({"detail": exc.msg}, status=status.HTTP_424_FAILED_DEPENDENCY) - elif isinstance(exc, mexcs.NotApplicable): - return Response({"detail": exc.msg}, + elif isinstance(exc, (mexcs.NotApplicable, Http404)): + return Response({"detail": getattr(exc, 'msg', str(exc))}, status=status.HTTP_404_NOT_FOUND) elif isinstance(exc, djexcs.ValidationError): if hasattr(exc, 'error_dict'): errors = dict(exc) - elif hasattr(exc, 'error_list'): + elif hasattr(exc, 'error_list'): # nocv errors = {'other_errors': list(exc)} else: - errors = {'other_errors': str(exc)} + errors = {'other_errors': str(exc)} # nocv return Response({"detail": errors}, status=status.HTTP_400_BAD_REQUEST) diff --git a/polemarch/api/permissions.py b/polemarch/api/permissions.py index f1ceab66..cc0862da 100644 --- a/polemarch/api/permissions.py +++ b/polemarch/api/permissions.py @@ -9,10 +9,10 @@ def has_object_permission(self, request, view, obj): if request.user.is_staff or request.user == obj: return True if request.method in permissions.SAFE_METHODS: # nocv - return obj.viewable_by(request.user) # nocv + return obj.acl_handler.viewable_by(request.user) # nocv if view.action in view.POST_WHITE_LIST: # nocv - return obj.viewable_by(request.user) # nocv - return obj.editable_by(request.user) # noce + return obj.acl_handler.viewable_by(request.user) # nocv + return obj.acl_handler.editable_by(request.user) # noce class SuperUserPermission(ModelPermission): diff --git a/polemarch/api/v1/filters.py b/polemarch/api/v1/filters.py index 66f45fe4..f71781c5 100644 --- a/polemarch/api/v1/filters.py +++ b/polemarch/api/v1/filters.py @@ -1,5 +1,5 @@ # pylint: disable=import-error -from rest_framework import filters +from django_filters import rest_framework as filters from django_filters import (CharFilter, NumberFilter, IsoDateTimeFilter) from django.contrib.auth.models import User from ...main import models diff --git a/polemarch/api/v1/serializers.py b/polemarch/api/v1/serializers.py index 4de4edf1..515ed831 100644 --- a/polemarch/api/v1/serializers.py +++ b/polemarch/api/v1/serializers.py @@ -110,18 +110,19 @@ def __get_all_permission_serializer(self): # noce self.instance.acl.all(), many=True ) - def __duplicates_check(self, data): + def __duplicates_check(self, data): # noce without_role = [ frozenset({e['member'], e['member_type']}) for e in data ] if len(without_role) != len(list(set(without_role))): raise ValueError("There is duplicates in your permissions set.") + @transaction.atomic def __permission_set(self, data, remove_old=True): # noce self.__duplicates_check(data) for permission_args in data: if remove_old: - self.instance.acl.extend().filter( + self.instance.acl.all().extend().filter( member=permission_args['member'], member_type=permission_args['member_type'] ).delete() @@ -130,10 +131,11 @@ def __permission_set(self, data, remove_old=True): # noce @transaction.atomic def permissions(self, request): # noce user = self.current_user() - if request.method != "GET" and not self.instance.manageable_by(user): + if request.method != "GET" and \ + not self.instance.acl_handler.manageable_by(user): raise PermissionDenied(self.perms_msg) if request.method == "DELETE": - self.instance.acl.filter_by_data(request.data).delete() + self.instance.acl.all().filter_by_data(request.data).delete() elif request.method == "POST": self.__permission_set(request.data) elif request.method == "PUT": @@ -142,9 +144,9 @@ def permissions(self, request): # noce return Response(self.__get_all_permission_serializer().data, 200) def _change_owner(self, request): # noce - if not self.instance.owned_by(self.current_user()): + if not self.instance.acl_handler.owned_by(self.current_user()): raise PermissionDenied(self.perms_msg) - self.instance.set_owner(User.objects.get(pk=request.data)) + self.instance.acl_handler.set_owner(User.objects.get(pk=request.data)) return Response("Owner changed", 200) def owner(self, request): # noce @@ -234,12 +236,14 @@ class OneTeamSerializer(TeamSerializer): users = UserSerializer(many=True, required=False) users_list = DictField(required=False) owner = UserSerializer(read_only=True) + notes = serializers.CharField(required=False, allow_blank=True) class Meta: model = models.UserGroup fields = ( 'id', "name", + "notes", "users", "users_list", "owner", @@ -250,6 +254,7 @@ class Meta: class OneUserSerializer(UserSerializer): groups = TeamSerializer(read_only=True, many=True) raw_password = serializers.HiddenField(default=False, initial=False) + password = serializers.CharField(write_only=True) class Meta: model = User @@ -281,6 +286,9 @@ class Meta: "stop_time", "initiator", "initiator_type", + "executor", + "revision", + "options", "url") @@ -296,14 +304,17 @@ class Meta: "status", "start_time", "stop_time", + "execution_time", "inventory", "raw_inventory", "raw_args", "raw_stdout", "initiator", "initiator_type", + "executor", "execute_args", "revision", + "options", "url") def get_raw(self, request): @@ -431,11 +442,13 @@ class Meta: class OneHostSerializer(HostSerializer): owner = UserSerializer(read_only=True) vars = DictField(required=False) + notes = serializers.CharField(required=False, allow_blank=True) class Meta: model = models.Host fields = ('id', 'name', + 'notes', 'type', 'vars', 'owner', @@ -500,11 +513,13 @@ def owner(self, request): # noce class OnePeriodictaskSerializer(PeriodictaskSerializer): vars = DictField(required=False) + notes = serializers.CharField(required=False, allow_blank=True) class Meta: model = models.PeriodicTask fields = ('id', 'name', + 'notes', 'type', 'schedule', 'mode', @@ -581,12 +596,14 @@ class OneTemplateSerializer(TemplateSerializer): owner = UserSerializer(read_only=True) options = DictField(required=False) options_list = DictField(read_only=True) + notes = serializers.CharField(required=False, allow_blank=True) class Meta: model = models.Template fields = ( 'id', 'name', + 'notes', 'kind', 'owner', 'data', @@ -632,11 +649,13 @@ class OneGroupSerializer(GroupSerializer, _InventoryOperations): hosts = HostSerializer(read_only=True, many=True) groups = GroupSerializer(read_only=True, many=True) owner = UserSerializer(read_only=True) + notes = serializers.CharField(required=False, allow_blank=True) class Meta: model = models.Group fields = ('id', 'name', + 'notes', 'hosts', "groups", 'vars', @@ -675,11 +694,13 @@ class OneInventorySerializer(InventorySerializer, _InventoryOperations): hosts = HostSerializer(read_only=True, many=True, source="hosts_list") groups = GroupSerializer(read_only=True, many=True, source="groups_list") owner = UserSerializer(read_only=True) + notes = serializers.CharField(required=False, allow_blank=True) class Meta: model = models.Inventory fields = ('id', 'name', + 'notes', 'hosts', 'all_hosts', "groups", @@ -714,11 +735,13 @@ class OneProjectSerializer(ProjectSerializer, _InventoryOperations): groups = GroupSerializer(read_only=True, many=True) inventories = InventorySerializer(read_only=True, many=True) owner = UserSerializer(read_only=True) + notes = serializers.CharField(required=False, allow_blank=True) class Meta: model = models.Project fields = ('id', 'name', + 'notes', 'status', 'repository', 'hosts', @@ -739,22 +762,30 @@ def sync(self): data = dict(detail="Sync with {}.".format(self.instance.repository)) return Response(data, 200) - def _execution(self, kind, data, user): + def _execution(self, kind, data, user, **kwargs): + template = kwargs.pop("template", None) inventory = data.pop("inventory") try: inventory = Inventory.objects.get(id=int(inventory)) - if not inventory.viewable_by(user): # nocv + if not inventory.acl_handler.viewable_by(user): # nocv raise PermissionDenied( "You don't have permission to inventory." ) except ValueError: pass + if template is not None: + init_type = "template" + obj_id = template + data['template_option'] = kwargs.get('template_option', None) + else: + init_type = "project" + obj_id = self.instance.id history_id = self.instance.execute( kind, str(data.pop(kind)), inventory, - initiator=user.id, **data + initiator=obj_id, initiator_type=init_type, executor=user, **data ) rdata = dict(detail="Started at inventory {}.".format(inventory), - history_id=history_id) + history_id=history_id, executor=user.id) return Response(rdata, 201) def execute_playbook(self, request): diff --git a/polemarch/api/v1/views.py b/polemarch/api/v1/views.py index d5ff1484..7769ba50 100644 --- a/polemarch/api/v1/views.py +++ b/polemarch/api/v1/views.py @@ -246,6 +246,7 @@ def types(self, request): class BulkViewSet(rest_views.APIView): serializer_classes = serializers + schema = None _op_types = { "get": "perform_get", @@ -266,7 +267,7 @@ class BulkViewSet(rest_views.APIView): def get_serializer_class(self, item): if item not in self._allowed_types: - raise excepts.UnsupportedMediaType(media_type=item) + raise excepts.UnsupportedMediaType(media_type=item) # nocv item = "One{}Serializer".format(item.title()) return getattr(self.serializer_classes, item) @@ -277,9 +278,11 @@ def get_serializer(self, *args, **kwargs): def get_object(self, item, pk, access="editable"): serializer_class = self.get_serializer_class(item) model = serializer_class.Meta.model - obj = model.objects.get(pk=pk) - if not getattr(obj, access + "_by")(self.request.user): - raise PermissionDenied("You don't have permission to this object.") + obj = model.objects.cleared().get(pk=pk) + if not getattr(obj.acl_handler, access + "_by")(self.request.user): + raise PermissionDenied( + "You don't have permission to this object." + ) # nocv return obj def perform_get(self, item, pk): diff --git a/polemarch/main/acl/__init__.py b/polemarch/main/acl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/polemarch/main/acl/handlers.py b/polemarch/main/acl/handlers.py new file mode 100644 index 00000000..23b6619e --- /dev/null +++ b/polemarch/main/acl/handlers.py @@ -0,0 +1,30 @@ + + +class Default(object): + # pylint: disable=unused-argument + qs_methods = [] + + def __init__(self, model=None, instance=None): + self.instance = instance + self.model = model + + def set_owner(self, user): # nocv + pass + + def owned_by(self, user): # nocv + return True + + def manageable_by(self, user): # nocv + return True + + def editable_by(self, user): # nocv + return True + + def viewable_by(self, user): + return True + + def user_filter(self, qs, user, only_leads=False): + return qs + + def qs_create(self, original_method, **kwargs): + return original_method(**kwargs) diff --git a/polemarch/main/hooks/http.py b/polemarch/main/hooks/http.py index 9ae13ea4..3a77c0ec 100644 --- a/polemarch/main/hooks/http.py +++ b/polemarch/main/hooks/http.py @@ -1,7 +1,12 @@ +import logging +import traceback import requests from .base import BaseHook +logger = logging.getLogger("polemarch") + + class Backend(BaseHook): def _execute(self, url, when, message): data = dict(type=when, payload=message) @@ -11,6 +16,10 @@ def _execute(self, url, when, message): response.status_code, response.reason, response.text ) except BaseException as err: + logger.error(traceback.format_exc()) + logger.error("Details:\nURL:{}\nWHEN:{}\n".format( + url, when + )) return str(err) def send(self, message, when): diff --git a/polemarch/main/hooks/script.py b/polemarch/main/hooks/script.py index 7900e55f..b311edaf 100644 --- a/polemarch/main/hooks/script.py +++ b/polemarch/main/hooks/script.py @@ -1,10 +1,10 @@ +from __future__ import unicode_literals import os import json import traceback import logging import subprocess from .base import BaseHook -from ..utils import tmp_file_context logger = logging.getLogger("polemarch") @@ -14,11 +14,17 @@ class Backend(BaseHook): def _execute(self, script, when, file): try: + work_dir = self.conf['HOOKS_DIR'] + script = '{}/{}'.format(work_dir, script) return subprocess.check_output( - [script, when, file.name], cwd=self.conf['HOOKS_DIR'] + [script, when], + cwd=work_dir, universal_newlines=True, input=file ) except BaseException as err: - logger.info(traceback.format_exc()) + logger.error(traceback.format_exc()) + logger.error("Details:\nSCRIPT:{}\nWHEN:{}\nCWD:{}\n".format( + script, when, self.conf['HOOKS_DIR'] + )) return str(err) def setup(self, **kwargs): @@ -34,9 +40,7 @@ def validate(self): def send(self, message, when): super(Backend, self).send(message, when) - with tmp_file_context() as file: - file.write(json.dumps(message)) - return "\n".join([ - self._execute(r, when, file) - for r in self.conf['recipients'] if r - ]) + return "\n".join([ + self._execute(r, when, json.dumps(message)) + for r in self.conf['recipients'] if r + ]) diff --git a/polemarch/main/middleware.py b/polemarch/main/middleware.py new file mode 100644 index 00000000..6bd0c1f4 --- /dev/null +++ b/polemarch/main/middleware.py @@ -0,0 +1,22 @@ +import logging +from django.conf import settings + + +logger = logging.getLogger("polemarch") + + +class BaseMiddleware(object): + def __init__(self, get_response): + self.get_response = get_response + super(BaseMiddleware, self).__init__() + + def __call__(self, request): + return self.get_response(request) + + +class PolemarchHeadersMiddleware(BaseMiddleware): + def __call__(self, request): + response = super(PolemarchHeadersMiddleware, self).__call__(request) + response['Polemarch-Version'] = getattr(settings, 'POLEMARCH_VERSION') + response['Polemarch-Timezone'] = getattr(settings, 'TIME_ZONE') + return response diff --git a/polemarch/main/migrations/0019_auto_20171025_0104.py b/polemarch/main/migrations/0019_auto_20171025_0104.py index a00b65ec..8d845b7e 100644 --- a/polemarch/main/migrations/0019_auto_20171025_0104.py +++ b/polemarch/main/migrations/0019_auto_20171025_0104.py @@ -4,7 +4,7 @@ from django.db import migrations, models from django.conf import settings import polemarch.main.models.users -import polemarch.main.models.acl_models +import polemarch.main.models.base class Migration(migrations.Migration): @@ -62,32 +62,32 @@ class Migration(migrations.Migration): migrations.AddField( model_name='group', name='owner', - field=models.ForeignKey(related_name='polemarch_group_set', default=polemarch.main.models.acl_models.first_staff_user, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(related_name='polemarch_group_set', default=polemarch.main.models.base.first_staff_user, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='host', name='owner', - field=models.ForeignKey(related_name='polemarch_host_set', default=polemarch.main.models.acl_models.first_staff_user, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(related_name='polemarch_host_set', default=polemarch.main.models.base.first_staff_user, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='inventory', name='owner', - field=models.ForeignKey(related_name='polemarch_inventory_set', default=polemarch.main.models.acl_models.first_staff_user, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(related_name='polemarch_inventory_set', default=polemarch.main.models.base.first_staff_user, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='periodictask', name='owner', - field=models.ForeignKey(related_name='polemarch_periodictask_set', default=polemarch.main.models.acl_models.first_staff_user, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(related_name='polemarch_periodictask_set', default=polemarch.main.models.base.first_staff_user, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='project', name='owner', - field=models.ForeignKey(related_name='polemarch_project_set', default=polemarch.main.models.acl_models.first_staff_user, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(related_name='polemarch_project_set', default=polemarch.main.models.base.first_staff_user, to=settings.AUTH_USER_MODEL), ), migrations.AddField( model_name='template', name='owner', - field=models.ForeignKey(related_name='polemarch_template_set', default=polemarch.main.models.acl_models.first_staff_user, to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(related_name='polemarch_template_set', default=polemarch.main.models.base.first_staff_user, to=settings.AUTH_USER_MODEL), ), migrations.AlterField( model_name='group', diff --git a/polemarch/main/migrations/0021_usergroup.py b/polemarch/main/migrations/0021_usergroup.py index fba79b05..245c55fd 100644 --- a/polemarch/main/migrations/0021_usergroup.py +++ b/polemarch/main/migrations/0021_usergroup.py @@ -6,7 +6,7 @@ from django.db import migrations, models import django.db.models.deletion import polemarch.main.models.users -import polemarch.main.models.acl_models +import polemarch.main.models.base class Migration(migrations.Migration): @@ -22,7 +22,7 @@ class Migration(migrations.Migration): name='UserGroup', fields=[ ('parent', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='auth.Group')), - ('owner', models.ForeignKey(default=polemarch.main.models.acl_models.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_usergroup_set', to=settings.AUTH_USER_MODEL)), + ('owner', models.ForeignKey(default=polemarch.main.models.base.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_usergroup_set', to=settings.AUTH_USER_MODEL)), ], options={ 'abstract': False, diff --git a/polemarch/main/migrations/0037_auto_20180403_0113.py b/polemarch/main/migrations/0037_auto_20180403_0113.py new file mode 100644 index 00000000..505ecefa --- /dev/null +++ b/polemarch/main/migrations/0037_auto_20180403_0113.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-04-03 01:13 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0036_merge_20180226_0050'), + ] + + operations = [ + migrations.AlterModelOptions( + name='history', + options={'ordering': ['-start_time']}, + ), + migrations.AddField( + model_name='aclpermission', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='group', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='history', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='historylines', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='hook', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='host', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='inventory', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='periodictask', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='project', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='task', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='template', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='usersettings', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='variable', + name='hidden', + field=models.BooleanField(default=False), + ), + ] diff --git a/polemarch/main/migrations/0038_auto_20180420_0836.py b/polemarch/main/migrations/0038_auto_20180420_0836.py new file mode 100644 index 00000000..8adfba2d --- /dev/null +++ b/polemarch/main/migrations/0038_auto_20180420_0836.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-04-20 08:36 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import polemarch.main.models.base + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0037_auto_20180403_0113'), + ] + + operations = [ + migrations.AlterField( + model_name='group', + name='owner', + field=models.ForeignKey(default=polemarch.main.models.base.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_group_set', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='host', + name='owner', + field=models.ForeignKey(default=polemarch.main.models.base.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_host_set', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='inventory', + name='owner', + field=models.ForeignKey(default=polemarch.main.models.base.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_inventory_set', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='periodictask', + name='owner', + field=models.ForeignKey(default=polemarch.main.models.base.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_periodictask_set', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='project', + name='owner', + field=models.ForeignKey(default=polemarch.main.models.base.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_project_set', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='template', + name='owner', + field=models.ForeignKey(default=polemarch.main.models.base.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_template_set', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/polemarch/main/migrations/0038_history_executor.py b/polemarch/main/migrations/0038_history_executor.py new file mode 100644 index 00000000..8a3878d3 --- /dev/null +++ b/polemarch/main/migrations/0038_history_executor.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-04-17 02:46 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('main', '0037_auto_20180403_0113'), + ] + + operations = [ + migrations.AddField( + model_name='history', + name='executor', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='history', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/polemarch/main/migrations/0039_auto_20180420_0908.py b/polemarch/main/migrations/0039_auto_20180420_0908.py new file mode 100644 index 00000000..5db3e700 --- /dev/null +++ b/polemarch/main/migrations/0039_auto_20180420_0908.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-04-20 09:08 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import polemarch.main.models.base + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0038_auto_20180420_0836'), + ] + + operations = [ + migrations.AddField( + model_name='usergroup', + name='hidden', + field=models.BooleanField(default=False), + ), + migrations.AlterField( + model_name='usergroup', + name='owner', + field=models.ForeignKey(default=polemarch.main.models.base.first_staff_user, on_delete=django.db.models.deletion.CASCADE, related_name='polemarch_usergroup_set', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/polemarch/main/migrations/0039_migrate_history.py b/polemarch/main/migrations/0039_migrate_history.py new file mode 100644 index 00000000..3186a734 --- /dev/null +++ b/polemarch/main/migrations/0039_migrate_history.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.6 on 2017-04-03 01:39 +from __future__ import unicode_literals +from django.db import migrations, models + + +def migrate_history(apps, schema_editor): + History = apps.get_registered_model('main', 'History') + User = apps.get_registered_model('auth', 'User') + for history in History.objects.filter(initiator_type="users"): + history.executor = User(pk=history.initiator) + history.initiator_type = "project" + history.initiator = history.project_id + history.save() + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('main', '0038_history_executor'), + ] + operations = [ + migrations.AlterField( + model_name='history', + name='initiator_type', + field=models.CharField(default='project', max_length=50), + ), + migrations.RunPython(migrate_history), + ] diff --git a/polemarch/main/migrations/0040_history_json_options.py b/polemarch/main/migrations/0040_history_json_options.py new file mode 100644 index 00000000..aca675a5 --- /dev/null +++ b/polemarch/main/migrations/0040_history_json_options.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-04-19 05:43 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0039_migrate_history'), + ] + + operations = [ + migrations.AddField( + model_name='history', + name='json_options', + field=models.TextField(default='{}'), + ), + ] diff --git a/polemarch/main/migrations/0041_auto_20180502_2355.py b/polemarch/main/migrations/0041_auto_20180502_2355.py new file mode 100644 index 00000000..bc3c489e --- /dev/null +++ b/polemarch/main/migrations/0041_auto_20180502_2355.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-05-02 23:55 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0040_history_json_options'), + ] + + operations = [ + migrations.AddField( + model_name='group', + name='notes', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='host', + name='notes', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='inventory', + name='notes', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='periodictask', + name='notes', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='project', + name='notes', + field=models.TextField(default=''), + ), + migrations.AddField( + model_name='template', + name='notes', + field=models.TextField(default=''), + ), + ] diff --git a/polemarch/main/migrations/0042_merge_20180504_0359.py b/polemarch/main/migrations/0042_merge_20180504_0359.py new file mode 100644 index 00000000..f8081d41 --- /dev/null +++ b/polemarch/main/migrations/0042_merge_20180504_0359.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-05-04 03:59 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0041_auto_20180502_2355'), + ('main', '0039_auto_20180420_0908'), + ] + + operations = [ + ] diff --git a/polemarch/main/migrations/0043_usergroup_notes.py b/polemarch/main/migrations/0043_usergroup_notes.py new file mode 100644 index 00000000..bc05d75f --- /dev/null +++ b/polemarch/main/migrations/0043_usergroup_notes.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.10 on 2018-05-04 04:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0042_merge_20180504_0359'), + ] + + operations = [ + migrations.AddField( + model_name='usergroup', + name='notes', + field=models.TextField(default=''), + ), + ] diff --git a/polemarch/main/models/__init__.py b/polemarch/main/models/__init__.py index 39535e91..6b7930aa 100644 --- a/polemarch/main/models/__init__.py +++ b/polemarch/main/models/__init__.py @@ -3,6 +3,7 @@ import os import sys import json +import logging from collections import OrderedDict import django_celery_beat from django_celery_beat.models import IntervalSchedule, CrontabSchedule @@ -20,7 +21,9 @@ from ..validators import RegexValidator from ..exceptions import UnknownTypeException from ..utils import raise_context, AnsibleArgumentsReference -from ..tasks import SendHook + + +logger = logging.getLogger('polemarch') ##################################### @@ -29,7 +32,8 @@ def send_hook(when, target): msg = OrderedDict(when=when) msg['target'] = target - SendHook.delay(when, msg) + logger.info('Sending {} hooks...'.format(when)) + Hook.objects.execute(when, msg) @raise_context() @@ -153,7 +157,7 @@ def save_to_beat(instance, **kwargs): manager = django_celery_beat.models.PeriodicTask.objects delete_from_beat(instance) if not instance.enabled: - return + return #nocv if instance.type == "INTERVAL": units = IntervalSchedule.SECONDS secs = instance.get_schedule() diff --git a/polemarch/main/models/acl.py b/polemarch/main/models/acl.py deleted file mode 100644 index 78945572..00000000 --- a/polemarch/main/models/acl.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.conf import settings -from ..utils import import_class - -ACLPermissionAbstract = import_class( - settings.ACL['DEFAULT_ACL_CLASSES']["ACLPermissionAbstract"] -) -ACLPermissionSubclass = import_class( - settings.ACL['DEFAULT_ACL_CLASSES']["ACLPermissionSubclass"] -) -ACLGroupSubclass = import_class( - settings.ACL['DEFAULT_ACL_CLASSES']["ACLGroupSubclass"] -) -ACLModel = import_class( - settings.ACL['DEFAULT_ACL_CLASSES']["ACLModel"] -) -ACLQuerySet = import_class( - settings.ACL['DEFAULT_ACL_CLASSES']["ACLQuerySet"] -) -ACLInventoriesQuerySet = import_class( - settings.ACL['DEFAULT_ACL_CLASSES']["ACLInventoriesQuerySet"] -) -ACLHistoryQuerySet = import_class( - settings.ACL['DEFAULT_ACL_CLASSES']["ACLHistoryQuerySet"] -) -ACLUserGroupsQuerySet = import_class( - settings.ACL['DEFAULT_ACL_CLASSES']["ACLUserGroupsQuerySet"] -) diff --git a/polemarch/main/models/acl_models.py b/polemarch/main/models/acl_models.py deleted file mode 100644 index ed9d18bf..00000000 --- a/polemarch/main/models/acl_models.py +++ /dev/null @@ -1,69 +0,0 @@ -# pylint: disable=unused-argument -from django.conf import settings -from django.contrib.auth.models import User as BaseUser -from .base import models, BModel, BQuerySet - - -def first_staff_user(): - return BaseUser.objects.filter(is_staff=True).first().id - - -class ACLPermissionQuerySet(BQuerySet): - use_for_related_fields = True - - -class ACLPermissionAbstract(BModel): - objects = ACLPermissionQuerySet.as_manager() - user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, null=True) - uagroup = models.ForeignKey('main.UserGroup', blank=True, null=True) - - class Meta: - abstract = True - - -class ACLQuerySet(BQuerySet): - use_for_related_fields = True - - def user_filter(self, user, only_leads=False): - return self - - -class ACLHistoryQuerySet(ACLQuerySet): - use_for_related_fields = True - - -class ACLPermissionSubclass(models.Model): - owner = models.ForeignKey(settings.AUTH_USER_MODEL, - default=first_staff_user, - related_name="polemarch_%(class)s_set") - acl = models.ManyToManyField("main.ACLPermission", - blank=True, null=True) - - class Meta: - abstract = True - - def set_owner(self, user): # nocv - pass - - def owned_by(self, user): # nocv - return True - - def manageable_by(self, user): # nocv - return True - - def editable_by(self, user): # nocv - return True - - def viewable_by(self, user): - return True - - -class ACLGroupSubclass(object): - pass - - -class ACLModel(BModel, ACLPermissionSubclass): - objects = ACLQuerySet.as_manager() - - class Meta: - abstract = True diff --git a/polemarch/main/models/base.py b/polemarch/main/models/base.py index 6b8a12b2..e9b775c3 100644 --- a/polemarch/main/models/base.py +++ b/polemarch/main/models/base.py @@ -1,16 +1,45 @@ from __future__ import unicode_literals from django.db import models +from django.conf import settings +from django.contrib.auth.models import User as BaseUser -from ...main.utils import Paginator +from ...main.utils import Paginator, classproperty, import_class -class BQuerySet(models.QuerySet): +def first_staff_user(): + return BaseUser.objects.filter(is_staff=True).first().id + - def paged(self, *args, **kwargs): # nocv +class BQuerySet(models.QuerySet): + use_for_related_fields = True + + def __decorator(self, func): # noce + def wrapper(*args, **kwargs): + return func(self, *args, **kwargs) + return wrapper + + def __getattribute__(self, item): + model = super(BQuerySet, self).__getattribute__("model") + if model and model.acl_handler and item in model.acl_handler.qs_methods: # noce + return self.__decorator(getattr(model.acl_handler, "qs_{}".format(item))) + return super(BQuerySet, self).__getattribute__(item) + + def create(self, **kwargs): + return self.model.acl_handler.qs_create( + super(BQuerySet, self).create, **kwargs + ) + + def cleared(self): + return ( + self.filter(hidden=False) if hasattr(self.model, "hidden") + else self + ) + + def paged(self, *args, **kwargs): return self.get_paginator(*args, **kwargs).items() - def get_paginator(self, *args, **kwargs): # nocv + def get_paginator(self, *args, **kwargs): return Paginator(self, *args, **kwargs) def _find(self, field_name, tp_name, *args, **kwargs): # nocv @@ -21,30 +50,50 @@ def _find(self, field_name, tp_name, *args, **kwargs): # nocv return getattr(self, tp_name)(**{field_name+"__in": field}) return getattr(self, tp_name)(**{field_name: field}) - -class BManager(models.Manager.from_queryset(BQuerySet)): - # pylint: disable=no-member - pass + def user_filter(self, user, only_leads=False): + # pylint: disable=unused-argument + return self.model.acl_handler.user_filter(self, user, only_leads=False) -class BModel(models.Model): - objects = BManager() - id = models.AutoField(primary_key=True, - max_length=20) +class BaseModel(models.Model): + objects = BQuerySet.as_manager() def __init__(self, *args, **kwargs): - super(BModel, self).__init__(*args, **kwargs) + super(BaseModel, self).__init__(*args, **kwargs) self.no_signal = False class Meta: abstract = True - def __unicode__(self): - return "<{}>".format(self.id) # nocv - def __str__(self): return self.__unicode__() + @staticmethod + def get_acl(cls, obj=None): + # pylint: disable=bad-staticmethod-argument + handler_class_name = settings.ACL['MODEL_HANDLERS'].get( + cls.__name__, settings.ACL['MODEL_HANDLERS'].get("Default") + ) + return import_class(handler_class_name)(cls, obj) + + @classproperty + def acl_handler(self): + classObj = self.__class__ + if isinstance(self, BaseModel): + return classObj.get_acl(classObj, self) + return self.get_acl(self) + + +class BModel(BaseModel): + id = models.AutoField(primary_key=True, max_length=20) + hidden = models.BooleanField(default=False) + + class Meta: + abstract = True + + def __unicode__(self): + return "<{}>".format(self.id) # nocv + class BGroupedModel(BModel): parent = models.ForeignKey('self', blank=True, null=True) @@ -78,3 +127,16 @@ class ManyToManyFieldACLReverse(models.ManyToManyField, class ForeignKeyACLReverse(models.ForeignKey, ReverseAccessExtendsFieldMixin): pass + + +class ACLModel(BModel): + owner = models.ForeignKey(settings.AUTH_USER_MODEL, + on_delete=None, + default=first_staff_user, + related_name="polemarch_%(class)s_set") + acl = models.ManyToManyField("main.ACLPermission", + blank=True, null=True) + notes = models.TextField(default="") + + class Meta: + abstract = True diff --git a/polemarch/main/models/hosts.py b/polemarch/main/models/hosts.py index 9c9d7620..ca7b241c 100644 --- a/polemarch/main/models/hosts.py +++ b/polemarch/main/models/hosts.py @@ -5,9 +5,8 @@ from django.db import transaction from django.db.models import Q -from .acl import ACLInventoriesQuerySet -from .base import BManager, models +from .base import models from .base import ManyToManyFieldACL, ManyToManyFieldACLReverse from .vars import AbstractModel, AbstractVarsQuerySet from ...main import exceptions as ex @@ -45,7 +44,7 @@ class HostQuerySet(AbstractVarsQuerySet): class Host(AbstractModel): - objects = BManager.from_queryset(HostQuerySet)() + objects = HostQuerySet.as_manager() type = models.CharField(max_length=5, default="HOST") @@ -109,7 +108,7 @@ def get_parents(self): class Group(AbstractModel): CiclicDependencyError = CiclicDependencyError - objects = BManager.from_queryset(GroupQuerySet)() + objects = GroupQuerySet.as_manager() hosts = ManyToManyFieldACL(Host, related_query_name="groups") parents = ManyToManyFieldACLReverse('Group', blank=True, null=True, related_query_name="childrens") @@ -138,12 +137,7 @@ def toString(self, var_sep="\n"): return get_render("models/group", data), keys -class InventoriesQuerySet(AbstractVarsQuerySet, ACLInventoriesQuerySet): - pass - - class Inventory(AbstractModel): - objects = InventoriesQuerySet.as_manager() hosts = ManyToManyFieldACL(Host) groups = ManyToManyFieldACL(Group) diff --git a/polemarch/main/models/projects.py b/polemarch/main/models/projects.py index b63c2422..503613e1 100644 --- a/polemarch/main/models/projects.py +++ b/polemarch/main/models/projects.py @@ -3,8 +3,6 @@ import os import logging -from collections import OrderedDict - import six from django.conf import settings from django.utils import timezone @@ -12,11 +10,11 @@ from .. import utils from . import hosts as hosts_models -from .vars import AbstractModel, AbstractVarsQuerySet, BManager, models +from .vars import AbstractModel, AbstractVarsQuerySet, models from ..exceptions import PMException from ..utils import ModelHandlers from .base import ManyToManyFieldACL -from ..tasks import SendHook +from .hooks import Hook logger = logging.getLogger("polemarch") @@ -24,6 +22,7 @@ class ProjectQuerySet(AbstractVarsQuerySet): + use_for_related_fields = True handlers = ModelHandlers("REPO_BACKENDS", "'repo_type' variable needed!") task_handlers = ModelHandlers("TASKS_HANDLERS", "Unknown execution type!") @@ -34,7 +33,7 @@ def create(self, **kwargs): class Project(AbstractModel): - objects = BManager.from_queryset(ProjectQuerySet)() + objects = ProjectQuerySet.as_manager() handlers = objects._queryset_class.handlers task_handlers = objects._queryset_class.task_handlers repository = models.CharField(max_length=2*1024) @@ -49,10 +48,25 @@ class Project(AbstractModel): class Meta: default_related_name = "projects" + class SyncError(Exception): + pass + HIDDEN_VARS = [ 'repo_password', ] + BOOLEAN_VARS = [ + 'repo_sync_on_run' + ] + + EXTRA_OPTIONS = { + 'initiator': 0, + 'initiator_type': 'project', + 'executor': None, + 'save_result': True, + 'template_option': None + } + def __unicode__(self): return str(self.name) # pragma: no cover @@ -76,20 +90,25 @@ def type(self): return self.variables.get(key="repo_type").value def _get_history(self, kind, mod_name, inventory, **extra): - initiator = extra.pop("initiator", 0) - initiator_type = extra.pop("initiator_type", "users") - save_result = extra.pop("save_result", True) + extra_options = dict() + for option in self.EXTRA_OPTIONS: + extra_options[option] = extra.pop(option, self.EXTRA_OPTIONS[option]) + options = dict() + if extra_options['template_option'] is not None: + options['template_option'] = extra_options['template_option'] command = kind.lower() ansible_args = dict(extra) utils.AnsibleArgumentsReference().validate_args(command, ansible_args) - if not save_result: + if not extra_options['save_result']: return None, extra from .tasks import History history_kwargs = dict( mode=mod_name, start_time=timezone.now(), inventory=inventory, project=self, kind=kind, raw_stdout="", execute_args=extra, - initiator=initiator, initiator_type=initiator_type, + initiator=extra_options['initiator'], + initiator_type=extra_options['initiator_type'], + executor=extra_options['executor'], hidden=self.hidden, options=options ) if isinstance(inventory, (six.string_types, six.text_type)): history_kwargs['inventory'] = None @@ -115,21 +134,16 @@ def _prepare_kw(self, kind, mod_name, inventory, **extra): kwargs.update(extra) return kwargs - def _send_hook(self, when, kind, kwargs): - msg = OrderedDict(execution_type=kind, when=when) - inventory = kwargs['inventory'] - if isinstance(inventory, hosts_models.Inventory): - inventory = inventory.get_hook_data(when) - msg['target'] = OrderedDict( - name=kwargs['target'], - inventory=inventory, - project=kwargs['project'].get_hook_data(when) - ) - if kwargs['history'] is not None: - msg['history'] = kwargs['history'].get_hook_data(when) - else: - msg['history'] = None - SendHook.delay(when, msg) + def hook(self, when, msg): + Hook.objects.execute(when, msg) + + def sync_on_execution_handler(self, history): + if not self.vars.get('repo_sync_on_run', False): + return + try: + self.sync() + except Exception as exc: + raise self.SyncError("ERROR on Sync operation: " + str(exc)) def execute(self, kind, *args, **extra): kind = kind.upper() @@ -139,9 +153,7 @@ def execute(self, kind, *args, **extra): kwargs = self._prepare_kw(kind, *args, **extra) history = kwargs['history'] if sync: - self._send_hook('on_execution', kind, kwargs) task_class(**kwargs) - self._send_hook('after_execution', kind, kwargs) else: task_class.delay(**kwargs) return history.id if history is not None else history diff --git a/polemarch/main/models/tasks.py b/polemarch/main/models/tasks.py index b6aed254..9ad16695 100644 --- a/polemarch/main/models/tasks.py +++ b/polemarch/main/models/tasks.py @@ -25,10 +25,10 @@ from ..utils import AnsibleArgumentsReference from . import Inventory from ..exceptions import DataNotReady, NotApplicable -from .base import BModel, BQuerySet, models +from .base import BModel, ACLModel, BQuerySet, models from .vars import AbstractModel, AbstractVarsQuerySet from .projects import Project -from .acl import ACLModel, ACLHistoryQuerySet + logger = logging.getLogger("polemarch") @@ -36,9 +36,6 @@ class TaskFilterQuerySet(BQuerySet): use_for_related_fields = True - def user_filter(self, user): - return self.filter(project__in=Project.objects.all().user_filter(user)) - # Block of real models class Task(BModel): @@ -54,9 +51,6 @@ class Meta: def __unicode__(self): return str(self.name) # nocv - def viewable_by(self, user): - return self.project.viewable_by(user) - class PeriodicTaskQuerySet(TaskFilterQuerySet, AbstractVarsQuerySet): use_for_related_fields = True @@ -151,12 +145,6 @@ def execute(self, sync=True): **self.vars ) - def editable_by(self, user): - return self.project.editable_by(user) - - def viewable_by(self, user): - return self.project.viewable_by(user) - class Template(ACLModel): name = models.CharField(max_length=512) @@ -219,7 +207,10 @@ def execute(self, serializer, user, option=None): vars.update(option_vars) data.update(option_data) data.update(vars) - return serializer._execution(tp, data, user) + return serializer._execution( + tp, data, user, + template=self.id, template_option=option + ) def _convert_to_data(self, value): if isinstance(value, (six.string_types, six.text_type)): @@ -276,12 +267,6 @@ def set_data(self, value): data['vars'] = self.keep_encrypted_data(data['vars']) self.template_data = json.dumps(data) - def __setattr__(self, key, value): - if key == "data": - self.set_data(value) - else: - super(Template, self).__setattr__(key, value) - @property def data(self): return self.get_data() @@ -313,7 +298,7 @@ def options_list(self): return list(self.options.keys()) -class HistoryQuerySet(ACLHistoryQuerySet): +class HistoryQuerySet(BQuerySet): use_for_related_fields = True def create(self, **kwargs): @@ -370,7 +355,9 @@ class History(BModel): status = models.CharField(max_length=50) initiator = models.IntegerField(default=0) # Initiator type should be always as in urls for api - initiator_type = models.CharField(max_length=50, default="users") + initiator_type = models.CharField(max_length=50, default="project") + executor = models.ForeignKey(User, blank=True, null=True, default=None) + json_options = models.TextField(default="{}") def __init__(self, *args, **kwargs): execute_args = kwargs.pop('execute_args', None) @@ -401,14 +388,20 @@ def get_hook_data(self, when): initiator_type=self.initiator_type, initiator_id=self.initiator, ) - if self.initiator_type == "users": - data["initiator"]['name'] = getattr( - self.initiator_object, 'username', None - ) - elif self.initiator_type == "scheduler": + if self.initiator_type in ["template", "scheduler"]: data["initiator"]['name'] = self.initiator_object.name return data + def _get_seconds_from_time(self, value): + return int(value.total_seconds()) + + @property + def execution_time(self): + if self.stop_time is None: + return self._get_seconds_from_time(now() - self.start_time) + else: + return self._get_seconds_from_time(self.stop_time - self.start_time) + @property def execute_args(self): return json.loads(self.json_args) @@ -416,19 +409,32 @@ def execute_args(self): @execute_args.setter def execute_args(self, value): if not isinstance(value, dict): - raise ValidationError(dict(args="Should be a list.")) + raise ValidationError(dict(args="Should be a dict.")) data = {k: v for k, v in value.items() if k not in ['group']} for key in data.keys(): if key in PeriodicTask.HIDDEN_VARS: data[key] = "[~~ENCRYPTED~~]" self.json_args = json.dumps(data) + # options + @property + def options(self): + return json.loads(self.json_options) + + @options.setter + def options(self, value): + if not isinstance(value, dict): + raise ValidationError(dict(args="Should be a dict.")) + self.json_options = json.dumps(value) + @property def initiator_object(self): - if self.initiator_type == "users" and self.initiator: - return User.objects.get(id=self.initiator) + if self.initiator_type == "project" and self.initiator: + return self elif self.initiator_type == "scheduler" and self.initiator: return PeriodicTask.objects.get(id=self.initiator) + elif self.initiator_type == "template" and self.initiator: + return Template.objects.get(id=self.initiator) else: return None @@ -476,25 +482,6 @@ def write_line(self, value, number): # nocv history=self, line_number=number, line=value ) - def editable_by(self, user): - if self.inventory is None: - return self.project.editable_by(user) - return self.inventory.editable_by(user) - - def _inventory_editable(self, user): - return self.inventory and self.inventory.editable_by(user) - - def _inventory_viewable(self, user): - return not self.inventory or self.inventory.viewable_by(user) - - def viewable_by(self, user): - return ( - self.project.editable_by(user) or - self._inventory_editable(user) or - (self.initiator == user.id and self.initiator_type == "users") or - (self.project.viewable_by(user) & self._inventory_viewable(user)) - ) - class HistoryLines(BModel): line = models.TextField(default="") diff --git a/polemarch/main/models/users.py b/polemarch/main/models/users.py index 69cd7a32..1e7d6589 100644 --- a/polemarch/main/models/users.py +++ b/polemarch/main/models/users.py @@ -6,23 +6,49 @@ from django.contrib.auth.models import Group as BaseGroup from django.contrib.auth.models import User as BaseUser -from .base import models, BModel -from . import acl +from .base import settings, models, BModel, ACLModel, BQuerySet logger = logging.getLogger("polemarch") -class ACLPermission(acl.ACLPermissionAbstract): +class ACLPermission(BModel): + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=None, + blank=True, null=True) + uagroup = models.ForeignKey('main.UserGroup', on_delete=None, blank=True, + null=True) role = models.CharField(max_length=10) + @property + def member(self): # noce + # pylint: disable=no-member + if self.user is not None: + return self.user.id + else: + return self.uagroup.id + + @member.setter + def member(self, value): # noce + pass + + @property + def member_type(self): # noce + if self.user is not None: + return "user" + else: + return "team" + + @member_type.setter + def member_type(self, value): # noce + pass + -class UserGroup(BaseGroup, acl.ACLGroupSubclass, acl.ACLPermissionSubclass): - objects = acl.ACLUserGroupsQuerySet.as_manager() - parent = models.OneToOneField(BaseGroup, parent_link=True) +class UserGroup(BaseGroup, ACLModel): + objects = BQuerySet.as_manager() + parent = models.OneToOneField(BaseGroup, on_delete=None, parent_link=True) users = BaseGroup.user_set - def __unicode__(self): + def __unicode__(self): # nocv return super(UserGroup, self).__unicode__() @property @@ -36,6 +62,7 @@ def users_list(self, value): class UserSettings(BModel): user = models.OneToOneField(BaseUser, + on_delete=None, related_query_name="settings", related_name="settings") settings = models.TextField(default="{}") diff --git a/polemarch/main/models/utils.py b/polemarch/main/models/utils.py index b15d0e3c..38fa17df 100644 --- a/polemarch/main/models/utils.py +++ b/polemarch/main/models/utils.py @@ -4,8 +4,9 @@ import re import sys import logging +import traceback from os.path import dirname -from collections import namedtuple +from collections import namedtuple, OrderedDict import six from django.utils import timezone @@ -24,8 +25,9 @@ # Classes and methods for support class DummyHistory(object): + # pylint: disable=unused-argument def __init__(self, *args, **kwargs): - pass + self.mode = kwargs.get('mode', None) def __setattr__(self, key, value): pass @@ -41,6 +43,9 @@ def raw_stdout(self): def raw_stdout(self, value): logger.info(value) # nocv + def get_hook_data(self, when): + return None + def write_line(self, value, number): # pylint: disable=unused-argument logger.info(value) # nocv @@ -127,6 +132,7 @@ def close(self): def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs + self.__will_raise_exception = False def __generate_arg_file(self, value): file = tmp_file(value) @@ -183,10 +189,25 @@ def prepare(self, target, inventory, history, project): self.inventory_object.raw ) self.history.status = "RUN" + self.project.sync_on_execution_handler(self.history) self.history.revision = project.revision self.history.save() self.executor = Executor(self.history) + def _send_hook(self, when): + msg = OrderedDict(execution_type=self.history.kind, when=when) + inventory = self.history.inventory + if isinstance(inventory, Inventory): + inventory = inventory.get_hook_data(when) + msg['target'] = OrderedDict( + name=self.history.mode, + inventory=inventory, + project=self.project.get_hook_data(when) + ) + msg['history'] = self.history.get_hook_data(when) + logger.info("Sending execution hooks...") + self.project.hook(when, msg) + def get_args(self, target, extra_args): return [self.path_to_ansible, target, '-i', self.inventory_object.file_name, '-v'] + extra_args @@ -197,27 +218,37 @@ def error_handler(self, exception): self.history.raw_stdout = "{}".format(exception.output) self.history.status = self.status_codes.get(exception.returncode, default_code) - else: - self.history.raw_stdout = self.history.raw_stdout + str(exception) - self.history.status = default_code + return + elif isinstance(exception, self.project.SyncError): + self.__will_raise_exception = True + self.history.raw_stdout = self.history.raw_stdout + str(exception) + self.history.status = default_code def execute(self, target, inventory, history, project, **extra_args): try: self.prepare(target, inventory, history, project) + self._send_hook('on_execution') self.history.status = "OK" extra = self.__parse_extra_args(**extra_args) args = self.get_args(self.target, extra.args) self.history.raw_stdout = self.executor.execute(args, self.workdir) except Exception as exception: self.error_handler(exception) + if self.__will_raise_exception: + raise finally: inventory_object = getattr(self, "inventory_object", None) inventory_object and inventory_object.close() self.history.stop_time = timezone.now() self.history.save() + self._send_hook('after_execution') def run(self): - return self.execute(*self.args, **self.kwargs) + try: + return self.execute(*self.args, **self.kwargs) + except Exception: # nocv + logger.error(traceback.format_exc()) + raise class AnsiblePlaybook(AnsibleCommand): diff --git a/polemarch/main/models/vars.py b/polemarch/main/models/vars.py index 20795eaa..5e56cc72 100644 --- a/polemarch/main/models/vars.py +++ b/polemarch/main/models/vars.py @@ -14,8 +14,7 @@ GenericRelation) from ..utils import tmp_file -from .base import BModel, BManager, models -from .acl import ACLModel, ACLQuerySet +from .base import ACLModel, BQuerySet, BModel, models logger = logging.getLogger("polemarch") @@ -32,7 +31,7 @@ def __unicode__(self): # pragma: no cover return "{}={}".format(self.key, self.value) -class AbstractVarsQuerySet(ACLQuerySet): +class AbstractVarsQuerySet(BQuerySet): use_for_related_fields = True @transaction.atomic @@ -52,7 +51,7 @@ def var_filter(self, **kwargs): class AbstractModel(ACLModel): - objects = BManager.from_queryset(AbstractVarsQuerySet) + objects = AbstractVarsQuerySet.as_manager() name = models.CharField(max_length=512, default=uuid.uuid1) variables = GenericRelation(Variable, related_query_name="variables", @@ -67,6 +66,8 @@ class Meta: 'ansible_become_pass', ] + BOOLEAN_VARS = [] + def __unicode__(self): # pragma: no cover _vars = " ".join(["{}={}".format(k, v) for k, v in self.vars.items()]) @@ -102,7 +103,13 @@ def get_vars(self): output_field=models.IntegerField(), ), ).order_by("name_sorter", "key") - return OrderedDict(qs.values_list('key', 'value')) + vars_dict = OrderedDict(qs.values_list('key', 'value')) + for bool_var in self.BOOLEAN_VARS: + value = vars_dict.get(bool_var, None) + if value is None: + continue + vars_dict[bool_var] = True if value == "True" else False + return vars_dict def get_generated_vars(self): tmp = None diff --git a/polemarch/main/repo/_base.py b/polemarch/main/repo/_base.py index 0f95f282..8ecfc94d 100644 --- a/polemarch/main/repo/_base.py +++ b/polemarch/main/repo/_base.py @@ -27,7 +27,9 @@ def _set_tasks_list(self, playbooks_names): self.proj.tasks.all().delete() for playbook in playbooks_names: name = playbook.split(".yml")[0] - self.proj.tasks.create(name=name, playbook=playbook) + self.proj.tasks.create( + name=name, playbook=playbook, hidden=self.proj.hidden + ) def _update_tasks(self, files): reg = re.compile(self.regex) diff --git a/polemarch/main/repo/vcs.py b/polemarch/main/repo/vcs.py index 6358aff0..3217a20e 100644 --- a/polemarch/main/repo/vcs.py +++ b/polemarch/main/repo/vcs.py @@ -5,7 +5,7 @@ from ..utils import tmp_file_context, raise_context -class _VCS(_Base): +class _VCS(_Base): # nocv def vsc_clone(self, *args, **kwargs): raise NotImplementedError() @@ -110,7 +110,7 @@ def _operate(self, operation, **env_vars): if self.proj.vars.get("repo_password", None) is not None: env_vars = self._with_password(tmp, env_vars) elif self.proj.vars.get("repo_key", None) is not None: - env_vars = self._with_password(tmp, env_vars) + env_vars = self._with_key(tmp, env_vars) return super(Git, self)._operate(operation, **env_vars) def _get_files(self, repo=None): diff --git a/polemarch/main/settings.py b/polemarch/main/settings.py index d165b637..e0ba944e 100644 --- a/polemarch/main/settings.py +++ b/polemarch/main/settings.py @@ -100,6 +100,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'polemarch.main.middleware.PolemarchHeadersMiddleware', ] # Fix for django 1.8-9 MIDDLEWARE_CLASSES = MIDDLEWARE @@ -140,7 +141,7 @@ try: int_values_types = ["timeout", "connect_timeout", "read_timeout", "write_timeout"] for k, v in config.items('database.options'): - if k in int_values_types: + if k in int_values_types: #nocv __DB_OPTIONS[k] = int(float(v)) continue __DB_OPTIONS[k] = v.format(**__kwargs) # nocv @@ -215,7 +216,7 @@ ), 'EXCEPTION_HANDLER': 'polemarch.api.handlers.polemarch_exception_handler', 'DEFAULT_FILTER_BACKENDS': ( - 'rest_framework.filters.DjangoFilterBackend', + 'django_filters.rest_framework.DjangoFilterBackend', 'rest_framework.filters.OrderingFilter', ), 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', @@ -391,15 +392,8 @@ } ACL = { - "DEFAULT_ACL_CLASSES": { - "ACLPermissionAbstract": "polemarch.main.models.acl_models.ACLPermissionAbstract", - "ACLModel": "polemarch.main.models.acl_models.ACLModel", - "ACLPermissionSubclass": "polemarch.main.models.acl_models.ACLPermissionSubclass", - "ACLGroupSubclass": "polemarch.main.models.acl_models.ACLGroupSubclass", - "ACLQuerySet": "polemarch.main.models.acl_models.ACLQuerySet", - "ACLInventoriesQuerySet": "django.db.models.query.QuerySet", - "ACLHistoryQuerySet": "polemarch.main.models.acl_models.ACLHistoryQuerySet", - "ACLUserGroupsQuerySet": "polemarch.main.models.acl_models.ACLQuerySet", + "MODEL_HANDLERS": { + "Default": "polemarch.main.acl.handlers.Default" } } diff --git a/polemarch/main/tasks/tasks.py b/polemarch/main/tasks/tasks.py index 57cf12d3..0b843ebc 100644 --- a/polemarch/main/tasks/tasks.py +++ b/polemarch/main/tasks/tasks.py @@ -5,7 +5,6 @@ from ..utils import task, BaseTask from .exceptions import TaskError from ..models.utils import AnsibleModule, AnsiblePlaybook -from ..models.hooks import Hook logger = logging.getLogger("polemarch") @@ -13,7 +12,7 @@ @task(app, ignore_result=True, default_retry_delay=1, max_retries=5, bind=True) class RepoTask(BaseTask): - accepted_oprations = ["clone", "sync"] + accepted_operations = ["clone", "sync"] class RepoTaskError(TaskError): pass @@ -24,7 +23,7 @@ class UnknownRepoOperation(RepoTaskError): def __init__(self, app, project, operation="sync", *args, **kwargs): super(self.__class__, self).__init__(app, *args, **kwargs) self.project, self.operation = project, operation - if self.operation not in self.accepted_oprations: + if self.operation not in self.accepted_operations: raise self.task_class.UnknownRepoOperation(self.operation) def run(self): @@ -44,10 +43,9 @@ def __init__(self, app, job_id, *args, **kwargs): def run(self): from ..models import PeriodicTask try: - task = PeriodicTask.objects.get(id=self.job_id) + PeriodicTask.objects.get(id=self.job_id).execute() except PeriodicTask.DoesNotExist: return - task.execute() class _ExecuteAnsible(BaseTask): @@ -67,14 +65,3 @@ class ExecuteAnsiblePlaybook(_ExecuteAnsible): @task(app, ignore_result=True, bind=True) class ExecuteAnsibleModule(_ExecuteAnsible): ansible_class = AnsibleModule - - -@task(app, ignore_result=True, bind=True) -class SendHook(BaseTask): - def __init__(self, app, when, message, *args, **kwargs): - super(self.__class__, self).__init__(app, *args, **kwargs) - self.when = when - self.message = message - - def run(self): - Hook.objects.execute(self.when, self.message) diff --git a/polemarch/main/templates/base.html b/polemarch/main/templates/base.html index 3b4c88cb..e2db0520 100644 --- a/polemarch/main/templates/base.html +++ b/polemarch/main/templates/base.html @@ -17,7 +17,7 @@ {% block pmInlineScripts %} - + + - + - + - + {% endblock %} diff --git a/polemarch/main/templates/gui/app-gui.html b/polemarch/main/templates/gui/app-gui.html new file mode 100644 index 00000000..80b7e9db --- /dev/null +++ b/polemarch/main/templates/gui/app-gui.html @@ -0,0 +1,489 @@ +{% load staticfiles %} + + + + +{% block pmInlineScripts %} + + + + +
+ +

+ Polemarch +

+ + +
+
+ +
+
+ + +
+
+
+
+ + + +{% endblock %} diff --git a/polemarch/main/templates/gui/gui.html b/polemarch/main/templates/gui/gui.html index e2c09391..1d60caf3 100644 --- a/polemarch/main/templates/gui/gui.html +++ b/polemarch/main/templates/gui/gui.html @@ -40,7 +40,8 @@ - + + {% endblock %} {% block script %} @@ -65,7 +66,8 @@ 'templates/pmGroups', 'templates/pmItems', //'templates/pmHosts', - //'templates/pmUsers' + 'templates/pmUsers', + 'templates/pmHooks' ] // alert("New-") diff --git a/polemarch/main/templates/left_menu.html b/polemarch/main/templates/left_menu.html index b09807df..027c3261 100644 --- a/polemarch/main/templates/left_menu.html +++ b/polemarch/main/templates/left_menu.html @@ -1,79 +1,42 @@
  • MAIN NAVIGATION
  • -
  • Home
  • -
  • Projects
  • -
  • Templates
  • -
  • History
  • -
  • Hosts
  • -
  • Groups
  • -
  • Inventories
  • -
  • Users
  • -
  • Documentation
  • - - \ No newline at end of file + + diff --git a/polemarch/main/templates/menu.html b/polemarch/main/templates/menu.html index 78ecea20..b4f10000 100644 --- a/polemarch/main/templates/menu.html +++ b/polemarch/main/templates/menu.html @@ -11,9 +11,9 @@ {% endif %} {% if user.is_authenticated %} -
  • {{user.username}}
  • +
  • {{user.username}}
  • Logout
  • {% else %} -
  • Login
  • +
  • Login
  • {% endif %} \ No newline at end of file diff --git a/polemarch/main/tests/_base.py b/polemarch/main/tests/_base.py index adb57ec7..1fe23667 100644 --- a/polemarch/main/tests/_base.py +++ b/polemarch/main/tests/_base.py @@ -80,7 +80,7 @@ def result(self, request, url, code=200, *args, **kwargs): if (response.status_code != 404 and getattr(response, "rendered_content", False)) \ else str(response.content.decode('utf-8')) - except ValueError: + except ValueError: # nocv return None def assertCount(self, list, count): @@ -93,12 +93,13 @@ def assertRCode(self, resp, code=200): :param code: - expected code :return: None ''' - err_msg = "{} != {}\n{}".format( + err_msg = "{} != {}\n{}\n{}".format( resp.status_code, code, resp.rendered_content.decode() if (resp.status_code != 404 and getattr(resp, "rendered_content", False)) - else resp.content + else resp.content, + self.user ) self.assertEqual(resp.status_code, code, err_msg) diff --git a/polemarch/main/tests/api.py b/polemarch/main/tests/api.py index 0f395e90..594f0f4d 100644 --- a/polemarch/main/tests/api.py +++ b/polemarch/main/tests/api.py @@ -6,7 +6,7 @@ try: from mock import patch -except ImportError: +except ImportError: # nocv from unittest.mock import patch from ..utils import redirect_stdany @@ -21,7 +21,6 @@ ApiHistoryTestCase) from .ansible import ApiAnsibleTestCase from .repo_backends import RepoBackendsTestCase -from ..models import UserGroup, History class ApiUsersTestCase(BaseTestCase): @@ -248,9 +247,11 @@ def test_api_groups(self): url = '/api/v1/teams/' range_groups = 10 for i in range(range_groups): - UserGroup.objects.create(name="test_group_{}".format(i)) + self.get_model_class('UserGroup').objects.create( + name="test_group_{}".format(i) + ) self.list_test(url, range_groups) - ug = UserGroup.objects.all().last() + ug = self.get_model_class('UserGroup').objects.all().last() self.details_test( url + "{}/".format(ug.id), name=ug.name, id=ug.id @@ -338,11 +339,13 @@ def _generate_history(self, days_ago, count, status="OK"): start_time = now() - timedelta(days=days_ago, hours=1) stop_time = now() - timedelta(days=days_ago) for i in range(count): - History.objects.create(start_time=start_time, stop_time=stop_time, - status=status, **default_kwargs) + self.get_model_class('History').objects.create(start_time=start_time, + stop_time=stop_time, + status=status, + **default_kwargs) def _prepare_statisic(self): - History.objects.all().delete() + self.get_model_class('History').objects.all().delete() self._generate_history(1, 10, 'OK') self._generate_history(1, 3, 'ERROR') self._generate_history(1, 2, 'STOP') diff --git a/polemarch/main/tests/bulk.py b/polemarch/main/tests/bulk.py index aa48bd04..7fda3d3b 100644 --- a/polemarch/main/tests/bulk.py +++ b/polemarch/main/tests/bulk.py @@ -2,7 +2,6 @@ from datetime import timedelta from django.utils.timezone import now from .inventory import _ApiGHBaseTestCase -from .. import models class ApiBulkTestCase(_ApiGHBaseTestCase): @@ -60,14 +59,14 @@ def abstract_test_bulk(self, single_data, new_single_data, url, self.assertEquals(current[key], details[key]) def test_bulk_hosts(self): - models.Host.objects.all().delete() + self.get_model_class('Host').objects.all().delete() data = dict(name="host", type="HOST") new = dict(name="host[1:3]", type="RANGE") self.abstract_test_bulk(data, new, "/api/v1/hosts/", "host") def test_bulk_groups(self): - models.Group.objects.all().delete() - models.Host.objects.all().delete() + self.get_model_class('Group').objects.all().delete() + self.get_model_class('Host').objects.all().delete() data = dict(name="group", children=True) new = dict(name="new_group", children=False) raised = False @@ -77,12 +76,12 @@ def test_bulk_groups(self): raised = True self.assertTrue(raised) new.pop("children") - models.Group.objects.all().delete() + self.get_model_class('Group').objects.all().delete() self.abstract_test_bulk(data, new, "/api/v1/groups/", "group") # Bulk add hosts - group1 = models.Group.objects.create(name="test1") - group2 = models.Group.objects.create(name="test2") + group1 = self.get_model_class('Group').objects.create(name="test1") + group2 = self.get_model_class('Group').objects.create(name="test2") hdata = dict(name="host", type="HOST") types = dict(hosts=self.mass_create("/api/v1/hosts/", data=[hdata])) self.abstract_test_bulk_mod([group1, group2], types, "group") @@ -90,19 +89,19 @@ def test_bulk_groups(self): self.assertCount(group2.hosts.all(), 1) def test_bulk_inventories(self): - models.Inventory.objects.all().delete() + self.get_model_class('Inventory').objects.all().delete() data = dict(name="inventory") new = dict(name="new_inventory") self.abstract_test_bulk(data, new, "/api/v1/inventories/", "inventory") def test_bulk_projects(self): - models.Project.objects.all().delete() + self.get_model_class('Project').objects.all().delete() data = dict(name="proj", repository="rep", vars=dict(repo_type="TEST")) new = dict(name="new_project") self.abstract_test_bulk(data, new, "/api/v1/projects/", "project") def test_bulk_periodictasks(self): - models.PeriodicTask.objects.all().delete() + self.get_model_class('PeriodicTask').objects.all().delete() data = dict(name="periodic-task", project=self.prj1.id, type="INTERVAL", schedule="10", inventory=str(self.inv2.id), mode="ok.yml") @@ -112,7 +111,7 @@ def test_bulk_periodictasks(self): ) def test_bulk_templates(self): - models.Template.objects.all().delete() + self.get_model_class('Template').objects.all().delete() data = dict( name="test_tmplt", kind="Task", @@ -133,7 +132,7 @@ def test_bulk_templates(self): ) def test_bulk_history(self): - models.History.objects.all().delete() + self.get_model_class('History').objects.all().delete() repo = "git@ex.us:dir/rep3.git" ph = self.get_model_filter("Project").create( name="Prj_History", repository=repo, vars=dict(repo_type="TEST") @@ -157,7 +156,7 @@ def test_bulk_history(self): self.assertEqual(result[0]['type'], 'get') self.assertEqual(result[1]['status'], 200) self.assertEqual(result[1]['data']['detail'], "Ok") - self.assertCount(models.History.objects.all(), 0) + self.assertCount(self.get_model_class('History').objects.all(), 0) bulk_data = [ {'type': "mod", 'item': "history", 'pk': h.id}, ] diff --git a/polemarch/main/tests/commands.py b/polemarch/main/tests/commands.py index d394a9b1..ef4c008c 100644 --- a/polemarch/main/tests/commands.py +++ b/polemarch/main/tests/commands.py @@ -18,8 +18,7 @@ def test_log_level_command_argument_exist(self): self.assertIn("Unknown level", str(e)) def test_version_output(self): - command = ServiceCommand() - vstr = command.get_version() + vstr = ServiceCommand().get_version() self.assertIn("Polemarch", vstr) self.assertIn(__version__, vstr) diff --git a/polemarch/main/tests/inventory.py b/polemarch/main/tests/inventory.py index 48d807e1..12c67a73 100644 --- a/polemarch/main/tests/inventory.py +++ b/polemarch/main/tests/inventory.py @@ -1,6 +1,5 @@ from ...api.v1.views import HostViewSet from ._base import BaseTestCase, json -from ..models import Host, Group, Inventory class _ApiGHBaseTestCase(BaseTestCase): @@ -50,11 +49,8 @@ def _create_inventories(self, inventories): return self.mass_create("/api/v1/inventories/", inventories, "name", "vars") - def _create_tasks(self, tasks): - return self.mass_create("/api/v1/tasks/", tasks, - "inventory", "playbook") - - def _create_periodic_tasks(self, tasks): + # No call of this function + def _create_periodic_tasks(self, tasks): # nocv return self.mass_create("/api/v1/periodic-tasks/", tasks, "task", "schedule", "type") @@ -81,19 +77,24 @@ def setUp(self): self.vars3 = dict(ansible_host="127.0.0.2") self.vars3.update(self.vars) self.vars3.update(self.vars2) - self.h1 = Host.objects.create(name="127.0.0.1", - type="HOST", vars=self.vars) - self.h2 = Host.objects.create(name="hostonlocal", - type="HOST", vars=self.vars3) - self.h3 = Host.objects.create(name="127.0.0.[3:4]", - type="RANGE", vars=self.vars) - self.h4 = Host.objects.create(name="127.0.0.[5:6]", - type="RANGE", vars=self.vars2) + self.h1 = self.get_model_class('Host').objects.create(name="127.0.0.1", + type="HOST", + vars=self.vars) + self.h2 = self.get_model_class('Host').objects.create(name="hostonlocal", + type="HOST", + vars=self.vars3) + self.h3 = self.get_model_class('Host').objects.create(name="127.0.0.[3:4]", + type="RANGE", + vars=self.vars) + self.h4 = self.get_model_class('Host').objects.create(name="127.0.0.[5:6]", + type="RANGE", + vars=self.vars2) def test_string_vars(self): vars3 = json.dumps(self.vars3) - host = Host.objects.create(name="127.0.0.1", - type="HOST", vars=vars3) + host = self.get_model_class('Host').objects.create(name="127.0.0.1", + type="HOST", + vars=vars3) self.assertEquals(host.vars['auth_user'], 'centos') def test_create_delete_host(self): @@ -109,23 +110,12 @@ def test_create_delete_host(self): for host_id in results_id: self.get_result("delete", url + "{}/".format(host_id)) - self.assertEqual(Host.objects.filter(id__in=results_id).count(), 0) + qs = self.get_model_class('Host').objects.filter(id__in=results_id) + self.assertEqual(qs.count(), 0) data = dict(name="127.0.1.1", type="host", vars=self.vars) self.get_result("post", url, 415, data=json.dumps(data)) - def hosts_validation(self): - url = "/api/v1/hosts/" - data = [dict(name="???", type="HOST", vars={}), - dict(name="hostlocl", type="HOST", - vars={"ansible_host": "???"})] - for h in data: - result = self.get_result("post", url, 400, data=json.dumps(h)) - self.assertIn("Invalid hostname or IP", str(result)) - data = dict(name="???", type="RANGE", vars={}) - result = self.get_result("post", url, 400, data=json.dumps(data)) - self.assertIn("Name must be Alphanumeric", str(result)) - def test_filter_host(self): base_url = "/api/v1/hosts/" filter_url = "{}?name=hostonlocal".format(base_url) @@ -223,7 +213,8 @@ def test_secret_host_vars(self): else: self.assertEqual(val, "lopuhost") - val = Host.objects.get(pk=host['id']).vars['ansible_become_pass'] + host_objects = self.get_model_class('Host').objects + val = host_objects.get(pk=host['id']).vars['ansible_become_pass'] self.assertEqual(val, "secret") @@ -235,20 +226,20 @@ def setUp(self): self.vars3 = dict(ansible_ssh_pass="qwerty") self.vars3.update(self.vars) self.vars3.update(self.vars2) - self.gr1 = Group.objects.create(name="base_one") - self.gr2 = Group.objects.create(name="base_two") - self.gr3 = Group.objects.create(name="base_three") + self.gr1 = self.get_model_class('Group').objects.create(name="base_one") + self.gr2 = self.get_model_class('Group').objects.create(name="base_two") + self.gr3 = self.get_model_class('Group').objects.create(name="base_three") def test_circular_deps(self): url = "/api/v1/groups/" # URL to groups layer groups = [ - Group.objects.create(name="base_0", children=True), - Group.objects.create(name="base_1", children=True), - Group.objects.create(name="base_2", children=True), - Group.objects.create(name="base_3", children=True), - Group.objects.create(name="base_4", children=True), - Group.objects.create(name="base_5", children=True), - Group.objects.create(name="base_6", children=True) + self.get_model_class('Group').objects.create(name="base_0", children=True), + self.get_model_class('Group').objects.create(name="base_1", children=True), + self.get_model_class('Group').objects.create(name="base_2", children=True), + self.get_model_class('Group').objects.create(name="base_3", children=True), + self.get_model_class('Group').objects.create(name="base_4", children=True), + self.get_model_class('Group').objects.create(name="base_5", children=True), + self.get_model_class('Group').objects.create(name="base_6", children=True) ] groups[1].groups.add(*[groups[2], groups[3], groups[5]]) groups[2].groups.add(groups[4]) @@ -269,9 +260,9 @@ def test_circular_deps(self): data=json.dumps([groups[6].id])) self.assertEqual(result["error_type"], "CiclicDependencyError") # Fix for clear group if CiclicDependencyError - g1 = Group.objects.create(name="base_01") - g2 = Group.objects.create(name="base_02", children=True) - g3 = Group.objects.create(name="base_03", children=True) + g1 = self.get_model_class('Group').objects.create(name="base_01") + g2 = self.get_model_class('Group').objects.create(name="base_02", children=True) + g3 = self.get_model_class('Group').objects.create(name="base_03", children=True) g3.groups.add(g2) g2.groups.add(g1) test_url = "{}{}/groups/".format(url, g2.id) @@ -282,7 +273,7 @@ def test_circular_deps(self): def test_create_delete_group(self): url = "/api/v1/groups/" - self.list_test(url, Group.objects.count()) + self.list_test(url, self.get_model_class('Group').objects.count()) self.details_test(url + "{}/".format(self.gr1.id), name=self.gr1.name) data = [dict(name="one", vars=self.vars), @@ -293,7 +284,7 @@ def test_create_delete_group(self): for group_id in results_id: self.get_result("delete", url + "{}/".format(group_id)) self.assertEqual(self.get_result("get", url)["count"], - Group.objects.count()) + self.get_model_class('Group').objects.count()) def test_hosts_in_group(self): url = "/api/v1/groups/" # URL to groups layer @@ -396,12 +387,16 @@ def setUp(self): self.vars3 = dict(ansible_ssh_pass="qwerty") self.vars3.update(self.vars) self.vars3.update(self.vars2) - self.inv1 = Inventory.objects.create(name="First_inventory") - self.inv2 = Inventory.objects.create(name="Second_inventory") + self.inv1 = self.get_model_class('Inventory').objects.create( + name="First_inventory" + ) + self.inv2 = self.get_model_class('Inventory').objects.create( + name="Second_inventory" + ) def test_create_delete_inventory(self): url = "/api/v1/inventories/" - self.list_test(url, Inventory.objects.count()) + self.list_test(url, self.get_model_class('Inventory').objects.count()) self.details_test(url + "{}/".format(self.inv1.id), name=self.inv1.name, hosts=[], groups=[]) diff --git a/polemarch/main/tests/project.py b/polemarch/main/tests/project.py index dcd6ff27..cf4a9f80 100644 --- a/polemarch/main/tests/project.py +++ b/polemarch/main/tests/project.py @@ -3,23 +3,30 @@ import sys import os from .inventory import _ApiGHBaseTestCase -from ..models import Project class ApiProjectsTestCase(_ApiGHBaseTestCase): def setUp(self): super(ApiProjectsTestCase, self).setUp() - self.prj1 = Project.objects.create(name="First_project", - repository="git@ex.us:dir/rep1.git", - vars=dict(repo_type="TEST")) - self.prj2 = Project.objects.create(name="Second_project", - repository="git@ex.us:dir/rep2.git", - vars=dict(repo_type="TEST", - some_arg="search_arg")) + self.prj1 = self.get_model_class('Project').objects.create( + name="First_project", + repository="git@ex.us:dir/rep1.git", + vars=dict(repo_type="TEST"), + notes="Description example" + ) + self.prj2 = self.get_model_class('Project').objects.create( + name="Second_project", + repository="git@ex.us:dir/rep2.git", + vars=dict(repo_type="TEST", some_arg="search_arg") + ) + + def test_pagenated(self): + for project in self.get_model_class('Project').objects.all().paged(chunk_size=1): + project.id def test_create_delete_project(self): url = "/api/v1/projects/" - self.list_test(url, Project.objects.all().count()) + self.list_test(url, self.get_model_class('Project').objects.all().count()) self.details_test(url + "{}/".format(self.prj1.id), name=self.prj1.name, repository="git@ex.us:dir/rep1.git") @@ -42,7 +49,7 @@ def test_create_delete_project(self): results_id = self.mass_create(url, data, "name", "repository") for project_id in results_id: - proj_obj = Project.objects.get(pk=project_id) + proj_obj = self.get_model_class('Project').objects.get(pk=project_id) self.assertEqual(proj_obj.vars["repo_type"], "TEST") self.assertEqual(proj_obj.status, "OK") file = "/f{}.yml".format(sys.version_info[0]) @@ -53,7 +60,8 @@ def test_create_delete_project(self): self.assertEqual(f.readline(), "update") self.get_result("delete", url + "{}/".format(project_id)) self.assertTrue(not os.path.exists(proj_obj.path + file)) - self.assertEqual(Project.objects.filter(id__in=results_id).count(), 0) + qs = self.get_model_class('Project').objects.filter(id__in=results_id) + self.assertEqual(qs.count(), 0) repo_url = "git@sdf:cepreu/ansible-experiments.git" data = dict(name="GitProject{}".format(sys.version_info[0]), @@ -75,6 +83,24 @@ def test_create_delete_project(self): self._filter_vars(url, "repo_type:TEST", 6) self._filter_vars(url, "some_arg:search_arg", 1) + # Create hidden projects for selfcare + data = dict( + name="TestHiddenProject", + vars=dict(repo_type='MANUAL'), + hidden=True + ) + hidden_prj = self.get_model_class('Project').objects.create(**data) + self._filter_test(url, dict(name="TestHidden"), 0) + self.get_result("get", url + "{}/".format(hidden_prj.id), 404) + self.get_result("delete", url + "{}/".format(hidden_prj.id), 404) + with open(hidden_prj.path + "/hidden_test.yml", 'w+') as plbook: + plbook.write("") + hidden_prj.sync() + self.assertCount(hidden_prj.tasks.all(), 1) + result = self.get_result("get", '/api/v1/tasks/?name=hidden') + self.assertEqual(result['count'], 0) + hidden_prj.delete() + def test_inventories_in_project(self): url = "/api/v1/projects/" # URL to projects layer @@ -106,7 +132,7 @@ def test_manual_repo(self): data = dict(name="Manual project", repository="manual", vars=dict(repo_type="MANUAL")) prj_id = self.mass_create(url, [data], "name", "repository")[0] - project = Project.objects.get(pk=prj_id) + project = self.get_model_class('Project').objects.get(pk=prj_id) self.assertEqual(project.vars["repo_type"], "MANUAL") self.assertEqual(project.status, "OK") task_url = task_url.format(project.id) diff --git a/polemarch/main/tests/repo_backends.py b/polemarch/main/tests/repo_backends.py index 5851c7b3..6f9bf78c 100644 --- a/polemarch/main/tests/repo_backends.py +++ b/polemarch/main/tests/repo_backends.py @@ -8,7 +8,7 @@ try: from mock import patch -except ImportError: +except ImportError: # nocv from unittest.mock import patch from django.test import override_settings from .inventory import _ApiGHBaseTestCase @@ -77,8 +77,8 @@ def test_git_import(self): tasks_url = "/api/v1/tasks/?project={}".format(prj_id) tasks = self.get_result("get", tasks_url, 200) self.assertEquals(tasks["count"], 2) - self.assertEquals(tasks["results"][0]["name"], "main") - self.assertEquals(tasks["results"][1]["name"], "other") + for i in tasks["results"]: + self.assertIn(i["name"], ["main", "other"]) self.assertEqual(project["revision"], second_revision) self.get_result("post", single_url+"sync/", 200) @@ -103,6 +103,19 @@ def test_git_import(self): self.assertEqual(project['status'], "OK") self.assertEqual(project['vars']['repo_branch'], "new_branch") self.assertEqual(project["revision"], first_revision) + + # With key + del data['vars']['repo_password'] + data['vars']['repo_key'] = "pN6BQnjCdVybFaaA" + prj_id = self.get_result("post", self.url, data=json.dumps(data))['id'] + self.projects_to_delete.append(prj_id) + + single_url = self.url + "{}/".format(prj_id) + project = self.get_result("get", single_url) + self.assertEqual(project['status'], "OK") + self.assertEqual(project['vars']['repo_branch'], "new_branch") + self.assertEqual(project['branch'], "new_branch") + # delete test repository shutil.rmtree(repo_dir) @@ -122,11 +135,19 @@ def test_tar_import(self, download): self.assertEqual(self.get_result("get", single_url)['status'], "OK") tasks_url = "/api/v1/tasks/?project={}".format(prj_id) tasks = self.get_result("get", tasks_url, 200) + self.assertEquals(tasks["count"], 1) self.assertEquals(tasks["results"][0]["name"], "main") + self.get_result("post", single_url + "sync/", 200) + download.side_effect = [self.tests_path + '/test_reposit.tar'] * 10 self.get_result("post", single_url + "sync/", 200) + with patch('polemarch.main.repo._base._Base._make_operations') \ + as tmp_mock: + tmp_mock.side_effect = ValueError + self.get_result("post", single_url + "sync/", 200) + # TODO: # pull not cloned repo # ssh repository with key access diff --git a/polemarch/main/tests/tasks.py b/polemarch/main/tests/tasks.py index 6de5e801..9a29bfa4 100644 --- a/polemarch/main/tests/tasks.py +++ b/polemarch/main/tests/tasks.py @@ -12,11 +12,9 @@ try: from mock import patch -except ImportError: +except ImportError: # nocv from unittest.mock import patch -from ..models import Project -from ..models import Task, PeriodicTask, History, Inventory, Template from ..tasks.tasks import ScheduledTask from .inventory import _ApiGHBaseTestCase @@ -30,16 +28,20 @@ def setUp(self): vars=dict(repo_type="TEST"))] self.project_id = self.mass_create("/api/v1/projects/", data, "name", "repository")[0] - self.task_proj = Project.objects.get(id=self.project_id) + self.task_proj = self.get_model_class('Project').objects.get( + id=self.project_id + ) - self.task1 = Task.objects.create(playbook="first.yml", - project=self.task_proj) - self.task2 = Task.objects.create(playbook="second.yml", - project=self.task_proj) + self.task1 = self.get_model_class('Task').objects.create( + playbook="first.yml", project=self.task_proj + ) + self.task2 = self.get_model_class('Task').objects.create( + playbook="second.yml", project=self.task_proj + ) def test_get_tasks(self): url = "/api/v1/tasks/" - self.list_test(url, Task.objects.all().count()) + self.list_test(url, self.get_model_class('Task').objects.all().count()) correct_simple_inventory = ( "127.0.1.1 ansible_user=centos " @@ -66,8 +68,9 @@ def create_inventory(self): data=json.dumps([inventory])) return inventory, host + @patch('polemarch.main.models.projects.Project.sync') @patch('polemarch.main.utils.CmdExecutor.execute') - def test_execute(self, subprocess_function): + def test_execute(self, subprocess_function, sync): inv1, h1 = self.create_inventory() # mock side effect to get ansible-playbook args for assertions in test result = dict() @@ -111,6 +114,37 @@ def side_effect(call_args, *args, **kwargs): self.assertEquals(result['host'], "127.0.1.1") self.assertEquals(result['ansible_user'], "centos") self.assertEquals(result['ansible_ssh_private_key_file'], "somekey") + self.assertEquals(sync.call_count, 0) + subprocess_function.reset_mock() + + # Sync on every execute. + # Add key for every run sync project + data = self.task_proj.vars + data['repo_sync_on_run'] = True + self.task_proj.vars = data + self.post_result( + "/api/v1/projects/{}/execute-playbook/".format(self.task_proj.id), + data=json.dumps( + dict(inventory=inv1, playbook="first.yml", sync=True) + ) + ) + self.assertEquals(sync.call_count, 1) + sync.reset_mock() + + def side_effect(*args, **kwargs): + raise Exception("Test exceptions") + + sync.side_effect = side_effect + self.post_result( + "/api/v1/projects/{}/execute-playbook/".format(self.task_proj.id), + data=json.dumps( + dict(inventory=inv1, playbook="first.yml", sync=True) + ), code=400 + ) + hist = self.get_model_class("History").objects.all().first() + self.assertIn("ERROR on Sync operation", hist.raw_stdout) + data['repo_sync_on_run'] = False + self.task_proj.vars = data @patch('polemarch.main.utils.CmdExecutor.execute') def test_execute_module(self, subprocess_function): @@ -157,7 +191,9 @@ def side_effect(call_args, *args, **kwargs): self.assertEquals(result['ansible_user'], "centos") self.assertEquals(result['ansible_ssh_private_key_file'], "somekey") self.assertEquals(result['ansible_become_pass'], "secret") - history = History.objects.get(id=answer["history_id"]) + history = self.get_model_class('History').objects.get( + id=answer["history_id"] + ) self.assertEquals(history.kind, "MODULE") self.assertEquals(history.mode, "shell") self.assertIn( @@ -263,7 +299,9 @@ def check_status(exception, status): self.assertEquals(history.status, status) def get_history_item(): - histories = History.objects.filter(mode="other/playbook.yml") + histories = self.get_model_class('History').objects.filter( + mode="other/playbook.yml" + ) self.assertEquals(histories.count(), 1) history = histories[0] # History.objects.all().delete() @@ -307,14 +345,14 @@ def side_effect(call_args, *args, **kwargs): history.start_time <= history.stop_time) self.assertTrue(history.stop_time <= end_time and history.stop_time >= history.start_time) - self.assertEqual(history.initiator_object, self.user) - History.objects.all().delete() + self.assertEqual(history.executor, self.user) + self.get_model_class('History').objects.all().delete() # node are offline check_status(subprocess.CalledProcessError(4, None, ""), "OFFLINE") - History.objects.all().delete() + self.get_model_class('History').objects.all().delete() # error at node check_status(subprocess.CalledProcessError(None, None, None), "ERROR") - History.objects.all().delete() + self.get_model_class('History').objects.all().delete() result = self.get_result( "post", url.format(self.task_proj.id), @@ -358,7 +396,11 @@ def test_cancel_task(self, subprocess_function): inv, _ = self.create_inventory() result = self.post_result( "/api/v1/projects/{}/execute-playbook/".format(self.task_proj.id), - data=json.dumps(dict(inventory=inv, playbook="first.yml"))) + data=json.dumps({ + "inventory": inv, + "playbook": "first.yml", + "vault-password-file": "vault-password-file1234", + })) history = self.get_result("get", "/api/v1/history/{}/".format( result["history_id"] @@ -412,6 +454,7 @@ def side_effect(url, when, message): return '200 OK: {"result": "ok"}' execute_method.side_effect = side_effect + self.post_result( "/api/v1/projects/{}/execute-playbook/".format(self.task_proj.id), data=json.dumps(dict(inventory=inv, playbook=playbook, sync=1)) @@ -435,6 +478,13 @@ def side_effect(url, when, message): h.save() hosts.delete() self.assertEquals(execute_method.call_count, 9) + hook_data['when'] = 'after_execution' + self.post_result("/api/v1/hooks/", data=json.dumps(hook_data)) + self.post_result( + "/api/v1/projects/{}/execute-playbook/".format(self.task_proj.id), + data=json.dumps(dict(inventory=inv, playbook=playbook, sync=1)) + ) + self.assertTrue(self.sended, "Fail") @patch('polemarch.main.utils.CmdExecutor.execute') def test_execute_inventory_file(self, subprocess_function): @@ -456,6 +506,7 @@ def side_effect(call_args, *args, **kwargs): "/api/v1/projects/{}/execute-module/".format(self.task_proj.id), data=json.dumps(dict(inventory="./12", module="ping", group="all")) ) + self.post_result( "/api/v1/projects/{}/execute-playbook/".format(self.task_proj.id), data=json.dumps(dict(inventory="inventory", playbook="first.yml")) @@ -479,16 +530,20 @@ def setUp(self): vars=dict(repo_type="TEST"))] self.periodic_project_id = self.mass_create("/api/v1/projects/", data, "name", "repository")[0] - self.project = Project.objects.get(id=self.periodic_project_id) - self.inventory = Inventory.objects.create() + self.project = self.get_model_class('Project').objects.get( + id=self.periodic_project_id + ) + self.inventory = self.get_model_class('Inventory').objects.create() - self.ptask1 = PeriodicTask.objects.create(mode="p1.yml", + self.ptask1 = self.get_model_class('PeriodicTask').objects.create( + mode="p1.yml", name="test", schedule="10", type="INTERVAL", project=self.project, inventory=self.inventory) - self.ptask2 = PeriodicTask.objects.create(mode="p2.yml", + self.ptask2 = self.get_model_class('PeriodicTask').objects.create( + mode="p2.yml", name="test", schedule="10", type="INTERVAL", @@ -497,7 +552,8 @@ def setUp(self): def test_create_delete_periodic_task(self): url = "/api/v1/periodic-tasks/" - self.list_test(url, PeriodicTask.objects.all().count()) + count = self.get_model_class('PeriodicTask').objects.all().count() + self.list_test(url, count) self.details_test(url + "{}/".format(self.ptask1.id), mode="p1.yml", schedule="10", @@ -518,13 +574,14 @@ def test_create_delete_periodic_task(self): dict(mode="p1.yml", schedule="30 */4", type="CRONTAB", project=self.periodic_project_id, inventory=self.inventory.id, name="four", vars=variables)] - results_id = self.mass_create(url, data, "mode", "schedule", - "type", "project", "name", "vars") + objs_id = self.mass_create(url, data, "mode", "schedule", + "type", "project", "name", "vars" + ) - for project_id in results_id: + for project_id in objs_id: self.get_result("delete", url + "{}/".format(project_id)) - count = PeriodicTask.objects.filter(id__in=results_id).count() - self.assertEqual(count, 0) + qs = self.get_model_class('PeriodicTask').objects.filter(id__in=objs_id) + self.assertEqual(qs.count(), 0) # test with bad value data = dict(mode="p1.yml", schedule="30 */4 foo", type="CRONTAB", @@ -549,7 +606,7 @@ def test_create_delete_periodic_task_module(self): type="INTERVAL", inventory=self.inventory, project=self.project) - ptask = PeriodicTask.objects.create(**details) + ptask = self.get_model_class('PeriodicTask').objects.create(**details) details['inventory'] = str(self.inventory.id) details['project'] = self.project.id url = "/api/v1/periodic-tasks/" @@ -577,15 +634,15 @@ def test_create_delete_periodic_task_module(self): "kind") for project_id in results_id: self.get_result("delete", url + "{}/".format(project_id)) - count = PeriodicTask.objects.filter(id__in=results_id).count() - self.assertEqual(count, 0) + qs = self.get_model_class('PeriodicTask').objects.filter(id__in=results_id) + self.assertEqual(qs.count(), 0) def test_periodic_task_ansible_args_validation(self): def update_func(args, mistake): args['vars'].update(mistake) url = "/api/v1/periodic-tasks/" - old_count = PeriodicTask.objects.count() + old_count = self.get_model_class('PeriodicTask').objects.count() variables = {"limit": "host-1"} # playbook data = dict(mode="p1.yml", schedule="10", type="INTERVAL", @@ -596,7 +653,7 @@ def update_func(args, mistake): data['kind'] = "MODULE" self.make_test(url, data, update_func, "group") # none of PeriodicTasks created in DB - self.assertEquals(old_count, PeriodicTask.objects.count()) + self.assertEquals(old_count, self.get_model_class('PeriodicTask').objects.count()) @patch('polemarch.main.utils.CmdExecutor.execute') def test_periodic_task_execution(self, subprocess_function): @@ -627,13 +684,13 @@ def test_periodic_task_execution(self, subprocess_function): subprocess_function.reset_mock() data['save_result'] = False id = self.get_result("post", url, 201, data=json.dumps(data))['id'] - count = History.objects.all().count() + count = self.get_model_class('History').objects.all().count() ScheduledTask.delay(id) self.assertEquals(subprocess_function.call_count, 1) call_args = subprocess_function.call_args[0][0] self.assertTrue(call_args[0].endswith("ansible-playbook")) self.assertTrue(call_args[1].endswith("p1.yml")) - self.assertCount(History.objects.all(), count) + self.assertCount(self.get_model_class('History').objects.all(), count) def side_effect(*args, **kwargs): raise Exception("Test text") @@ -641,8 +698,10 @@ def side_effect(*args, **kwargs): subprocess_function.reset_mock() subprocess_function.side_effect = side_effect ScheduledTask(id) + for history in self.histories: + self.assertEqual(history.executor, None) self.assertEquals(subprocess_function.call_count, 1) - self.assertCount(History.objects.all(), count) + self.assertCount(self.get_model_class('History').objects.all(), count) @patch('polemarch.main.utils.CmdExecutor.execute') def test_periodictask_inventory_file(self, subprocess_function): @@ -656,6 +715,7 @@ def test_periodictask_inventory_file(self, subprocess_function): inventory="inventory", name="one", vars={"args": "ls -la", "group": "all"}) id = self.get_result("post", url, 201, data=json.dumps(data))['id'] + ScheduledTask.delay(-1) ScheduledTask.delay(id) self.assertEquals(subprocess_function.call_count, 1) call_args = subprocess_function.call_args[0][0] @@ -715,7 +775,7 @@ class ApiTemplateTestCase(_ApiGHBaseTestCase, AnsibleArgsValidationTest): def setUp(self): super(ApiTemplateTestCase, self).setUp() - self.pr_tmplt = Project.objects.create(**dict( + self.pr_tmplt = self.get_model_class('Project').objects.create(**dict( name="TmpltProject", repository="git@ex.us:dir/rep3.git", vars=dict(repo_type="TEST") @@ -732,9 +792,11 @@ def setUp(self): connection="paramiko", tags="update", ) - ) + ), + notes="Test template" ) - self.job_template = Template.objects.create(**self.tmplt_data) + job_tmplt = self.get_model_class('Template').objects.create(**self.tmplt_data) + self.job_template = job_tmplt # Ugly hack for fix some errors self.tmplt_data.update(dict( name=self.job_template.name, @@ -754,8 +816,16 @@ def side_effect(call_args, *args, **kwargs): tmplt = self.post_result(url, data=json.dumps(self.tmplt_data)) single_url = "{}{}/".format(url, tmplt['id']) # test playbook execution - self.post_result(single_url + "execute/", code=201) + result = self.post_result(single_url + "execute/", code=201) self.assertIn('test.yml', ansible_args) + history = self.get_model_filter('History', pk=result['history_id']).get() + self.assertEqual(history.initiator_type, "template") + self.assertEqual(history.initiator, tmplt['id']) + self.assertEqual(history.executor.id, tmplt['owner']['id']) + self.assertEqual( + history.initiator_object, + self.get_model_filter('Template', pk=tmplt['id']).get() + ) # test module execution ansible_args = [] module_data = dict( @@ -819,7 +889,7 @@ def side_effect(call_args, *args, **kwargs): # test playbook execution one option ansible_args = [] - self.post_result(single_url + "execute/", 201, data=dict(option='one')) + result = self.post_result(single_url + "execute/", 201, data=dict(option='one')) self.assertIn(tmpl_with_opts['data']['module'], ansible_args) self.assertIn(tmpl_with_opts['options']['one']['group'], ansible_args) self.assertIn('--forks', ansible_args) @@ -827,6 +897,13 @@ def side_effect(call_args, *args, **kwargs): str(tmpl_with_opts['data']['vars']['forks']), ansible_args ) + # get history + history = self.get_model_filter('History', pk=result['history_id']).get() + # Check in options `template_option_name` + self.assertEqual(history.options['template_option'], 'one') + with self.assertRaises(ValidationError): + history.options = "string" + # test playbook execution two option ansible_args = [] self.post_result(single_url + "execute/", 201, data=dict(option='two')) @@ -859,6 +936,8 @@ def side_effect(call_args, *args, **kwargs): ) def test_string_template_data(self): + with open(self.pr_tmplt.path + "/inv1", 'w') as inv1: + inv1.write("") tmplt_data = dict( name="test_tmplt", kind="Task", @@ -868,11 +947,11 @@ def test_string_template_data(self): vars=dict() ) ) - job_template = Template.objects.create(**tmplt_data) + job_template = self.get_model_class('Template').objects.create(**tmplt_data) job_template.data = json.dumps(dict( playbook="test.yml", project=self.pr_tmplt.id, - inventory=self.history_inventory.id, + inventory="./inv1", vars=dict( connection="paramiko", tags="update", @@ -883,11 +962,11 @@ def test_string_template_data(self): job_template.data = object() with self.assertRaises(ValidationError): - Template.objects.create(**tmplt_data) + self.get_model_class('Template').objects.create(**tmplt_data) def test_templates(self): url = "/api/v1/templates/" - self.list_test(url, Template.objects.all().count()) + self.list_test(url, self.get_model_class('Template').objects.all().count()) self.details_test(url + "{}/".format(self.job_template.id), **self.tmplt_data) @@ -910,13 +989,13 @@ def test_templates(self): for project_id in results_id: self.get_result("delete", url + "{}/".format(project_id)) - count = Template.objects.filter(id__in=results_id).count() + count = self.get_model_class('Template').objects.filter(id__in=results_id).count() self.assertEqual(count, 0) result = self.get_result("get", url+"supported-kinds/") - self.assertEqual(result, Template.template_fields) + self.assertEqual(result, self.get_model_class('Template').template_fields) - ptask_template_data = dict( + ptask_tmplt_data = dict( name="test_ptask_template", kind="PeriodicTask", data=dict( @@ -932,14 +1011,14 @@ def test_templates(self): ) ) ) - ptask_template = Template.objects.create(**ptask_template_data) - ptask_template_data.update(dict( - name=ptask_template.name, - kind=ptask_template.kind, - data=ptask_template.data + ptask_tmplt = self.get_model_class('Template').objects.create(**ptask_tmplt_data) + ptask_tmplt_data.update(dict( + name=ptask_tmplt.name, + kind=ptask_tmplt.kind, + data=ptask_tmplt.data )) - self.details_test(url + "{}/".format(ptask_template.id), - **ptask_template_data) + self.details_test(url + "{}/".format(ptask_tmplt.id), + **ptask_tmplt_data) module_template_data = dict( name="test_ptask_template", @@ -956,7 +1035,11 @@ def test_templates(self): ) ) ) - module_template = Template.objects.create(**module_template_data) + + module_template = self.get_model_class('Template').objects.create( + **module_template_data + ) + module_template_data.update(dict( name=module_template.name, kind=module_template.kind, @@ -971,17 +1054,17 @@ def update_func(args, mistake): self.make_test(url, self.tmplt_data, update_func) self.make_test(url, module_template_data, update_func, "group") - self.make_test(url, ptask_template_data, update_func, "group") + self.make_test(url, ptask_tmplt_data, update_func, "group") # Filters # by Project search_url = "{}?project={}".format(url, self.pr_tmplt.id) - real_count = Template.objects.filter(project=self.pr_tmplt).count() + qs = self.get_model_class('Template').objects.filter(project=self.pr_tmplt) res = self.get_result("get", search_url) - self.assertEqual(res["count"], real_count, [res, real_count]) + self.assertEqual(res["count"], qs.count(), [res, qs.count()]) # by Inventory search_url = "{}?inventory={}".format(url, self.history_inventory.id) - real_count = Template.objects.filter( + real_count = self.get_model_class('Template').objects.filter( inventory=str(self.history_inventory.id) ).count() res = self.get_result("get", search_url) @@ -1013,7 +1096,8 @@ def test_secret_template_vars(self): self.get_result("patch", "{}{}/".format(url, t['id']), data=json.dumps(data)) - for val in Template.objects.get(pk=t['id']).data['vars'].values(): + tmpls_objcts = self.get_model_class('Template').objects + for val in tmpls_objcts.get(pk=t['id']).data['vars'].values(): self.assertEqual(val, "secret") @@ -1022,32 +1106,42 @@ def setUp(self): super(ApiHistoryTestCase, self).setUp() repo = "git@ex.us:dir/rep3.git" - self.history_inventory = Inventory.objects.create() - self.ph = Project.objects.create(name="Prj_History", - repository=repo, - vars=dict(repo_type="TEST")) + self.history_inventory = self.get_model_class('Inventory').objects.create() + self.ph = self.get_model_class('Project').objects.create( + name="Prj_History", + repository=repo, + vars=dict(repo_type="TEST") + ) self.default_kwargs = dict(project=self.ph, mode="task.yml", raw_inventory="inventory", raw_stdout="text", inventory=self.history_inventory, initiator=self.user.id) self.histories = [ - History.objects.create(status="OK", - start_time=now() - timedelta(hours=15), - stop_time=now() - timedelta(hours=14), - **self.default_kwargs), - History.objects.create(status="STOP", - start_time=now() - timedelta(hours=25), - stop_time=now() - timedelta(hours=24), - **self.default_kwargs), - History.objects.create(status="ERROR", - start_time=now() - timedelta(hours=35), - stop_time=now() - timedelta(hours=34), - **self.default_kwargs), + self.get_model_class('History').objects.create( + status="OK", + start_time=now() - timedelta(hours=15), + stop_time=now() - timedelta(hours=14), + **self.default_kwargs + ), + self.get_model_class('History').objects.create( + status="STOP", + start_time=now() - timedelta(hours=25), + stop_time=now() - timedelta(hours=24), + **self.default_kwargs), + self.get_model_class('History').objects.create( + status="ERROR", + start_time=now() - timedelta(hours=35), + stop_time=now() - timedelta(hours=34), + **self.default_kwargs), + self.get_model_class('History').objects.create( + status="RUN", + start_time=now() - timedelta(hours=40), + **self.default_kwargs) ] self.default_kwargs["raw_stdout"] = "one\ntwo\nthree\nfour" self.default_kwargs["mode"] = "task2.yml" - self.histories.append(History.objects.create( + self.histories.append(self.get_model_class('History').objects.create( status="ERROR", start_time=now() - timedelta(hours=35), stop_time=now() - timedelta(hours=34), **self.default_kwargs) ) @@ -1056,20 +1150,40 @@ def test_history_of_executions(self): url = "/api/v1/history/" df = "%Y-%m-%dT%H:%M:%S.%fZ" self.list_test(url, len(self.histories)) - self.details_test(url + "{}/".format(self.histories[0].id), - mode="task.yml", - status="OK", project=self.ph.id, - start_time=self.histories[0].start_time.strftime(df), - stop_time=self.histories[0].stop_time.strftime(df), - raw_inventory="inventory", - inventory=self.history_inventory.id, - initiator=self.user.id, initiator_type="users") + self.details_test( + url + "{}/".format(self.histories[0].id), + mode="task.yml", + status="OK", project=self.ph.id, + # Commented because DRF broke API by fields + # start_time=self.histories[0].start_time.strftime(df), + # stop_time=self.histories[0].stop_time.strftime(df), + raw_inventory="inventory", + inventory=self.history_inventory.id, + executor=None, initiator_type="project", + execution_time=3600 + ) + + pached_method = 'polemarch.main.models.tasks.History._get_seconds_from_time' + with patch(pached_method) as time_mock: + time_mock.return_value = 144000 + self.details_test( + url + "{}/".format(self.histories[3].id), + mode="task.yml", + status="RUN", project=self.ph.id, + # Commented because DRF broke API by fields + # start_time=self.histories[0].start_time.strftime(df), + # stop_time=self.histories[0].stop_time.strftime(df), + raw_inventory="inventory", + inventory=self.history_inventory.id, + executor=None, initiator_type="project", + execution_time=144000 + ) result = self.get_result("get", "{}?status={}".format(url, "OK")) self.assertEqual(result["count"], 1, result) res = self.get_result("get", "{}?mode={}".format(url, "task.yml")) - self.assertEqual(res["count"], 3, res) + self.assertEqual(res["count"], 4, res) res = self.get_result("get", "{}?project={}".format(url, self.ph.id)) self.assertEqual(res["count"], len(self.histories), res) @@ -1093,12 +1207,12 @@ def test_history_of_executions(self): 405, data=dict(**self.default_kwargs)) # Lines pagination - lines_url = url+"{}/lines/?limit=2".format(self.histories[3].id) + lines_url = url+"{}/lines/?limit=2".format(self.histories[4].id) result = self.get_result("get", lines_url) self.assertEqual(result["count"], 4, result) self.assertCount(result["results"], 2) lines_url = url - lines_url += "{}/lines/?after=2&before=4".format(self.histories[3].id) + lines_url += "{}/lines/?after=2&before=4".format(self.histories[4].id) result = self.get_result("get", lines_url) self.assertEqual(result["count"], 1, result) self.assertCount(result["results"], 1) @@ -1130,7 +1244,7 @@ def test_history_raw_output(self): start_time=now() - timedelta(hours=15), stop_time=now() - timedelta(hours=14)) default_kwargs['raw_stdout'] = raw_stdout - history = History.objects.create(**default_kwargs) + history = self.get_model_class('History').objects.create(**default_kwargs) url = "/api/v1/history/{}/raw/".format(history.id) result = self.get_result("get", url) self.assertEquals(result, nocolor) @@ -1158,7 +1272,7 @@ def test_history_facts(self): status="OK", start_time=now() - timedelta(hours=15), stop_time=now() - timedelta(hours=14)) - history = History.objects.create(**history_kwargs) + history = self.get_model_class('History').objects.create(**history_kwargs) stdout = self._get_string_from_file("facts_stdout") history.raw_stdout = stdout history.save() diff --git a/polemarch/main/unittests/__init__.py b/polemarch/main/unittests/__init__.py index cd316ede..2bfa6d93 100644 --- a/polemarch/main/unittests/__init__.py +++ b/polemarch/main/unittests/__init__.py @@ -1,7 +1,6 @@ -from .locks import LocksTestCase -from .kvexchanger import KVExchangerTestCase from .ansible import AnsibleTestCase -from .modelrelatedfield import ModelRelatedFieldTestCase -from .executor import ExecutorTestCase -from .routers import RoutersTestCase +from .utils import ExecutorTestCase, KVExchangerTestCase, LocksTestCase, CMDExecutorTestCase, tmp_fileTestCase, ModelHandlerTestCase +from .api import RoutersTestCase, ModelRelatedFieldTestCase, UserSettingsTestCase from .hooks import HooksTestCase +from .tasks import TasksTestCase, TestTaskError, TestRepoTask, ApiTemplateUnitTestCase +from .models import ModelsTestCase diff --git a/polemarch/main/unittests/api.py b/polemarch/main/unittests/api.py new file mode 100644 index 00000000..9a6a7083 --- /dev/null +++ b/polemarch/main/unittests/api.py @@ -0,0 +1,31 @@ +from ..tests._base import BaseTestCase +from ...api.v1.serializers import ModelRelatedField +from ...api.urls import router_v1, v1 + + +class ModelRelatedFieldTestCase(BaseTestCase): + def test_modelrelatedfield(self): + ModelRelatedField(model=self.get_model_class('Host')) + + +class RoutersTestCase(BaseTestCase): + def test_uregister(self): + router_v1.unregister("history") + for pattern in router_v1.get_urls(): + self.assertIsNone(pattern.regex.search("history/1/")) + router_v1.register('history', v1.UserViewSet) + checked = False + for pattern in router_v1.registry: + if pattern[0] == 'history': + checked = True + self.assertEqual(pattern[1], v1.UserViewSet) + self.assertTrue(checked, "Not registered!") + + +class UserSettingsTestCase(BaseTestCase): + + def test_del_user_settings(self): + test_settings = self.get_model_class('UserSettings')() + test_settings.data = 'something for test' + del(test_settings.data) + self.assertEqual(test_settings.data, {}) diff --git a/polemarch/main/unittests/executor.py b/polemarch/main/unittests/executor.py deleted file mode 100644 index a1ff72bc..00000000 --- a/polemarch/main/unittests/executor.py +++ /dev/null @@ -1,35 +0,0 @@ -from subprocess import CalledProcessError - -from django.test import TestCase - -from ..utils import KVExchanger - -try: - from mock import MagicMock -except ImportError: - from unittest.mock import MagicMock - -from ..models.utils import Executor - - -class ExecutorTestCase(TestCase): - def test_executor(self): - # test output on `sleep --help` - output = [] - - def add_line(line, line_number): - # pylint: disable=unused-argument - output.append(line) - - history = MagicMock() - history.id = 999 - history.write_line = add_line - executor = Executor(history) - executor.execute(['sleep', '--version'], '/') - result = "\n".join(output) - self.assertIn("sleep", result) - # test interrupt on `sleep 5m` - KVExchanger(Executor.CANCEL_PREFIX + str(history.id)).send(True, 10) - executor = Executor(history) - with self.assertRaises(CalledProcessError): - executor.execute(['sleep', '5m'], '/') diff --git a/polemarch/main/unittests/hooks.py b/polemarch/main/unittests/hooks.py index 687098f7..f655af3e 100644 --- a/polemarch/main/unittests/hooks.py +++ b/polemarch/main/unittests/hooks.py @@ -8,7 +8,7 @@ from django.core.validators import ValidationError from requests import Response from polemarch.main.models import Hook -from polemarch.main.utils import tmp_file_context, raise_context +from polemarch.main.utils import raise_context class HooksTestCase(TestCase): @@ -27,11 +27,14 @@ def tearDown(self): def check_output_run(self, check_data, *args, **kwargs): # pylint: disable=unused-argument - self.assertEqual(check_data[0], self.recipients[self.count]) + rep = str(kwargs['cwd'] or '') + rep += '/' if kwargs['cwd'] else '' + rep += self.recipients[self.count] + self.assertEqual(check_data[0], rep) self.assertEqual(check_data[1], 'on_execution') - with open(check_data[2], 'r') as file: - message = json.load(file) - self.assertEqual(message.get('test', None), 'test') + message = json.loads(kwargs['input']) + self.assertEqual(message.get('test', None), 'test') + self.assertTrue(kwargs['universal_newlines']) self.count += 1 return "Ok" @@ -62,9 +65,11 @@ def test_script(self): def check_output_run_http(self, method, url, data, **kwargs): # pylint: disable=protected-access, unused-argument self.assertEqual(method, "post") - with tmp_file_context() as file: - file.write(json.dumps(data['payload'])) - self.check_output_run([url, data['type'], file.name]) + self.check_output_run( + [url, data['type']], + cwd='', input=json.dumps(data['payload']), + universal_newlines=True + ) the_response = Response() the_response.reason = "ok" the_response.status_code = 200 diff --git a/polemarch/main/unittests/kvexchanger.py b/polemarch/main/unittests/kvexchanger.py deleted file mode 100644 index 3467b3ac..00000000 --- a/polemarch/main/unittests/kvexchanger.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.test import TestCase - -from ..utils import KVExchanger - - -class KVExchangerTestCase(TestCase): - def test_kvexchanger(self): - KVExchanger("somekey").send(True, 10) - self.assertTrue(KVExchanger("somekey").get()) diff --git a/polemarch/main/unittests/locks.py b/polemarch/main/unittests/locks.py deleted file mode 100644 index 681f7753..00000000 --- a/polemarch/main/unittests/locks.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.test import TestCase - -from ..utils import model_lock_decorator, Lock - - -class LocksTestCase(TestCase): - def test_locks(self): - @model_lock_decorator() - def method(pk): - # pylint: disable=unused-argument - pass - - @model_lock_decorator() - def method2(pk): - method(pk=pk) - - method(pk=123) - with self.assertRaises(Lock.AcquireLockException): - method2(pk=123) diff --git a/polemarch/main/unittests/modelrelatedfield.py b/polemarch/main/unittests/modelrelatedfield.py deleted file mode 100644 index 83e9899a..00000000 --- a/polemarch/main/unittests/modelrelatedfield.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.test import TestCase - -from ..models import Host -from ...api.v1.serializers import ModelRelatedField - - -class ModelRelatedFieldTestCase(TestCase): - def test_modelrelatedfield(self): - ModelRelatedField(model=Host) diff --git a/polemarch/main/unittests/models.py b/polemarch/main/unittests/models.py new file mode 100644 index 00000000..5180589e --- /dev/null +++ b/polemarch/main/unittests/models.py @@ -0,0 +1,13 @@ +from ..tests._base import BaseTestCase + + +class ModelsTestCase(BaseTestCase): + def test_acl_handler(self): + ObjClass = self.get_model_class("Host") + obj = ObjClass() + class_handler = ObjClass.acl_handler + object_handler = obj.acl_handler + self.assertEqual(class_handler.instance, None) + self.assertEqual(class_handler.model, ObjClass) + self.assertEqual(object_handler.instance, obj) + self.assertEqual(object_handler.model, ObjClass) diff --git a/polemarch/main/unittests/routers.py b/polemarch/main/unittests/routers.py deleted file mode 100644 index a5c43877..00000000 --- a/polemarch/main/unittests/routers.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.test import TestCase - -from ...api.urls import router_v1, v1 - - -class RoutersTestCase(TestCase): - def test_uregister(self): - router_v1.unregister("history") - for pattern in router_v1.get_urls(): - self.assertIsNone(pattern.regex.search("history/1/")) - router_v1.register('history', v1.UserViewSet) - checked = False - for pattern in router_v1.registry: - if pattern[0] == 'history': - checked = True - self.assertEqual(pattern[1], v1.UserViewSet) - self.assertTrue(checked, "Not registered!") diff --git a/polemarch/main/unittests/tasks.py b/polemarch/main/unittests/tasks.py new file mode 100644 index 00000000..134b0ac4 --- /dev/null +++ b/polemarch/main/unittests/tasks.py @@ -0,0 +1,89 @@ +# import json +# +# import re +# +# from datetime import timedelta +# +# import subprocess +# +# from django.conf import settings +# from django.utils.timezone import now +from django.core.validators import ValidationError + +# try: +# from mock import patch +# except ImportError: # nocv +# from unittest.mock import patch +from django.test import TestCase +from ..tasks.exceptions import TaskError +from ..tasks import RepoTask +from ..exceptions import PMException +from ..models import History +from ..tests.inventory import _ApiGHBaseTestCase +from ..tests._base import AnsibleArgsValidationTest + + +class TasksTestCase(TestCase): + testHistory = History() + + def test_initiator_object(self): + self.testHistory.initiator_type = "Something else" + self.assertEquals(self.testHistory.initiator_object, None) + self.testHistory.initiator_type = "project" + self.testHistory.initiator = 1 + self.assertEqual(self.testHistory.initiator_object, self.testHistory) + + def test_execute_args_setter(self): + with self.assertRaises(ValidationError): + self.testHistory.execute_args = "something" + + +class ApiTemplateUnitTestCase(_ApiGHBaseTestCase, AnsibleArgsValidationTest): + def setUp(self): + super(ApiTemplateUnitTestCase, self).setUp() + + self.pr_tmplt = self.get_model_class('Project').objects.create(**dict( + name="TmpltProject", + repository="git@ex.us:dir/rep3.git", + vars=dict(repo_type="TEST") + ) + ) + self.tmplt_data = dict( + name="test_tmplt", + kind="Task", + data=dict( + playbook="test.yml", + somekey="somevalue", + project=1, + inventory=2, + vars=dict( + connection="paramiko", + tags="update", + ) + ) + ) + + with self.assertRaises(ValidationError): + self.get_model_class('Template').objects.create(**self.tmplt_data) + + def test_setup(self): + self.setUp() + self.assertRaises(ValidationError) + + +class TestTaskError(TestCase): + + def test_init_task_error(self): + msg = "Test error message" + first_task_error = TaskError(msg) + second_task_error = PMException(msg) + self.assertEqual(first_task_error.msg, second_task_error.msg) + + +class TestRepoTask(TestCase): + + def test_init_repo_task(self): + app = None + project = "TestProject" + with self.assertRaises(RepoTask.task_class.UnknownRepoOperation): + RepoTask(app, project, "error") diff --git a/polemarch/main/unittests/utils.py b/polemarch/main/unittests/utils.py new file mode 100644 index 00000000..a7e46d5b --- /dev/null +++ b/polemarch/main/unittests/utils.py @@ -0,0 +1,86 @@ +from subprocess import CalledProcessError + +from ..tests._base import BaseTestCase +from ..utils import KVExchanger, model_lock_decorator, Lock, CmdExecutor, \ + tmp_file, ModelHandlers + +try: + from mock import MagicMock +except ImportError: # nocv + from unittest.mock import MagicMock + +from ..models.utils import Executor + + +class ExecutorTestCase(BaseTestCase): + def test_executor(self): + # test output on `sleep --help` + output = [] + + def add_line(line, line_number): + # pylint: disable=unused-argument + output.append(line) + + history = MagicMock() + history.id = 999 + history.write_line = add_line + executor = Executor(history) + executor.execute(['sleep', '--version'], '/') + result = "\n".join(output) + self.assertIn("sleep", result) + # test interrupt on `sleep 5m` + KVExchanger(Executor.CANCEL_PREFIX + str(history.id)).send(True, 10) + executor = Executor(history) + with self.assertRaises(CalledProcessError): + executor.execute(['sleep', '5m'], '/') + + +class KVExchangerTestCase(BaseTestCase): + def test_kvexchanger(self): + KVExchanger("somekey").send(True, 10) + KVExchanger("somekey").prolong() + self.assertTrue(KVExchanger("somekey").get()) + + +class LocksTestCase(BaseTestCase): + def test_locks(self): + @model_lock_decorator() + def method(pk): + # pylint: disable=unused-argument + pass + + @model_lock_decorator() + def method2(pk): + method(pk=pk) + + method(pk=123) + method(pk=None) + with self.assertRaises(Lock.AcquireLockException): + method2(pk=123) + + +class CMDExecutorTestCase(BaseTestCase): + + test_cmd_executor = CmdExecutor() + + def test_write_output(self): + self.test_cmd_executor.write_output(5) + self.assertEqual(self.test_cmd_executor.output, '5') + + +class tmp_fileTestCase(BaseTestCase): + + def test_magic_enter_exit(self): + tmp = tmp_file(mode="r") + with tmp as test_tmp_file: + self.assertEqual(test_tmp_file, tmp) + tmp = tmp_file(mode="r") + self.assertEqual(tmp.__exit__(ValueError, 22, "Traceback"), False) + + +class ModelHandlerTestCase(BaseTestCase): + + def test_iter(self): + test_model_handler = ModelHandlers("HOOKS", "'type' needed!") + for i in test_model_handler: + pass diff --git a/polemarch/main/urls.py b/polemarch/main/urls.py index fa1ddade..f350a572 100644 --- a/polemarch/main/urls.py +++ b/polemarch/main/urls.py @@ -17,6 +17,7 @@ urlpatterns = [ url(r'^$', views.GUIView.as_view()), + url(r'^app$', views.AppGUIView.as_view()), url(r'^{}'.format(login_url), views.Login.as_view(), name='login'), url(r'^{}'.format(logout_url), views.Logout.as_view(), {'next_page': '/'}), url(r'^admin/', admin.site.urls), diff --git a/polemarch/main/utils.py b/polemarch/main/utils.py index 9152c9ab..31e1e1e5 100644 --- a/polemarch/main/utils.py +++ b/polemarch/main/utils.py @@ -3,6 +3,7 @@ import sys import time +import logging import traceback from subprocess import CalledProcessError, Popen, PIPE, STDOUT from threading import Thread @@ -36,12 +37,15 @@ from . import __file__ as file +logger = logging.getLogger('polemarch') + + def import_class(path): ''' Get class from string-path :param path: -- string containing full python-path - :type path: str + :type path: str,unicode :return: -- return class or module in path :rtype: class, module, object ''' @@ -84,6 +88,40 @@ def get_render(name, data, trans='en'): return result +class ClassPropertyDescriptor(object): + + def __init__(self, fget, fset=None): + self.fget = fget + self.fset = fset + + def __get__(self, obj, klass=None): + if obj is not None: + return self.fget.__get__(obj, obj)() + if klass is None: + klass = type(obj) # noce + return self.fget.__get__(obj, klass)() + + def __set__(self, obj, value): # noce + if not self.fset: + raise AttributeError("can't set attribute") + if obj is not None: + return self.fset.__get__(obj, obj)(value) + type_ = type(obj) + return self.fset.__get__(obj, type_)(value) + + def setter(self, func): # noce + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + self.fset = func + return self + + +def classproperty(func): + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + return ClassPropertyDescriptor(func) + + class CmdExecutor(object): # pylint: disable=no-member ''' @@ -93,7 +131,7 @@ class CmdExecutor(object): newlines = ['\n', '\r\n', '\r'] def __init__(self): - self.output = None + self.output = '' def write_output(self, line): ''' @@ -102,7 +140,7 @@ def write_output(self, line): :return: None :rtype: None ''' - self.output += line + self.output += str(line) def _enqueue_output(self, out, queue): line = out.readline() @@ -239,7 +277,7 @@ class KVExchanger(object): try: cache = caches["locks"] - except InvalidCacheBackendError: + except InvalidCacheBackendError: # nocv cache = caches["default"] def __init__(self, key, timeout=None): @@ -364,7 +402,7 @@ class ModelHandlers(object): def __init__(self, tp, err_message=None): ''' :param tp: -- type name for backends.Like name in dict. - :type tp: str + :type tp: str,unicode ''' self.type = tp self.err_message = err_message @@ -467,7 +505,10 @@ class raise_context(assertRaises): def execute(self, func, *args, **kwargs): with self.__class__(self._excepts, **self._kwargs): return func(*args, **kwargs) - return sys.exc_info() + type, value, traceback_obj = sys.exc_info() + if type is not None: # nocv + logger.debug(traceback.format_exc()) + return type, value, traceback_obj def __enter__(self): return self.execute diff --git a/polemarch/main/views.py b/polemarch/main/views.py index 7a20428b..d28825fa 100644 --- a/polemarch/main/views.py +++ b/polemarch/main/views.py @@ -19,6 +19,11 @@ class GUIView(BaseView): template_name = "gui/gui.html" +class AppGUIView(BaseView): + login_required = True + template_name = "gui/app-gui.html" + + class Login(BaseView): template_name = 'auth/login.html' @@ -35,6 +40,10 @@ def post(self, request, *args, **kwargs): return self.login(request) +class AppLogin(Login): + template_name = 'auth/app-login.html' + + class Logout(BaseView): def get(self, request, *args, **kwargs): diff --git a/polemarch/static/auth/main.css b/polemarch/static/auth/main.css index 42a75e7f..f4185d54 100644 --- a/polemarch/static/auth/main.css +++ b/polemarch/static/auth/main.css @@ -101,7 +101,8 @@ h3 { .mobile-index { background-image: linear-gradient(7deg, #f6f8fb, #eaeef5); - height:100vh; + min-height:100vh; + height: auto; width: 100%; text-align: center; padding: 20px 25px; @@ -131,7 +132,7 @@ h3 { /* height: 300px; */ max-width: 750px; margin: 0 auto; - margin-top: 100px; + /*margin-top: 100px;*/ } .mobile-index-body-inner h1 { @@ -174,7 +175,7 @@ h3 { display: block; width: 100%; height: 58px; - /* border-radius: 29px;*/ + /* border-radius: 29px;*/ padding: 0 32px; font-family: 'Source Sans Pro', sans-serif; font-size: 23px; @@ -263,10 +264,18 @@ h1 { .logo-img { width:50%; margin-bottom:20px; + margin-top: 12vh; } @media (max-width: 480px) { .logo-img { width:80%; + margin-top: 8vh; + } +} + +@media (max-width: 320px) { + .logo-img { + margin-top: 0vh; } } \ No newline at end of file diff --git a/polemarch/static/css/gui.css b/polemarch/static/css/gui.css index 2fad4559..a8a37f13 100644 --- a/polemarch/static/css/gui.css +++ b/polemarch/static/css/gui.css @@ -586,9 +586,9 @@ table{ .w-btn { padding: 5px; - font-size: 12px; - background: transparent; - color: #97a0b3; + font-size: 12px; + background: transparent; + color: #97a0b3; } .w-btn:focus{ @@ -627,9 +627,9 @@ table{ background-position: center center; background-repeat: no-repeat; -webkit-background-size:150px 40px; - -o-background-size: 150px 40px; - -moz-background-size:150px 40px; - background-size: 150px 40px; + -o-background-size: 150px 40px; + -moz-background-size:150px 40px; + background-size: 150px 40px; display: block; width:150px; height: 40px; @@ -645,9 +645,9 @@ table{ background-position: center center; background-repeat: no-repeat; -webkit-background-size:40px 40px; - -o-background-size: 40px 40px; - -moz-background-size:40px 40px; - background-size: 40px 40px; + -o-background-size: 40px 40px; + -moz-background-size:40px 40px; + background-size: 40px 40px; display: block; width: 40px; height: 40px; @@ -671,3 +671,307 @@ table{ background-image: url(/static/img/logo/logo-bw.png); } } + +.user-status, .group-children, .all-only, .new_subitem { + font-weight: bold; + text-align: center; + vertical-align: middle; + color: #276900; +} + +.matches { + font-weight: bold; + text-align: center; + vertical-align: middle; + color: #C1801A; +} + +/*стили для сайдбара */ +.skin-black-light .sidebar-menu>li:hover>a, .skin-black-light .sidebar-menu>li.active>a { + color: #000; + background: #EEEEEE; +} + +.skin-black-light .sidebar-menu>li>a { + border-left: 5px solid transparent; +} + +.sidebar-menu>li.active-li>a { + border-left: 5px solid #00c0ef; +} + +.pm-treeview { + position:relative; +} + +.pm-treeview>.pm-treeview-menu{ + display: none; +} + +.pm-treeview.hover-li .pm-treeview-menu { + display: block; + position: absolute; + left: 212px; + top: -30px; + list-style: none; + padding: 0; + padding-top: 30px; + padding-left: 20px; + padding-right: 30px; + padding-bottom: 30px; + margin: 0; + list-style: none; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; + z-index: 2000; +} + +.pm-treeview.hover-li .pm-treeview-menu>li:last-child, +.pm-treeview.hover-li .pm-treeview-menu>li:last-child>a { + border-bottom-right-radius: 4px; +} + +.pm-treeview.hover-li .pm-treeview-menu>li, +.pm-treeview-active.hover-li .pm-treeview-menu>li{ + display: block; +} + +.pm-treeview.hover-li .pm-treeview-menu>li, +.sidebar-mini.sidebar-collapse .sidebar-menu> .pm-treeview-active.hover-li .pm-treeview-menu>li{ + box-shadow: 2px 1px 3px rgba(0,0,0,0.1); +} + +.pm-treeview.hover-li .pm-treeview-menu>li>a { + padding: 12px 5px 12px 15px; + background-color: #fcfcfc; + width: 150px; + display: block; +} + +.sidebar-menu>li.pm-treeview>.pm-treeview-menu>li>a:hover { + background: #EEEEEE; +} + +.active-bold>a { + font-weight: bold; + background-color: #E0E0E0!important; +} + +.sidebar-menu>li.pm-treeview-active>.pm-treeview-menu>li>a:hover, +.pm-treeview-active>a:hover, +.sidebar-mini.sidebar-collapse .sidebar-menu>li.pm-treeview-active>a, +.sidebar-mini.sidebar-collapse .sidebar-menu>li.active-li>a { + background-color: #E0E0E0!important; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>li.hover-li .pm-treeview-menu>.active-bold>a{ + background-color: #fcfcfc!important; +} + +.sidebar-menu>li.pm-treeview-active>a, +.sidebar-menu>li.pm-treeview-active>.pm-treeview-menu { + border-left: 5px solid #00c0ef; +} + +.sidebar-menu>li.pm-treeview-active>.pm-treeview-menu>li>a { + background-color: #EEEEEE; +} + +.pm-treeview-active > .pm-treeview-menu { + display: block; + list-style: none; + padding: 0; + margin: 0; +} + +.pm-treeview-active > .pm-treeview-menu>li>a { + padding: 12px 5px 12px 38px; + display: block; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active .pm-treeview-menu { + display: none; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview.hover-li .pm-treeview-menu, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li .pm-treeview-menu { + display: block; + position: absolute; + left: 42px; + top: 43px; + list-style: none; + padding: 0; + padding-bottom: 30px; + padding-left: 10px; + padding-right: 30px; + margin: 0; + width: 220px; + border-left: none; + z-index: 2000; + border-bottom-right-radius: 4px; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview.hover-li .pm-treeview-menu>li:last-child, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview.hover-li .pm-treeview-menu>li:last-child>a, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li .pm-treeview-menu>li:last-child, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li .pm-treeview-menu>li:last-child>a{ + border-bottom-right-radius: 4px; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview.hover-li .pm-treeview-menu>li>a, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li .pm-treeview-menu>li>a { + padding: 12px 5px 12px 20px; + display: block; + background-color: #fcfcfc; + width: 180px; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview.hover-li .pm-treeview-menu>li:nth-child(2)>a, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li .pm-treeview-menu>li:nth-child(2)>a{ + border-top: 1px solid #d2d6de; +} + + +.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right) { + display: none!important; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>li.hover-li>a>span:not(.pull-right) { + display: block!important; + position: absolute; + width: 220px; + left: 40px; + top: -30px; + margin-left: -3px; + padding-left: 10px; + padding-right: 30px; + padding-bottom: 30px; + padding-top: 30px; + background-color: transparent!important; +} + +.li-header-span-i { + font-style: normal; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>li.hover-li .li-header-span-i { + padding: 12px 5px 12px 20px; + display: block; + background-color: #fcfcfc; + width: 180px; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li a>.li-header-span> .li-header-span-i, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview.hover-li a>.li-header-span> .li-header-span-i{ + border-bottom-right-radius: 0px!important; + box-shadow: 2px 0px 3px rgba(0,0,0,0.1); +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.li-header-span> .li-header-span-i:hover, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li .pm-treeview-menu>li>.li-header-span> .li-header-span-i:hover, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li .pm-treeview-menu>li>a:hover, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview.hover-li .pm-treeview-menu>li>a:hover { + background-color: #EEEEEE!important; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.li-header-span>.li-header-span-i, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active.hover-li .pm-treeview-menu>li>.li-header-span>.li-header-span-i { + background-color: #fcfcfc!important; + box-shadow: 2px 1px 3px rgba(0,0,0,0.1); + border-top-right-radius: 4px; +} + +.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.pm-treeview)>a>.li-header-span>.li-header-span-i, +.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.pm-treeview-active)>a>.li-header-span>.li-header-span-i { + border-bottom-right-radius: 4px; +} + +.sidebar-menu>li>ul>li { + background-color: #fcfcfc; +} + +.skin-black-light .pm-treeview-menu>li>a { + color: #333; +} + +.sidebar-menu>.pm-treeview-active>.pm-treeview-menu>.group-header-copy, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview>.pm-treeview-menu>.group-header-copy, +.sidebar-mini.sidebar-collapse .sidebar-menu>.pm-treeview-active>.pm-treeview-menu>.group-header-copy{ + display: none; +} + +.group-header-copy { + font-weight: bold; + border-bottom: 1px solid #d2d6de; + border-top-right-radius: 4px; +} + +.group-header-copy a { + border-top-right-radius: 4px; +} + +/* конец стилей для сайдбара */ + +.imported-subitems-table th, .imported-subitems-table td { + word-wrap:break-word; +} + +.imported-subitems-table ul { + margin-left:0px; + padding-left: 0px; +} + +.imported-subitems-table ul li { + list-style: none; +} + +.recipients_list ul { + margin-left:15px; + padding-left: 0px; +} + +.light-tr:hover { + background-color: #D8EDF8; + cursor: pointer; +} + +.start-time-td { + width:160px; +} + + +@media (max-width: 1200px) { + .hidden-1200 { + display: none; + } +} + +@media (max-width: 1100px) { + .hidden-1100 { + display: none; + } +} + +@media (max-width: 1050px) { + .hidden-1050 { + display: none; + } +} + +@media (max-width: 1000px) { + .hidden-1000 { + display: none; + } +} + +@media (max-width: 850px) { + .start-time-td { + width:100px; + } +} + +@media (max-width: 480px) { + .hidden-480 { + display: none; + } +} \ No newline at end of file diff --git a/polemarch/static/js/app.js b/polemarch/static/js/app.js index 9e5ac8aa..beed944f 100644 --- a/polemarch/static/js/app.js +++ b/polemarch/static/js/app.js @@ -13,7 +13,7 @@ //Make sure jQuery has been loaded before app.js if (typeof jQuery === "undefined") { - throw new Error("AdminLTE requires jQuery"); + throw new Error("AdminLTE requires jQuery"); } /* AdminLTE @@ -33,103 +33,103 @@ $.AdminLTE = {}; * Modify these options to suit your implementation */ $.AdminLTE.options = { - //Add slimscroll to navbar menus - //This requires you to load the slimscroll plugin - //in every page before app.js - navbarMenuSlimscroll: true, - navbarMenuSlimscrollWidth: "3px", //The width of the scroll bar - navbarMenuHeight: "200px", //The height of the inner menu - //General animation speed for JS animated elements such as box collapse/expand and - //sidebar treeview slide up/down. This option accepts an integer as milliseconds, - //'fast', 'normal', or 'slow' - animationSpeed: 500, - //Sidebar push menu toggle button selector - sidebarToggleSelector: "[data-toggle='offcanvas']", - //Activate sidebar push menu - sidebarPushMenu: true, - //Activate sidebar slimscroll if the fixed layout is set (requires SlimScroll Plugin) - sidebarSlimScroll: true, - //Enable sidebar expand on hover effect for sidebar mini - //This option is forced to true if both the fixed layout and sidebar mini - //are used together - sidebarExpandOnHover: false, - //BoxRefresh Plugin - enableBoxRefresh: true, - //Bootstrap.js tooltip - enableBSToppltip: true, - BSTooltipSelector: "[data-toggle='tooltip']", - //Enable Fast Click. Fastclick.js creates a more - //native touch experience with touch devices. If you - //choose to enable the plugin, make sure you load the script - //before AdminLTE's app.js - enableFastclick: false, - //Control Sidebar Tree views - enableControlTreeView: true, - //Control Sidebar Options - enableControlSidebar: true, - controlSidebarOptions: { - //Which button should trigger the open/close event - toggleBtnSelector: "[data-toggle='control-sidebar']", - //The sidebar selector - selector: ".control-sidebar", - //Enable slide over content - slide: true - }, - //Box Widget Plugin. Enable this plugin - //to allow boxes to be collapsed and/or removed - enableBoxWidget: true, - //Box Widget plugin options - boxWidgetOptions: { - boxWidgetIcons: { - //Collapse icon - collapse: 'fa-minus', - //Open icon - open: 'fa-plus', - //Remove icon - remove: 'fa-times' + //Add slimscroll to navbar menus + //This requires you to load the slimscroll plugin + //in every page before app.js + navbarMenuSlimscroll: true, + navbarMenuSlimscrollWidth: "3px", //The width of the scroll bar + navbarMenuHeight: "200px", //The height of the inner menu + //General animation speed for JS animated elements such as box collapse/expand and + //sidebar treeview slide up/down. This option accepts an integer as milliseconds, + //'fast', 'normal', or 'slow' + animationSpeed: 500, + //Sidebar push menu toggle button selector + sidebarToggleSelector: "[data-toggle='offcanvas']", + //Activate sidebar push menu + sidebarPushMenu: true, + //Activate sidebar slimscroll if the fixed layout is set (requires SlimScroll Plugin) + sidebarSlimScroll: true, + //Enable sidebar expand on hover effect for sidebar mini + //This option is forced to true if both the fixed layout and sidebar mini + //are used together + sidebarExpandOnHover: false, + //BoxRefresh Plugin + enableBoxRefresh: true, + //Bootstrap.js tooltip + enableBSToppltip: true, + BSTooltipSelector: "[data-toggle='tooltip']", + //Enable Fast Click. Fastclick.js creates a more + //native touch experience with touch devices. If you + //choose to enable the plugin, make sure you load the script + //before AdminLTE's app.js + enableFastclick: false, + //Control Sidebar Tree views + enableControlTreeView: true, + //Control Sidebar Options + enableControlSidebar: true, + controlSidebarOptions: { + //Which button should trigger the open/close event + toggleBtnSelector: "[data-toggle='control-sidebar']", + //The sidebar selector + selector: ".control-sidebar", + //Enable slide over content + slide: true }, - boxWidgetSelectors: { - //Remove button selector - remove: '[data-widget="remove"]', - //Collapse button selector - collapse: '[data-widget="collapse"]' + //Box Widget Plugin. Enable this plugin + //to allow boxes to be collapsed and/or removed + enableBoxWidget: true, + //Box Widget plugin options + boxWidgetOptions: { + boxWidgetIcons: { + //Collapse icon + collapse: 'fa-minus', + //Open icon + open: 'fa-plus', + //Remove icon + remove: 'fa-times' + }, + boxWidgetSelectors: { + //Remove button selector + remove: '[data-widget="remove"]', + //Collapse button selector + collapse: '[data-widget="collapse"]' + } + }, + //Direct Chat plugin options + directChat: { + //Enable direct chat by default + enable: true, + //The button to open and close the chat contacts pane + contactToggleSelector: '[data-widget="chat-pane-toggle"]' + }, + //Define the set of colors to use globally around the website + colors: { + lightBlue: "#3c8dbc", + red: "#f56954", + green: "#00a65a", + aqua: "#00c0ef", + yellow: "#f39c12", + blue: "#0073b7", + navy: "#001F3F", + teal: "#39CCCC", + olive: "#3D9970", + lime: "#01FF70", + orange: "#FF851B", + fuchsia: "#F012BE", + purple: "#8E24AA", + maroon: "#D81B60", + black: "#222222", + gray: "#d2d6de" + }, + //The standard screen sizes that bootstrap uses. + //If you change these in the variables.less file, change + //them here too. + screenSizes: { + xs: 480, + sm: 768, + md: 992, + lg: 1200 } - }, - //Direct Chat plugin options - directChat: { - //Enable direct chat by default - enable: true, - //The button to open and close the chat contacts pane - contactToggleSelector: '[data-widget="chat-pane-toggle"]' - }, - //Define the set of colors to use globally around the website - colors: { - lightBlue: "#3c8dbc", - red: "#f56954", - green: "#00a65a", - aqua: "#00c0ef", - yellow: "#f39c12", - blue: "#0073b7", - navy: "#001F3F", - teal: "#39CCCC", - olive: "#3D9970", - lime: "#01FF70", - orange: "#FF851B", - fuchsia: "#F012BE", - purple: "#8E24AA", - maroon: "#D81B60", - black: "#222222", - gray: "#d2d6de" - }, - //The standard screen sizes that bootstrap uses. - //If you change these in the variables.less file, change - //them here too. - screenSizes: { - xs: 480, - sm: 768, - md: 992, - lg: 1200 - } }; /* ------------------ @@ -140,90 +140,90 @@ $.AdminLTE.options = { * options above. */ $(function () { - "use strict"; - - //Fix for IE page transitions - $("body").removeClass("hold-transition"); - - //Extend options if external options exist - if (typeof AdminLTEOptions !== "undefined") { - $.extend(true, - $.AdminLTE.options, - AdminLTEOptions); - } - - //Easy access to options - var o = $.AdminLTE.options; - - //Set up the object - _init(); - - //Activate the layout maker - $.AdminLTE.layout.activate(); - - //Enable sidebar tree view controls - if (o.enableControlTreeView) { - $.AdminLTE.tree('.sidebar'); - } - - //Enable control sidebar - if (o.enableControlSidebar) { - $.AdminLTE.controlSidebar.activate(); - } - - //Add slimscroll to navbar dropdown - if (o.navbarMenuSlimscroll && typeof $.fn.slimscroll != 'undefined') { - $(".navbar .menu").slimscroll({ - height: o.navbarMenuHeight, - alwaysVisible: false, - size: o.navbarMenuSlimscrollWidth - }).css("width", "100%"); - } - - //Activate sidebar push menu - if (o.sidebarPushMenu) { - $.AdminLTE.pushMenu.activate(o.sidebarToggleSelector); - } - - //Activate Bootstrap tooltip - if (o.enableBSToppltip) { - $('body').tooltip({ - selector: o.BSTooltipSelector, - container: 'body' - }); - } - - //Activate box widget - if (o.enableBoxWidget) { - $.AdminLTE.boxWidget.activate(); - } - - //Activate fast click - if (o.enableFastclick && typeof FastClick != 'undefined') { - FastClick.attach(document.body); - } - - //Activate direct chat widget - if (o.directChat.enable) { - $(document).on('click', o.directChat.contactToggleSelector, function () { - var box = $(this).parents('.direct-chat').first(); - box.toggleClass('direct-chat-contacts-open'); - }); - } - - /* - * INITIALIZE BUTTON TOGGLE - * ------------------------ - */ - $('.btn-group[data-toggle="btn-toggle"]').each(function () { - var group = $(this); - $(this).find(".btn").on('click', function (e) { - group.find(".btn.active").removeClass("active"); - $(this).addClass("active"); - e.preventDefault(); - }); + "use strict"; + + //Fix for IE page transitions + $("body").removeClass("hold-transition"); + + //Extend options if external options exist + if (typeof AdminLTEOptions !== "undefined") { + $.extend(true, + $.AdminLTE.options, + AdminLTEOptions); + } + + //Easy access to options + var o = $.AdminLTE.options; + + //Set up the object + _init(); + + //Activate the layout maker + $.AdminLTE.layout.activate(); + + //Enable sidebar tree view controls + if (o.enableControlTreeView) { + $.AdminLTE.tree('.sidebar'); + } + + //Enable control sidebar + if (o.enableControlSidebar) { + $.AdminLTE.controlSidebar.activate(); + } + + //Add slimscroll to navbar dropdown + if (o.navbarMenuSlimscroll && typeof $.fn.slimscroll != 'undefined') { + $(".navbar .menu").slimscroll({ + height: o.navbarMenuHeight, + alwaysVisible: false, + size: o.navbarMenuSlimscrollWidth + }).css("width", "100%"); + } - }); + //Activate sidebar push menu + if (o.sidebarPushMenu) { + $.AdminLTE.pushMenu.activate(o.sidebarToggleSelector); + } + + //Activate Bootstrap tooltip + if (o.enableBSToppltip) { + $('body').tooltip({ + selector: o.BSTooltipSelector, + container: 'body' + }); + } + + //Activate box widget + if (o.enableBoxWidget) { + $.AdminLTE.boxWidget.activate(); + } + + //Activate fast click + if (o.enableFastclick && typeof FastClick != 'undefined') { + FastClick.attach(document.body); + } + + //Activate direct chat widget + if (o.directChat.enable) { + $(document).on('click', o.directChat.contactToggleSelector, function () { + var box = $(this).parents('.direct-chat').first(); + box.toggleClass('direct-chat-contacts-open'); + }); + } + + /* + * INITIALIZE BUTTON TOGGLE + * ------------------------ + */ + $('.btn-group[data-toggle="btn-toggle"]').each(function () { + var group = $(this); + $(this).find(".btn").on('click', function (e) { + group.find(".btn.active").removeClass("active"); + $(this).addClass("active"); + e.preventDefault(); + }); + + }); }); /* ---------------------------------- @@ -232,379 +232,379 @@ $(function () { * All AdminLTE functions are implemented below. */ function _init() { - 'use strict'; - /* Layout - * ====== - * Fixes the layout height in case min-height fails. - * - * @type Object - * @usage $.AdminLTE.layout.activate() - * $.AdminLTE.layout.fix() - * $.AdminLTE.layout.fixSidebar() - */ - $.AdminLTE.layout = { - activate: function () { - var _this = this; - _this.fix(); - _this.fixSidebar(); - $('body, html, .wrapper').css('height', 'auto'); - $(window, ".wrapper").resize(function () { - _this.fix(); - _this.fixSidebar(); - }); - }, - fix: function () { - // Remove overflow from .wrapper if layout-boxed exists - $(".layout-boxed > .wrapper").css('overflow', 'hidden'); - //Get window height and the wrapper height - var footer_height = $('.main-footer').outerHeight() || 0; - var neg = $('.main-header').outerHeight() + footer_height; - var window_height = $(window).height(); - var sidebar_height = $(".sidebar").height() || 0; - //Set the min-height of the content and sidebar based on the - //the height of the document. - if ($("body").hasClass("fixed")) { - $(".content-wrapper, .right-side").css('min-height', window_height - footer_height); - } else { - var postSetWidth; - if (window_height >= sidebar_height) { - $(".content-wrapper, .right-side").css('min-height', window_height - neg); - postSetWidth = window_height - neg; - } else { - $(".content-wrapper, .right-side").css('min-height', sidebar_height); - postSetWidth = sidebar_height; - } - - //Fix for the control sidebar height - var controlSidebar = $($.AdminLTE.options.controlSidebarOptions.selector); - if (typeof controlSidebar !== "undefined") { - if (controlSidebar.height() > postSetWidth) - $(".content-wrapper, .right-side").css('min-height', controlSidebar.height()); + 'use strict'; + /* Layout + * ====== + * Fixes the layout height in case min-height fails. + * + * @type Object + * @usage $.AdminLTE.layout.activate() + * $.AdminLTE.layout.fix() + * $.AdminLTE.layout.fixSidebar() + */ + $.AdminLTE.layout = { + activate: function () { + var _this = this; + _this.fix(); + _this.fixSidebar(); + $('body, html, .wrapper').css('height', 'auto'); + $(window, ".wrapper").resize(function () { + _this.fix(); + _this.fixSidebar(); + }); + }, + fix: function () { + // Remove overflow from .wrapper if layout-boxed exists + $(".layout-boxed > .wrapper").css('overflow', 'hidden'); + //Get window height and the wrapper height + var footer_height = $('.main-footer').outerHeight() || 0; + var neg = $('.main-header').outerHeight() + footer_height; + var window_height = $(window).height(); + var sidebar_height = $(".sidebar").height() || 0; + //Set the min-height of the content and sidebar based on the + //the height of the document. + if ($("body").hasClass("fixed")) { + $(".content-wrapper, .right-side").css('min-height', window_height - footer_height); + } else { + var postSetWidth; + if (window_height >= sidebar_height) { + $(".content-wrapper, .right-side").css('min-height', window_height - neg); + postSetWidth = window_height - neg; + } else { + $(".content-wrapper, .right-side").css('min-height', sidebar_height); + postSetWidth = sidebar_height; + } + + //Fix for the control sidebar height + var controlSidebar = $($.AdminLTE.options.controlSidebarOptions.selector); + if (typeof controlSidebar !== "undefined") { + if (controlSidebar.height() > postSetWidth) + $(".content-wrapper, .right-side").css('min-height', controlSidebar.height()); + } + + } + }, + fixSidebar: function () { + //Make sure the body tag has the .fixed class + if (!$("body").hasClass("fixed")) { + if (typeof $.fn.slimScroll != 'undefined') { + $(".sidebar").slimScroll({destroy: true}).height("auto"); + } + return; + } else if (typeof $.fn.slimScroll == 'undefined' && window.console) { + window.console.error("Error: the fixed layout requires the slimscroll plugin!"); + } + //Enable slimscroll for fixed layout + if ($.AdminLTE.options.sidebarSlimScroll) { + if (typeof $.fn.slimScroll != 'undefined') { + //Destroy if it exists + $(".sidebar").slimScroll({destroy: true}).height("auto"); + //Add slimscroll + $(".sidebar").slimScroll({ + height: ($(window).height() - $(".main-header").height()) + "px", + color: "rgba(0,0,0,0.2)", + size: "3px" + }); + } + } } - - } - }, - fixSidebar: function () { - //Make sure the body tag has the .fixed class - if (!$("body").hasClass("fixed")) { - if (typeof $.fn.slimScroll != 'undefined') { - $(".sidebar").slimScroll({destroy: true}).height("auto"); - } - return; - } else if (typeof $.fn.slimScroll == 'undefined' && window.console) { - window.console.error("Error: the fixed layout requires the slimscroll plugin!"); - } - //Enable slimscroll for fixed layout - if ($.AdminLTE.options.sidebarSlimScroll) { - if (typeof $.fn.slimScroll != 'undefined') { - //Destroy if it exists - $(".sidebar").slimScroll({destroy: true}).height("auto"); - //Add slimscroll - $(".sidebar").slimScroll({ - height: ($(window).height() - $(".main-header").height()) + "px", - color: "rgba(0,0,0,0.2)", - size: "3px" - }); - } - } - } - }; - - /* PushMenu() - * ========== - * Adds the push menu functionality to the sidebar. - * - * @type Function - * @usage: $.AdminLTE.pushMenu("[data-toggle='offcanvas']") - */ - $.AdminLTE.pushMenu = { - activate: function (toggleBtn) { - //Get the screen sizes - var screenSizes = $.AdminLTE.options.screenSizes; - - //Enable sidebar toggle - $(document).on('click', toggleBtn, function (e) { - e.preventDefault(); - - //Enable sidebar push menu - if ($(window).width() > (screenSizes.sm - 1)) { - if ($("body").hasClass('sidebar-collapse')) - { - $("body").removeClass('sidebar-collapse').trigger('expanded.pushMenu'); - pmLocalSettings.set('hideMenu', false); - } - else - { - $("body").addClass('sidebar-collapse').trigger('collapsed.pushMenu'); - pmLocalSettings.set('hideMenu', true); - } + }; + + /* PushMenu() + * ========== + * Adds the push menu functionality to the sidebar. + * + * @type Function + * @usage: $.AdminLTE.pushMenu("[data-toggle='offcanvas']") + */ + $.AdminLTE.pushMenu = { + activate: function (toggleBtn) { + //Get the screen sizes + var screenSizes = $.AdminLTE.options.screenSizes; + + //Enable sidebar toggle + $(document).on('click', toggleBtn, function (e) { + e.preventDefault(); + + //Enable sidebar push menu + if (window.innerWidth > (screenSizes.sm - 1)) { + if ($("body").hasClass('sidebar-collapse')) + { + $("body").removeClass('sidebar-collapse').trigger('expanded.pushMenu'); + pmLocalSettings.set('hideMenu', false); + } + else + { + $("body").addClass('sidebar-collapse').trigger('collapsed.pushMenu'); + pmLocalSettings.set('hideMenu', true); + } + } + //Handle sidebar push menu for small screens + else { + if ($("body").hasClass('sidebar-open')) { + $("body").removeClass('sidebar-open').trigger('collapsed.pushMenu'); + } else { + $("body").addClass('sidebar-open').trigger('expanded.pushMenu'); + } + } + }); + + $(".content-wrapper").click(function () { + //Enable hide menu when clicking on the content-wrapper on small screens + if (window.innerWidth <= (screenSizes.sm - 1) && $("body").hasClass("sidebar-open")) { + $("body").removeClass('sidebar-open'); + } + }); + + //Enable expand on hover for sidebar mini + if ($.AdminLTE.options.sidebarExpandOnHover + || ($('body').hasClass('fixed') + && $('body').hasClass('sidebar-mini'))) { + this.expandOnHover(); + } + }, + expandOnHover: function () { + var _this = this; + var screenWidth = $.AdminLTE.options.screenSizes.sm - 1; + //Expand sidebar on hover + $('.main-sidebar').hover(function () { + if ($('body').hasClass('sidebar-mini') + && $("body").hasClass('sidebar-collapse') + && $(window).width() > screenWidth) { + _this.expand(); + } + }, function () { + if ($('body').hasClass('sidebar-mini') + && $('body').hasClass('sidebar-expanded-on-hover') + && $(window).width() > screenWidth) { + _this.collapse(); + } + }); + }, + expand: function () { + $("body").removeClass('sidebar-collapse').addClass('sidebar-expanded-on-hover'); + }, + collapse: function () { + if ($('body').hasClass('sidebar-expanded-on-hover')) { + $('body').removeClass('sidebar-expanded-on-hover').addClass('sidebar-collapse'); + } } - //Handle sidebar push menu for small screens - else { - if ($("body").hasClass('sidebar-open')) { - $("body").removeClass('sidebar-open').removeClass('sidebar-collapse').trigger('collapsed.pushMenu'); - } else { - $("body").addClass('sidebar-open').trigger('expanded.pushMenu'); - } + }; + + /* Tree() + * ====== + * Converts the sidebar into a multilevel + * tree view menu. + * + * @type Function + * @Usage: $.AdminLTE.tree('.sidebar') + */ + $.AdminLTE.tree = function (menu) { + var _this = this; + var animationSpeed = $.AdminLTE.options.animationSpeed; + $(document).off('click', menu + ' li a') + .on('click', menu + ' li a', function (e) { + //Get the clicked link and the next element + var $this = $(this); + var checkElement = $this.next(); + + //Check if the next element is a menu and is visible + if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')) && (!$('body').hasClass('sidebar-collapse'))) { + //Close the menu + checkElement.slideUp(animationSpeed, function () { + checkElement.removeClass('menu-open'); + //Fix the layout in case the sidebar stretches over the height of the window + //_this.layout.fix(); + }); + checkElement.parent("li").removeClass("active"); + } + //If the menu is not visible + else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible'))) { + //Get the parent menu + var parent = $this.parents('ul').first(); + //Close all open menus within the parent + var ul = parent.find('ul:visible').slideUp(animationSpeed); + //Remove the menu-open class from the parent + ul.removeClass('menu-open'); + //Get the parent li + var parent_li = $this.parent("li"); + + //Open the target menu and add the menu-open class + checkElement.slideDown(animationSpeed, function () { + //Add the class active to the parent li + checkElement.addClass('menu-open'); + parent.find('li.active').removeClass('active'); + parent_li.addClass('active'); + //Fix the layout in case the sidebar stretches over the height of the window + _this.layout.fix(); + }); + } + //if this isn't a link, prevent the page from being redirected + if (checkElement.is('.treeview-menu')) { + e.preventDefault(); + } + }); + }; + + /* ControlSidebar + * ============== + * Adds functionality to the right sidebar + * + * @type Object + * @usage $.AdminLTE.controlSidebar.activate(options) + */ + $.AdminLTE.controlSidebar = { + //instantiate the object + activate: function () { + //Get the object + var _this = this; + //Update options + var o = $.AdminLTE.options.controlSidebarOptions; + //Get the sidebar + var sidebar = $(o.selector); + //The toggle button + var btn = $(o.toggleBtnSelector); + + //Listen to the click event + btn.on('click', function (e) { + e.preventDefault(); + //If the sidebar is not open + if (!sidebar.hasClass('control-sidebar-open') + && !$('body').hasClass('control-sidebar-open')) { + //Open the sidebar + _this.open(sidebar, o.slide); + } else { + _this.close(sidebar, o.slide); + } + }); + + //If the body has a boxed layout, fix the sidebar bg position + var bg = $(".control-sidebar-bg"); + _this._fix(bg); + + //If the body has a fixed layout, make the control sidebar fixed + if ($('body').hasClass('fixed')) { + _this._fixForFixed(sidebar); + } else { + //If the content height is less than the sidebar's height, force max height + if ($('.content-wrapper, .right-side').height() < sidebar.height()) { + _this._fixForContent(sidebar); + } + } + }, + //Open the control sidebar + open: function (sidebar, slide) { + //Slide over content + if (slide) { + sidebar.addClass('control-sidebar-open'); + } else { + //Push the content by adding the open class to the body instead + //of the sidebar itself + $('body').addClass('control-sidebar-open'); + } + }, + //Close the control sidebar + close: function (sidebar, slide) { + if (slide) { + sidebar.removeClass('control-sidebar-open'); + } else { + $('body').removeClass('control-sidebar-open'); + } + }, + _fix: function (sidebar) { + var _this = this; + if ($("body").hasClass('layout-boxed')) { + sidebar.css('position', 'absolute'); + sidebar.height($(".wrapper").height()); + if (_this.hasBindedResize) { + return; + } + $(window).resize(function () { + _this._fix(sidebar); + }); + _this.hasBindedResize = true; + } else { + sidebar.css({ + 'position': 'fixed', + 'height': 'auto' + }); + } + }, + _fixForFixed: function (sidebar) { + sidebar.css({ + 'position': 'fixed', + 'max-height': '100%', + 'overflow': 'auto', + 'padding-bottom': '50px' + }); + }, + _fixForContent: function (sidebar) { + $(".content-wrapper, .right-side").css('min-height', sidebar.height()); } - }); - - $(".content-wrapper").click(function () { - //Enable hide menu when clicking on the content-wrapper on small screens - if ($(window).width() <= (screenSizes.sm - 1) && $("body").hasClass("sidebar-open")) { - $("body").removeClass('sidebar-open'); - } - }); - - //Enable expand on hover for sidebar mini - if ($.AdminLTE.options.sidebarExpandOnHover - || ($('body').hasClass('fixed') - && $('body').hasClass('sidebar-mini'))) { - this.expandOnHover(); - } - }, - expandOnHover: function () { - var _this = this; - var screenWidth = $.AdminLTE.options.screenSizes.sm - 1; - //Expand sidebar on hover - $('.main-sidebar').hover(function () { - if ($('body').hasClass('sidebar-mini') - && $("body").hasClass('sidebar-collapse') - && $(window).width() > screenWidth) { - _this.expand(); + }; + + /* BoxWidget + * ========= + * BoxWidget is a plugin to handle collapsing and + * removing boxes from the screen. + * + * @type Object + * @usage $.AdminLTE.boxWidget.activate() + * Set all your options in the main $.AdminLTE.options object + */ + $.AdminLTE.boxWidget = { + selectors: $.AdminLTE.options.boxWidgetOptions.boxWidgetSelectors, + icons: $.AdminLTE.options.boxWidgetOptions.boxWidgetIcons, + animationSpeed: $.AdminLTE.options.animationSpeed, + activate: function (_box) { + var _this = this; + if (!_box) { + _box = document; // activate all boxes per default + } + //Listen for collapse event triggers + $(_box).on('click', _this.selectors.collapse, function (e) { + e.preventDefault(); + _this.collapse($(this)); + }); + + //Listen for remove event triggers + $(_box).on('click', _this.selectors.remove, function (e) { + e.preventDefault(); + _this.remove($(this)); + }); + }, + collapse: function (element) { + var _this = this; + //Find the box parent + var box = element.parents(".box").first(); + //Find the body and the footer + var box_content = box.find("> .box-body, > .box-footer, > form >.box-body, > form > .box-footer"); + if (!box.hasClass("collapsed-box")) { + //Convert minus into plus + element.children(":first") + .removeClass(_this.icons.collapse) + .addClass(_this.icons.open); + //Hide the content + box_content.slideUp(_this.animationSpeed, function () { + box.addClass("collapsed-box"); + }); + } else { + //Convert plus into minus + element.children(":first") + .removeClass(_this.icons.open) + .addClass(_this.icons.collapse); + //Show the content + box_content.slideDown(_this.animationSpeed, function () { + box.removeClass("collapsed-box"); + }); + } + }, + remove: function (element) { + //Find the box parent + var box = element.parents(".box").first(); + box.slideUp(this.animationSpeed); } - }, function () { - if ($('body').hasClass('sidebar-mini') - && $('body').hasClass('sidebar-expanded-on-hover') - && $(window).width() > screenWidth) { - _this.collapse(); - } - }); - }, - expand: function () { - $("body").removeClass('sidebar-collapse').addClass('sidebar-expanded-on-hover'); - }, - collapse: function () { - if ($('body').hasClass('sidebar-expanded-on-hover')) { - $('body').removeClass('sidebar-expanded-on-hover').addClass('sidebar-collapse'); - } - } - }; - - /* Tree() - * ====== - * Converts the sidebar into a multilevel - * tree view menu. - * - * @type Function - * @Usage: $.AdminLTE.tree('.sidebar') - */ - $.AdminLTE.tree = function (menu) { - var _this = this; - var animationSpeed = $.AdminLTE.options.animationSpeed; - $(document).off('click', menu + ' li a') - .on('click', menu + ' li a', function (e) { - //Get the clicked link and the next element - var $this = $(this); - var checkElement = $this.next(); - - //Check if the next element is a menu and is visible - if ((checkElement.is('.treeview-menu')) && (checkElement.is(':visible')) && (!$('body').hasClass('sidebar-collapse'))) { - //Close the menu - checkElement.slideUp(animationSpeed, function () { - checkElement.removeClass('menu-open'); - //Fix the layout in case the sidebar stretches over the height of the window - //_this.layout.fix(); - }); - checkElement.parent("li").removeClass("active"); - } - //If the menu is not visible - else if ((checkElement.is('.treeview-menu')) && (!checkElement.is(':visible'))) { - //Get the parent menu - var parent = $this.parents('ul').first(); - //Close all open menus within the parent - var ul = parent.find('ul:visible').slideUp(animationSpeed); - //Remove the menu-open class from the parent - ul.removeClass('menu-open'); - //Get the parent li - var parent_li = $this.parent("li"); - - //Open the target menu and add the menu-open class - checkElement.slideDown(animationSpeed, function () { - //Add the class active to the parent li - checkElement.addClass('menu-open'); - parent.find('li.active').removeClass('active'); - parent_li.addClass('active'); - //Fix the layout in case the sidebar stretches over the height of the window - _this.layout.fix(); - }); - } - //if this isn't a link, prevent the page from being redirected - if (checkElement.is('.treeview-menu')) { - e.preventDefault(); - } - }); - }; - - /* ControlSidebar - * ============== - * Adds functionality to the right sidebar - * - * @type Object - * @usage $.AdminLTE.controlSidebar.activate(options) - */ - $.AdminLTE.controlSidebar = { - //instantiate the object - activate: function () { - //Get the object - var _this = this; - //Update options - var o = $.AdminLTE.options.controlSidebarOptions; - //Get the sidebar - var sidebar = $(o.selector); - //The toggle button - var btn = $(o.toggleBtnSelector); - - //Listen to the click event - btn.on('click', function (e) { - e.preventDefault(); - //If the sidebar is not open - if (!sidebar.hasClass('control-sidebar-open') - && !$('body').hasClass('control-sidebar-open')) { - //Open the sidebar - _this.open(sidebar, o.slide); - } else { - _this.close(sidebar, o.slide); - } - }); - - //If the body has a boxed layout, fix the sidebar bg position - var bg = $(".control-sidebar-bg"); - _this._fix(bg); - - //If the body has a fixed layout, make the control sidebar fixed - if ($('body').hasClass('fixed')) { - _this._fixForFixed(sidebar); - } else { - //If the content height is less than the sidebar's height, force max height - if ($('.content-wrapper, .right-side').height() < sidebar.height()) { - _this._fixForContent(sidebar); - } - } - }, - //Open the control sidebar - open: function (sidebar, slide) { - //Slide over content - if (slide) { - sidebar.addClass('control-sidebar-open'); - } else { - //Push the content by adding the open class to the body instead - //of the sidebar itself - $('body').addClass('control-sidebar-open'); - } - }, - //Close the control sidebar - close: function (sidebar, slide) { - if (slide) { - sidebar.removeClass('control-sidebar-open'); - } else { - $('body').removeClass('control-sidebar-open'); - } - }, - _fix: function (sidebar) { - var _this = this; - if ($("body").hasClass('layout-boxed')) { - sidebar.css('position', 'absolute'); - sidebar.height($(".wrapper").height()); - if (_this.hasBindedResize) { - return; - } - $(window).resize(function () { - _this._fix(sidebar); - }); - _this.hasBindedResize = true; - } else { - sidebar.css({ - 'position': 'fixed', - 'height': 'auto' - }); - } - }, - _fixForFixed: function (sidebar) { - sidebar.css({ - 'position': 'fixed', - 'max-height': '100%', - 'overflow': 'auto', - 'padding-bottom': '50px' - }); - }, - _fixForContent: function (sidebar) { - $(".content-wrapper, .right-side").css('min-height', sidebar.height()); - } - }; - - /* BoxWidget - * ========= - * BoxWidget is a plugin to handle collapsing and - * removing boxes from the screen. - * - * @type Object - * @usage $.AdminLTE.boxWidget.activate() - * Set all your options in the main $.AdminLTE.options object - */ - $.AdminLTE.boxWidget = { - selectors: $.AdminLTE.options.boxWidgetOptions.boxWidgetSelectors, - icons: $.AdminLTE.options.boxWidgetOptions.boxWidgetIcons, - animationSpeed: $.AdminLTE.options.animationSpeed, - activate: function (_box) { - var _this = this; - if (!_box) { - _box = document; // activate all boxes per default - } - //Listen for collapse event triggers - $(_box).on('click', _this.selectors.collapse, function (e) { - e.preventDefault(); - _this.collapse($(this)); - }); - - //Listen for remove event triggers - $(_box).on('click', _this.selectors.remove, function (e) { - e.preventDefault(); - _this.remove($(this)); - }); - }, - collapse: function (element) { - var _this = this; - //Find the box parent - var box = element.parents(".box").first(); - //Find the body and the footer - var box_content = box.find("> .box-body, > .box-footer, > form >.box-body, > form > .box-footer"); - if (!box.hasClass("collapsed-box")) { - //Convert minus into plus - element.children(":first") - .removeClass(_this.icons.collapse) - .addClass(_this.icons.open); - //Hide the content - box_content.slideUp(_this.animationSpeed, function () { - box.addClass("collapsed-box"); - }); - } else { - //Convert plus into minus - element.children(":first") - .removeClass(_this.icons.open) - .addClass(_this.icons.collapse); - //Show the content - box_content.slideDown(_this.animationSpeed, function () { - box.removeClass("collapsed-box"); - }); - } - }, - remove: function (element) { - //Find the box parent - var box = element.parents(".box").first(); - box.slideUp(this.animationSpeed); - } - }; + }; } /* ------------------ @@ -624,70 +624,70 @@ function _init() { */ (function ($) { - "use strict"; - - $.fn.boxRefresh = function (options) { - - // Render options - var settings = $.extend({ - //Refresh button selector - trigger: ".refresh-btn", - //File source to be loaded (e.g: ajax/src.php) - source: "", - //Callbacks - onLoadStart: function (box) { - return box; - }, //Right after the button has been clicked - onLoadDone: function (box) { - return box; - } //When the source has been loaded - - }, options); - - //The overlay - var overlay = $('
    '); - - return this.each(function () { - //if a source is specified - if (settings.source === "") { - if (window.console) { - window.console.log("Please specify a source first - boxRefresh()"); - } - return; - } - //the box - var box = $(this); - //the button - var rBtn = box.find(settings.trigger).first(); - - //On trigger click - rBtn.on('click', function (e) { - e.preventDefault(); - //Add loading overlay - start(box); - - //Perform ajax call - box.find(".box-body").load(settings.source, function () { - done(box); + "use strict"; + + $.fn.boxRefresh = function (options) { + + // Render options + var settings = $.extend({ + //Refresh button selector + trigger: ".refresh-btn", + //File source to be loaded (e.g: ajax/src.php) + source: "", + //Callbacks + onLoadStart: function (box) { + return box; + }, //Right after the button has been clicked + onLoadDone: function (box) { + return box; + } //When the source has been loaded + + }, options); + + //The overlay + var overlay = $('
    '); + + return this.each(function () { + //if a source is specified + if (settings.source === "") { + if (window.console) { + window.console.log("Please specify a source first - boxRefresh()"); + } + return; + } + //the box + var box = $(this); + //the button + var rBtn = box.find(settings.trigger).first(); + + //On trigger click + rBtn.on('click', function (e) { + e.preventDefault(); + //Add loading overlay + start(box); + + //Perform ajax call + box.find(".box-body").load(settings.source, function () { + done(box); + }); + }); }); - }); - }); - function start(box) { - //Add overlay and loading img - box.append(overlay); + function start(box) { + //Add overlay and loading img + box.append(overlay); - settings.onLoadStart.call(box); - } + settings.onLoadStart.call(box); + } - function done(box) { - //Remove overlay and loading img - box.find(overlay).remove(); + function done(box) { + //Remove overlay and loading img + box.find(overlay).remove(); - settings.onLoadDone.call(box); - } + settings.onLoadDone.call(box); + } - }; + }; })(jQuery); @@ -704,21 +704,21 @@ function _init() { */ (function ($) { - 'use strict'; + 'use strict'; - $.fn.activateBox = function () { - $.AdminLTE.boxWidget.activate(this); - }; + $.fn.activateBox = function () { + $.AdminLTE.boxWidget.activate(this); + }; - $.fn.toggleBox = function () { - var button = $($.AdminLTE.boxWidget.selectors.collapse, this); - $.AdminLTE.boxWidget.collapse(button); - }; + $.fn.toggleBox = function () { + var button = $($.AdminLTE.boxWidget.selectors.collapse, this); + $.AdminLTE.boxWidget.collapse(button); + }; - $.fn.removeBox = function () { - var button = $($.AdminLTE.boxWidget.selectors.remove, this); - $.AdminLTE.boxWidget.remove(button); - }; + $.fn.removeBox = function () { + var button = $($.AdminLTE.boxWidget.selectors.remove, this); + $.AdminLTE.boxWidget.remove(button); + }; })(jQuery); \ No newline at end of file diff --git a/polemarch/static/js/common.js b/polemarch/static/js/common.js index 51b04ee6..9ce8d8f5 100644 --- a/polemarch/static/js/common.js +++ b/polemarch/static/js/common.js @@ -1,78 +1,78 @@ function loadQUnitTests() { - - $('body').append(''); - + + $('body').append(''); + var intervaId = setInterval(function() { if(window.injectQunit !== undefined) { clearInterval(intervaId) injectQunit() - } + } }, 1000) } function addslashes(string) { return string.replace(/\\/g, '\\\\'). - replace(/\u0008/g, '\\b'). - replace(/\t/g, '\\t'). - replace(/\n/g, '\\n'). - replace(/\f/g, '\\f'). - //replace(/\r/g, '\\r'). - //replace(/\a/g, '\\a'). - replace(/\v/g, '\\v'). - //replace(/\e/g, '\\e'). - replace(/'/g, '\\\''). - replace(/"/g, '\\"'); + replace(/\u0008/g, '\\b'). + replace(/\t/g, '\\t'). + replace(/\n/g, '\\n'). + replace(/\f/g, '\\f'). + //replace(/\r/g, '\\r'). + //replace(/\a/g, '\\a'). + replace(/\v/g, '\\v'). + //replace(/\e/g, '\\e'). + replace(/'/g, '\\\''). + replace(/"/g, '\\"'); } function stripslashes (str) { - // discuss at: http://locutus.io/php/stripslashes/ - // original by: Kevin van Zonneveld (http://kvz.io) - // improved by: Ates Goral (http://magnetiq.com) - // improved by: marrtins - // improved by: rezna - // fixed by: Mick@el - // bugfixed by: Onno Marsman (https://twitter.com/onnomarsman) - // bugfixed by: Brett Zamir (http://brett-zamir.me) - // input by: Rick Waldron - // input by: Brant Messenger (http://www.brantmessenger.com/) - // reimplemented by: Brett Zamir (http://brett-zamir.me) - // example 1: stripslashes('Kevin\'s code') - // returns 1: "Kevin's code" - // example 2: stripslashes('Kevin\\\'s code') - // returns 2: "Kevin\'s code" - return (str + '') - .replace(/\\(.?)/g, function (s, n1) { - switch (n1) { - case '\\': - return '\\' - case '0': - return '\u0000' - case 't': - return "\t" - case 'n': - return "\n" - case 'f': - return "\f" - //case 'e': - // return "\e" - case 'v': - return "\v" - //case 'a': - // return "\a" - case 'b': - return "\b" - //case 'r': - // return "\r" - case '': - return '' - default: - return n1 - } - }) + // discuss at: http://locutus.io/php/stripslashes/ + // original by: Kevin van Zonneveld (http://kvz.io) + // improved by: Ates Goral (http://magnetiq.com) + // improved by: marrtins + // improved by: rezna + // fixed by: Mick@el + // bugfixed by: Onno Marsman (https://twitter.com/onnomarsman) + // bugfixed by: Brett Zamir (http://brett-zamir.me) + // input by: Rick Waldron + // input by: Brant Messenger (http://www.brantmessenger.com/) + // reimplemented by: Brett Zamir (http://brett-zamir.me) + // example 1: stripslashes('Kevin\'s code') + // returns 1: "Kevin's code" + // example 2: stripslashes('Kevin\\\'s code') + // returns 2: "Kevin\'s code" + return (str + '') + .replace(/\\(.?)/g, function (s, n1) { + switch (n1) { + case '\\': + return '\\' + case '0': + return '\u0000' + case 't': + return "\t" + case 'n': + return "\n" + case 'f': + return "\f" + //case 'e': + // return "\e" + case 'v': + return "\v" + //case 'a': + // return "\a" + case 'b': + return "\b" + //case 'r': + // return "\r" + case '': + return '' + default: + return n1 + } + }) } /** * Тестовый тест, чтоб было видно что тесты вообще хоть как то работают. @@ -85,7 +85,7 @@ function trim(s) function inheritance(obj, constructor) -{ +{ var object = undefined; var item = function() { @@ -93,12 +93,12 @@ function inheritance(obj, constructor) { return constructor.apply(jQuery.extend(true, item, object), arguments); } - + return jQuery.extend(true, item, object); } - + object = jQuery.extend(true, item, obj) - + return object } @@ -113,24 +113,26 @@ var pmLocalSettings = { tabSignal.emit('pmLocalSettings.'+name, {type:'set', name:name, value:value}) } } - + if(window.localStorage['pmLocalSettings']) { try{ pmLocalSettings.__settings = window.localStorage['pmLocalSettings']; pmLocalSettings.__settings = JSON.parse(pmLocalSettings.__settings) - + }catch (e) { - + } } if(pmLocalSettings.get('hideMenu')) { - $("body").addClass('sidebar-collapse') + if(window.innerWidth>767){ + $("body").addClass('sidebar-collapse'); + } } @@ -139,14 +141,182 @@ function toIdString(str) return str.replace(/[^A-z0-9\-]/img, "_").replace(/[\[\]]/gi, "_"); } -function hidemodal() { - - +function hidemodal() +{ var def= new $.Deferred(); $(".modal.fade.in").on('hidden.bs.modal', function (e) { - def.resolve(); + def.resolve(); }) $(".modal.fade.in").modal('hide'); return def.promise(); -} \ No newline at end of file +} + + +function capitalizeString(string) +{ + return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); +} + +function isEmptyObject(obj) { + for (var i in obj) { + if (obj.hasOwnProperty(i)) { + return false; + } + } + return true; +} + +window.onresize=function () +{ + if(window.innerWidth>767) + { + if(pmLocalSettings.get('hideMenu')) + { + $("body").addClass('sidebar-collapse'); + } + if ($("body").hasClass('sidebar-open')) + { + $("body").removeClass('sidebar-open'); + } + } + else + { + if ($("body").hasClass('sidebar-collapse')){ + $("body").removeClass('sidebar-collapse'); + } + } +} +function setActiveMenuLiBase() +{ + if(/\?projects/.test(window.location.href) || /\?project/.test(window.location.href) || + /\?new-project/.test(window.location.href)) + { + $("#menu-projects").addClass("pm-treeview-active active active-li active-bold"); + $("#menu-projects-projects").addClass("active-bold"); + $("#menu-projects").removeClass("pm-treeview"); + } + else if(/\?templates/.test(window.location.href) || + /\?template/.test(window.location.href)) + { + $("#menu-projects").addClass("pm-treeview-active active active-li"); + $("#menu-projects-templates").addClass("active-bold"); + $("#menu-projects").removeClass("pm-treeview"); + } + else if(/\?hosts/.test(window.location.href) || /\?host/.test(window.location.href) || + /\?new-host/.test(window.location.href)) + { + $("#menu-inventories").addClass("pm-treeview-active active active-li"); + $("#menu-inventories-hosts").addClass("active-bold"); + $("#menu-inventories").removeClass("pm-treeview"); + } + else if(/\?new-group/.test(window.location.href) || /\?groups/.test(window.location.href) || + /\?group/.test(window.location.href)) + { + $("#menu-inventories").addClass("pm-treeview-active active active-li"); + $("#menu-inventories-groups").addClass("active-bold"); + $("#menu-inventories").removeClass("pm-treeview"); + } + else if(/\?inventories/.test(window.location.href) || /\?inventory/.test(window.location.href) || + /\?new-inventory/.test(window.location.href)) + { + $("#menu-inventories").addClass("pm-treeview-active active active-li active-bold"); + $("#menu-inventories-inventories").addClass("active-bold"); + $("#menu-inventories").removeClass("pm-treeview"); + } + else if(/\?history/.test(window.location.href)){ + + $("#menu-history").addClass("active active-li active-bold"); + } + else if(/\?hooks/.test(window.location.href) || /\?hook/.test(window.location.href) || + /\?new-hook/.test(window.location.href)) + { + $("#menu-system").addClass("pm-treeview-active active active-li"); + $("#menu-system-hooks").addClass("active-bold"); + $("#menu-system").removeClass("pm-treeview"); + } + else if(/\?users/.test(window.location.href) || /\?user/.test(window.location.href) || + /\?new-user/.test(window.location.href) || /\?profile/.test(window.location.href)) + { + $("#menu-system").addClass("pm-treeview-active active active-li"); + $("#menu-system-users").addClass("active-bold"); + $("#menu-system").removeClass("pm-treeview"); + } + else + { + $("#menu-home").addClass("active active-li active-bold"); + } +} + +function setActiveMenuLi() +{ + if($('li').is('.pm-treeview-active')) + { + var t=$(".pm-treeview-active"); + $(t).addClass("pm-treeview"); + $(t).removeClass("pm-treeview-active"); + } + + if($('li').is('.active-li')) + { + var t=$(".active-li"); + $(t).removeClass("active"); + $(t).removeClass("active-li"); + } + + if($('li').is('.active-bold')) + { + var g=$(".active-bold"); + $(g).removeClass("active-bold"); + } + + return setActiveMenuLiBase(); +} + +/* + * Функция добавляет элементу меню (при наведении на него) + * css-класс hover-li, который добавляет необходимые стили. + * Добавление класса происходит не сразу, а после небольшой паузы. + * Это необходимо для того, чтобы выпавшее подменю быстро не пропадало + * при попытке навести курсор на него. + */ +$(".sidebar-menu > li").mouseenter(function () { + var thisEl = this; + setTimeout(function () { + var pmTreeviewMenues = $(".pm-treeview-menu"); + var bool = false; + for(var i=0; i 0) { offset = this.pageSize*(data.reg[2] - 1); - } else { + } + else + { offset=0; } var search = this.searchStringToObject(decodeURIComponent(data.reg[1]), 'mode') return $.when(this.sendSearchQuery(search,limit,offset)).done(function() { - $(holder).insertTpl(spajs.just.render(thisObj.model.name+'_list', {query:decodeURIComponent(data.reg[1])})) + $.when($(holder).insertTpl(spajs.just.render(thisObj.model.name+'_list', {query:decodeURIComponent(data.reg[1])}))).done(function(){ + pmHistory.setTableRowLinkInLink(); + }) }).fail(function() { $.notify("", "error"); @@ -68,6 +72,15 @@ pmHistory.search = function(query, options) return spajs.open({ menuId:'project/' + options.project_id +"/" + this.model.name+"/search/"+this.searchObjectToString(trim(query), 'mode'), reopen:true}); } + else if(options.initiator_type && options.initiator_type=='template') + { + if(this.isEmptySearchQuery(query)) + { + return spajs.open({ menuId:'template/' + options.kind + '/' + options.initiator +"/" + this.model.name, reopen:true}); + } + + return spajs.open({ menuId:'template/' + options.kind + '/' + options.initiator +"/" + this.model.name+"/search/"+this.searchObjectToString(trim(query), 'mode'), reopen:true}); + } else if(this.isEmptySearchQuery(query)) { return spajs.open({ menuId:this.model.name, reopen:true}); @@ -77,6 +90,42 @@ pmHistory.search = function(query, options) } +/** + * Строит страницу со списком объектоа + * @param {type} holder + * @param {type} menuInfo + * @param {type} data + * @returns {$.Deferred} + */ +pmHistory.showList = function (holder, menuInfo, data) +{ + setActiveMenuLi(); + var thisObj = this; + var offset = 0 + var limit = this.pageSize; + if (data.reg && data.reg[1] > 0) + { + offset = this.pageSize * (data.reg[1] - 1); + } + + return $.when(this.loadItems(limit, offset)).done(function () + { + var tpl = thisObj.model.name + '_list' + if (!spajs.just.isTplExists(tpl)) + { + tpl = 'items_list' + } + + $.when($(holder).insertTpl(spajs.just.render(tpl, {query: "", pmObj: thisObj, opt: {}}))).done(function() + { + pmHistory.setTableRowLinkInLink(); + }) + }).fail(function () + { + $.notify("", "error"); + }) +} + pmHistory.showListInProjects = function(holder, menuInfo, data) { var thisObj = this; @@ -90,7 +139,9 @@ pmHistory.showListInProjects = function(holder, menuInfo, data) return $.when(this.sendSearchQuery({project:project_id}, limit, offset), pmProjects.loadItem(project_id)).done(function() { - $(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInProjects', {query:"", project_id:project_id})) + $.when($(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInProjects', {query:"", project_id:project_id}))).done(function(){ + pmHistory.setTableRowLinkInLink(); + }) thisObj.model.selectedCount = $('.multiple-select .selected').length; }).fail(function() { @@ -111,7 +162,32 @@ pmHistory.showListInInventory = function(holder, menuInfo, data) return $.when(this.sendSearchQuery({inventory:inventory_id}, limit, offset), pmInventories.loadItem(inventory_id)).done(function() { - $(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInInventory', {query:"", inventory_id:inventory_id})) + $.when($(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInInventory', {query:"", inventory_id:inventory_id}))).done(function(){ + pmHistory.setTableRowLinkInLink(); + }) + }).fail(function() + { + $.notify("", "error"); + }) +} + +pmHistory.showListInTemplate = function(holder, menuInfo, data, pmObj) +{ + var thisObj = this; + var offset = 0 + var limit = this.pageSize; + if(data.reg && data.reg[2] > 0) + { + offset = this.pageSize*(data.reg[2] - 1); + } + var template_id = data.reg[1]; + //debugger; + return $.when(this.sendSearchQuery({initiator_type:"template",initiator:template_id}, limit, offset), pmObj.loadItem(template_id)).done(function() + { + $.when($(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInTemplate', {query:"", pmObj:pmObj, template_id:template_id}))).done(function(){ + pmHistory.setTableRowLinkInLink(); + }) + thisObj.model.selectedCount = $('.multiple-select .selected').length; }).fail(function() { $.notify("", "error"); @@ -122,13 +198,71 @@ pmHistory.showSearchResultsInProjects = function(holder, menuInfo, data) { var thisObj = this; var project_id = data.reg[1]; + var offset = 0 + var limit = this.pageSize; + if(data.reg && data.reg[3] > 0) + { + offset = this.pageSize*(data.reg[3] - 1); + } var search = this.searchStringToObject(decodeURIComponent(data.reg[2]), 'mode') search['project'] = project_id - return $.when(this.sendSearchQuery(search), pmProjects.loadItem(project_id)).done(function() + return $.when(this.sendSearchQuery(search, limit, offset), pmProjects.loadItem(project_id)).done(function() + { + $.when($(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInProjects', {query:decodeURIComponent(data.reg[2]), project_id:project_id}))).done(function(){ + pmHistory.setTableRowLinkInLink(); + }) + }).fail(function() + { + $.notify("", "error"); + }) +} + +pmHistory.showSearchResultsInInventory = function(holder, menuInfo, data) +{ + var thisObj = this; + var inventory_id = data.reg[1]; + var offset = 0 + var limit = this.pageSize; + if(data.reg && data.reg[3] > 0) + { + offset = this.pageSize*(data.reg[3] - 1); + } + var search = this.searchStringToObject(decodeURIComponent(data.reg[2]), 'mode') + search['inventory'] = inventory_id + + return $.when(this.sendSearchQuery(search, limit, offset), pmInventories.loadItem(inventory_id)).done(function() { - $(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInProjects', {query:decodeURIComponent(data.reg[2]), project_id:project_id})) + $.when($(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInInventory', {query:decodeURIComponent(data.reg[2]), inventory_id:inventory_id}))).done(function(){ + pmHistory.setTableRowLinkInLink(); + }) + }).fail(function() + { + $.notify("", "error"); + }) +} + +pmHistory.showSearchResultsInTemplate = function(holder, menuInfo, data, pmObj) +{ + var thisObj = this; + var template_id = data.reg[1]; + var offset = 0 + var limit = this.pageSize; + if(data.reg && data.reg[3] > 0) + { + offset = this.pageSize*(data.reg[3] - 1); + } + + var search = this.searchStringToObject(decodeURIComponent(data.reg[2]), 'mode'); + search['initiator_type'] = 'template'; + search['initiator'] = template_id; + + return $.when(this.sendSearchQuery(search, limit, offset), pmObj.loadItem(template_id)).done(function() + { + $.when($(holder).insertTpl(spajs.just.render(thisObj.model.name+'_listInTemplate', {query:decodeURIComponent(data.reg[2]), pmObj:pmObj, template_id:template_id}))).done(function(){ + pmHistory.setTableRowLinkInLink(); + }) }).fail(function() { $.notify("", "error"); @@ -144,12 +278,6 @@ pmHistory.showItem = function(holder, menuInfo, data) var item_id = data.reg[1]; return $.when(this.loadItem(item_id)).done(function() { - if(pmHistory.model.items[item_id].initiator > 0 ) - { - pmUsers.loadItem(pmHistory.model.items[item_id].initiator); - } - - if (pmHistory.model.items[item_id].inventory != null) { var promiss = pmInventories.loadItem(pmHistory.model.items[item_id].inventory); $.when(promiss).done(function () { @@ -163,8 +291,6 @@ pmHistory.showItem = function(holder, menuInfo, data) pmHistory.bindStdoutUpdates(item_id) } - - }).fail(function() { $.notify("", "error"); @@ -174,16 +300,11 @@ pmHistory.showItem = function(holder, menuInfo, data) pmHistory.showItemInProjects = function(holder, menuInfo, data) { var thisObj = this; - //console.log(menuInfo, data) var project_id = data.reg[1]; var item_id = data.reg[2]; - return $.when(this.loadItem(item_id), pmProjects.loadItem(project_id)).done(function() + //debugger; + return $.when(this.loadItem(item_id)).done(function() { - if(pmHistory.model.items[item_id].initiator > 0 ) - { - pmUsers.loadItem(pmHistory.model.items[item_id].initiator); - } - if (pmHistory.model.items[item_id].inventory != null) { var promiss = pmInventories.loadItem(pmHistory.model.items[item_id].inventory); $.when(promiss).done(function () { @@ -205,6 +326,39 @@ pmHistory.showItemInProjects = function(holder, menuInfo, data) }) } +pmHistory.showItemInInventory = function(holder, menuInfo, data) +{ + var thisObj = this; + var inventory_id = data.reg[1]; + var item_id = data.reg[2]; + return $.when(this.loadItem(item_id), pmInventories.loadItem(inventory_id)).done(function() + { + $(holder).insertTpl(spajs.just.render(thisObj.model.name+'_pageInInventory', {item_id:item_id, inventory_id:inventory_id})) + pmHistory.bindStdoutUpdates(item_id) + }).fail(function() + { + $.notify("", "error"); + }) +} + +pmHistory.showItemInTemplate = function(holder, menuInfo, data, pmObj) +{ + var thisObj = this; + var template_id = data.reg[1]; + var item_id = data.reg[2]; + return $.when(this.loadItem(item_id), pmObj.loadItem(template_id)).done(function() + { + $.when(pmInventories.loadItem(pmHistory.model.items[item_id].inventory)).done(function(){ + $(holder).insertTpl(spajs.just.render(thisObj.model.name+'_pageInTemplate', {item_id:item_id, pmObj:pmObj, template_id:template_id})) + pmHistory.bindStdoutUpdates(item_id); + }); + + }).fail(function() + { + $.notify("", "error"); + }) +} + pmHistory.bindStdoutUpdates = function(item_id) { var thisObj = this; @@ -263,7 +417,7 @@ pmHistory.bindStdoutUpdates = function(item_id) } /** - * Обновляет поле модел this.model.items[item_id] и ложит туда пользователя + * Загружает данные элемента истории */ pmHistory.loadItem = function(item_id) { @@ -271,7 +425,7 @@ pmHistory.loadItem = function(item_id) var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/"+this.model.name+"/"+item_id+"/", + url: hostname + "/api/v1/"+this.model.name+"/"+item_id+"/", type: "GET", contentType:'application/json', data: "", @@ -293,15 +447,24 @@ pmHistory.loadItem = function(item_id) { promise = pmPeriodicTasks.loadItemsByIds([data.initiator]) } + else if(data.initiator_type == 'project') + { + promise = pmProjects.loadItemsByIds([data.initiator]) + } + else if(data.initiator_type == 'template') + { + promise = pmTasksTemplates.loadItemsByIds([data.initiator]) + } - if(data.initiator_type == 'users') + var promise2 = undefined; + if(data.executor!=null) { - promise = pmUsers.loadItemsByIds([data.initiator]) + pmUsers.loadItem(data.executor); } pmHistory.model.items.justWatch(item_id); - $.when(pmProjects.loadItem(data.project), promise).always(function(){ + $.when(pmProjects.loadItem(data.project), promise, promise2).always(function(){ def.resolve(data) }) }, @@ -337,7 +500,7 @@ pmHistory.sendSearchQuery = function(query, limit, offset) var def = new $.Deferred(); var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/"+this.model.name+"/?"+q.join('&'), + url: hostname + "/api/v1/"+this.model.name+"/?"+q.join('&'), type: "GET", contentType:'application/json', data: "limit="+encodeURIComponent(limit)+"&offset="+encodeURIComponent(offset), @@ -352,6 +515,7 @@ pmHistory.sendSearchQuery = function(query, limit, offset) var projects = []; var usersIds = []; var periodicTasks = []; + var templates = []; for(var i in data.results) { @@ -364,19 +528,29 @@ pmHistory.sendSearchQuery = function(query, limit, offset) projects.push(val.project) } - if(val.initiator > 0 && val.initiator_type == 'users' && $.inArray(val.initiator, usersIds) == -1) + if(val.initiator > 0 && val.initiator_type == 'project' && $.inArray(val.initiator, projects) == -1) { - usersIds.push(val.initiator); + projects.push(val.initiator); } else if(val.initiator > 0 && val.initiator_type == 'scheduler' && $.inArray(val.initiator, periodicTasks) == -1) { periodicTasks.push(val.initiator); } + else if(val.initiator > 0 && val.initiator_type == 'template' && $.inArray(val.initiator, templates) == -1) + { + templates.push(val.initiator); + } + + if(val.executor && !pmUsers.model.items[val.executor] && $.inArray(val.executor, usersIds) == -1) + { + usersIds.push(val.executor) + } } var users_promise = undefined; var projects_promise = undefined; var periodicTasks_promise = undefined; + var templates_promise = undefined; if(periodicTasks.length) { @@ -393,7 +567,12 @@ pmHistory.sendSearchQuery = function(query, limit, offset) projects_promise = pmProjects.sendSearchQuery({id:projects.join(',')}) } - $.when(users_promise, projects_promise, periodicTasks_promise).done(function(){ + if(templates.length) + { + templates_promise = pmTasksTemplates.loadItemsByIds(templates); + } + + $.when(users_promise, projects_promise, periodicTasks_promise, templates_promise).done(function(){ def.resolve(data) }) }, @@ -418,7 +597,7 @@ pmHistory.ifIncreaseTotalCount = function() var def = new $.Deferred(); var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/history", + url: hostname + "/api/v1/history", type: "GET", contentType:'application/json', data: "limit=1&rand="+Math.random(), @@ -491,7 +670,7 @@ pmHistory.loadItems = function(limit, offset) var def = new $.Deferred(); var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/"+this.model.name+"/", + url: hostname + "/api/v1/"+this.model.name+"/", type: "GET", contentType:'application/json', data: "limit="+encodeURIComponent(limit)+"&offset="+encodeURIComponent(offset), @@ -510,6 +689,7 @@ pmHistory.loadItems = function(limit, offset) var projects = []; var usersIds = []; var periodicTasks = []; + var templates = []; for(var i in data.results) { @@ -523,19 +703,29 @@ pmHistory.loadItems = function(limit, offset) projects.push(val.project) } - if(val.initiator > 0 && val.initiator_type == 'users' && $.inArray(val.initiator, usersIds) == -1) + if(val.initiator > 0 && val.initiator_type == 'project' && $.inArray(val.initiator, projects) == -1) { - usersIds.push(val.initiator); + projects.push(val.initiator); } else if(val.initiator > 0 && val.initiator_type == 'scheduler' && $.inArray(val.initiator, periodicTasks) == -1) { periodicTasks.push(val.initiator); } + else if(val.initiator > 0 && val.initiator_type == 'template' && $.inArray(val.initiator, templates) == -1) + { + templates.push(val.initiator); + } + + if(val.executor && !pmUsers.model.items[val.executor] && $.inArray(val.executor, usersIds) == -1) + { + usersIds.push(val.executor) + } } var users_promise = undefined; var projects_promise = undefined; var periodicTasks_promise = undefined; + var templates_promise = undefined; if(periodicTasks.length) { @@ -552,7 +742,12 @@ pmHistory.loadItems = function(limit, offset) projects_promise = pmProjects.sendSearchQuery({id:projects.join(',')}) } - $.when(users_promise, projects_promise, periodicTasks_promise).always(function(){ + if(templates.length) + { + templates_promise = pmTasksTemplates.loadItemsByIds(templates); + } + + $.when(users_promise, projects_promise, periodicTasks_promise, templates_promise).always(function(){ def.resolve(data) }) }, @@ -703,7 +898,7 @@ pmHistory.loadLines = function(item_id, opt) var def = new $.Deferred(); spajs.ajax.Call({ - url: "/api/v1/history/"+item_id+"/lines/", + url: hostname + "/api/v1/history/"+item_id+"/lines/", type: "GET", contentType:'application/json', data: opt, @@ -749,11 +944,10 @@ pmHistory.loadLines = function(item_id, opt) return def.promise(); } -///////////////////////////////// pmHistory.clearLogs=function(item_id) { return spajs.ajax.Call({ - url: "/api/v1/history/"+item_id+"/clear/", + url: hostname + "/api/v1/history/"+item_id+"/clear/", type: "DELETE", contentType:'application/json', success: function(data) @@ -776,7 +970,29 @@ pmHistory.hideClearLogsButton=function() $("#clear_logs").slideToggle(); } } -///////////////////////////////// + +pmHistory.setTableRowLinkInLink = function() +{ + $('.light-tr').on('click', function(evt) { + + if(!(evt.target.classList.contains('light-tr-none') || + evt.target.classList.contains('ico-on') || + evt.target.classList.contains('ico-off')) + ) + { + if(evt.target.hasAttribute('href')) + { + var href = evt.target.getAttribute('href'); + } + else + { + var href = evt.currentTarget.getAttribute('data-href'); + } + spajs.openURL(href); + } + }); +} + tabSignal.connect("polemarch.start", function() { // history @@ -800,6 +1016,7 @@ tabSignal.connect("polemarch.start", function() onClose:function(){return pmHistory.stopUpdates();} }) + // history in project spajs.addMenu({ id:"history-item-in-project", urlregexp:[/^project\/([0-9]+)\/history\/([0-9]+)$/], @@ -833,4 +1050,64 @@ tabSignal.connect("polemarch.start", function() onOpen:function(holder, menuInfo, data){return pmHistory.showSearchResultsInProjects(holder, menuInfo, data);} }) + // history in inventory + spajs.addMenu({ + id:"inventory-history", + urlregexp:[/^inventory\/([0-9]+)\/history$/, /^inventory\/([0-9]+)\/history\/page\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showListInInventory(holder, menuInfo, data);} + }) + + spajs.addMenu({ + id:"history-item-in-inventory", + urlregexp:[/^inventory\/([0-9]+)\/history\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showItemInInventory(holder, menuInfo, data);}, + onClose:function(){return pmHistory.stopUpdates();} + }) + + spajs.addMenu({ + id:"inventory-history-search", + urlregexp:[/^inventory\/([0-9]+)\/history\/search\/([A-z0-9 %\-.:,=]+)$/,/^inventory\/([0-9]+)\/history\/search\/([A-z0-9 %\-.:,=]+)\/page\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showSearchResultsInInventory(holder, menuInfo, data);} + }) + + // history in task template + spajs.addMenu({ + id:"task-template-history", + urlregexp:[/^template\/Task\/([0-9]+)\/history$/, /^template\/Task\/([0-9]+)\/history\/page\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showListInTemplate(holder, menuInfo, data, pmTasksTemplates);} + }) + + spajs.addMenu({ + id:"history-item-in-task-template", + urlregexp:[/^template\/Task\/([0-9]+)\/history\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showItemInTemplate(holder, menuInfo, data, pmTasksTemplates);}, + onClose:function(){return pmHistory.stopUpdates();} + }) + + spajs.addMenu({ + id:"task-template-history-search", + urlregexp:[/^template\/Task\/([0-9]+)\/history\/search\/([A-z0-9 %\-.:,=]+)$/,/^template\/Task\/([0-9]+)\/history\/search\/([A-z0-9 %\-.:,=]+)\/page\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showSearchResultsInTemplate(holder, menuInfo, data, pmTasksTemplates);} + }) + + // history in module template + spajs.addMenu({ + id:"module-template-history", + urlregexp:[/^template\/Module\/([0-9]+)\/history$/, /^template\/Module\/([0-9]+)\/history\/page\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showListInTemplate(holder, menuInfo, data, pmModuleTemplates);} + }) + + spajs.addMenu({ + id:"history-item-in-module-template", + urlregexp:[/^template\/Module\/([0-9]+)\/history\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showItemInTemplate(holder, menuInfo, data, pmModuleTemplates);}, + onClose:function(){return pmHistory.stopUpdates();} + }) + + spajs.addMenu({ + id:"module-template-history-search", + urlregexp:[/^template\/Module\/([0-9]+)\/history\/search\/([A-z0-9 %\-.:,=]+)$/,/^template\/Module\/([0-9]+)\/history\/search\/([A-z0-9 %\-.:,=]+)\/page\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHistory.showSearchResultsInTemplate(holder, menuInfo, data, pmModuleTemplates);} + }) + }) \ No newline at end of file diff --git a/polemarch/static/js/pmHooks.js b/polemarch/static/js/pmHooks.js new file mode 100644 index 00000000..7ae64780 --- /dev/null +++ b/polemarch/static/js/pmHooks.js @@ -0,0 +1,442 @@ +var pmHooks = inheritance(pmItems) +pmHooks.model.name = "hooks" +pmHooks.model.page_name = "hook" +pmHooks.model.bulk_name = "hook" +pmHooks.model.className = "pmHooks" + + +pmHooks.filed.selectHookType = inheritance(filedsLib.filed.simpleText) +pmHooks.filed.selectHookType.type = 'selectHookType' +pmHooks.filed.selectHookType.getValue = function(pmObj, filed){ + return ''; +} + +pmHooks.filed.selectHookWhen = inheritance(filedsLib.filed.simpleText) +pmHooks.filed.selectHookWhen.type = 'selectHookWhen' +pmHooks.filed.selectHookWhen.getValue = function(pmObj, filed){ + return ''; +} + +pmHooks.model.page_list = { + buttons:[ + { + class:'btn btn-primary', + function:function(){ return "spajs.open({ menuId:'new-"+this.model.page_name+"'}); return false;"}, + title:'Create', + link:function(){ return '/?new-'+this.model.page_name}, + }, + ], + title: "Hooks", + short_title: "Hooks", + fileds:[ + { + title:'Name', + name:'name', + }, + { + title:'Type', + name:'type', + class:function(item){ return 'class="hidden-xs"'}, + value:function(item){ return item['type']}, + }, + { + title:'When', + name: 'when', + class:function(item){ return 'class="hidden-480"'}, + value:function(item){ return pmHooks.model.supportedWhens[item['when']]}, + }, + { + title:'Recipients', + name: 'recipients', + class:function(item){ return 'class="hidden-xs recipients_list"'}, + value:function(item) + { + var recipient_list="
      "; + var recipients_arr = pmHooks.parseRecipientsFromStrToArr(item['recipients']) + for (var i in recipients_arr) + { + recipient_list+="
    • "+recipients_arr[i]+"
    • "; + } + recipient_list+="
    "; + return recipient_list; + }, + } + ], + actions:[ + { + function:function(item){ return 'spajs.showLoader('+this.model.className+'.deleteItem('+item.id+')); return false;'}, + title:'Delete', + link:function(){ return '#'} + } + ] +} + +pmHooks.model.page_item = { + buttons:[ + { + class:'btn btn-primary', + function:function(item_id){ return 'spajs.showLoader($.when('+this.model.className+'.updateItem('+item_id+')).done(function() {return spajs.openURL("'+window.location.href+'");})); return false;'}, + title:'Save', + link:function(){ return '#'}, + }, + { + class:'btn btn-danger danger-right', + function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.deleteItem('+item_id+')); return false;'}, + title:' ', + link:function(){ return '#'}, + }, + ], + title: function(item_id){ + return "Hook "+this.model.items[item_id].justText('name') + }, + short_title: function(item_id){ + return "Hook "+this.model.items[item_id].justText('name', function(v){return v.slice(0, 20)}) + }, + fileds:[ + [ + { + filed: new filedsLib.filed.text(), + title:'Name', + name:'name', + placeholder:'Enter hook name', + validator:function(value){ + return filedsLib.validator.notEmpty(value, 'Name') + }, + fast_validator:function(value){ return value != '' && value} + }, + { + filed: new pmHooks.filed.selectHookType(), + name:'type', + }, + { + filed: new pmHooks.filed.selectHookWhen(), + name:'when', + }, + ] + ], + sections:[ + function(section, item_id){ + return spajs.just.render("section_hook_recipients", {item_id:item_id}) + } + ], + onUpdate:function(result) + { + return true; + }, + onBeforeSave:function(data, item_id) + { + if(pmHooks.model.items[item_id].recipients.length == 0) + { + $.notify("You should add at least one recipient", "error"); + return false; + } + data['type'] = $('#hook-'+item_id+'-type').val(); + data['when'] = $('#hook-'+item_id+'-when').val(); + if(data['when'] == "null") + { + data['when'] = null; + } + data['recipients'] = pmHooks.model.items[item_id].recipients; + return data + }, +} + + +pmHooks.model.page_new = { + title: "New hook", + short_title: "New hook", + fileds:[ + [ + { + filed: new filedsLib.filed.text(), + title:'Name', + name:'name', + placeholder:'Hook name', + validator:function(value){ + return filedsLib.validator.notEmpty(value, 'Name') + }, + fast_validator:function(value){ return value != '' && value} + }, + { + filed: new pmHooks.filed.selectHookType(), + name:'type', + }, + { + filed: new pmHooks.filed.selectHookWhen(), + name:'when', + }, + ] + ], + sections:[ + function(section){ + return spajs.just.render("section_hook_recipients", {item_id:"new"}) + } + ], + onBeforeSave:function(data) + { + if(pmHooks.model.newItem.recipients.length == 0) + { + $.notify("You should add at least one recipient", "error"); + return false; + } + data['type'] = $('#new-hook-type').val(); + data['when'] = $('#new-hook-when').val(); + if(data['when'] == "null") + { + data['when'] = null; + } + data['recipients'] = pmHooks.model.newItem.recipients; + + return data + }, + onCreate:function(result) + { + var def = new $.Deferred(); + $.notify("Hook created", "success"); + $.when(spajs.open({ menuId:this.model.page_name+"/"+result.id})).always(function(){ + def.resolve() + }) + + return def.promise(); + } +} + + +/** + * Функция получает доступные types и when для хуков + * и сохраняет их в pmHooks.model.supportedTypes и pmHooks.model.supportedWhens + */ +pmHooks.getSupportedTypes = function() +{ + return spajs.ajax.Call({ + url: hostname + "/api/v1/hooks/types/", + type: "GET", + contentType:'application/json', + success: function(data) + { + pmHooks.model.supportedTypes = data['types']; + pmHooks.model.supportedWhens = {}; + pmHooks.model.supportedWhens['null'] = 'Always'; + for (var i in data['when']) { + pmHooks.model.supportedWhens[i] = data['when'][i]; + } + }, + error:function(e) + { + console.warn("getSupportedTypes error - " + JSON.stringify(e)); + } + }); +} + + +/** + * Функция вызывается при открытии какого либо из меню(url) предназначенного для хуков. + * После того, как фукнция pmHooks.getSupportedTypes успешно выполнится, + * Данная функция откроет необходимую для нашего меню страницу. + * @param {String} functionName - имя функции которая будет вызвана после успешного + * вызова pmHooks.getSupportedTypes + */ +pmHooks.openSomeHookPage = function (holder, menuInfo, data, functionName) +{ + return $.when(pmHooks.getSupportedTypes()).done(function() + { + if(functionName=="showNewItemPage") + { + pmHooks.model.newItem={}; + pmHooks.model.newItem.recipients=""; + } + pmHooks[functionName](holder, menuInfo, data); + + }).fail(function () + { + $.notify("Error with opening this page"); + }).promise(); +} + + +/** + * Фукнция преобраузет строку с со списком recipients в массив. + * @param {String} recipients_str - строка содержащая список recipients, которые разделены + * между собой группой символов: " | ". + */ +pmHooks.parseRecipientsFromStrToArr = function(recipients_str) +{ + if(recipients_str == "") + { + return []; + } + else + { + return recipients_str.split(" | "); + } +} + + +/** + * Фукнция преобраузет массив recipients в строку с разделителем: " | ". + * @param {Array} recipients_arr - массив содержащий список recipients + */ +pmHooks.parseRecipientsFromArrToStr = function(recipients_arr) +{ + if(recipients_arr.length == 0) + { + return ""; + } + else + { + return recipients_arr.join(" | "); + } +} + + +/** + * Фукнция открывает модальное окно для создание нового recipient'a. + * @param {String/Number} item_id - "new" для нового хука либо id для существующего хука. + */ +pmHooks.openNewRecipientModal = function (item_id) +{ + if($('div').is('#modal-new-recipient')) + { + $('#modal-new-recipient').empty(); + $('#modal-new-recipient').insertTpl(pmHooks.renderNewRecipientModal(item_id)); + $("#modal-new-recipient").modal('show'); + } + else + { + var t=$(".content")[0]; + $('
    ', { id: "modal-new-recipient", class: "modal fade in"}).appendTo(t); + $('#modal-new-recipient').insertTpl(pmHooks.renderNewRecipientModal(item_id)); + $("#modal-new-recipient").modal('show'); + } +} + + +/** + * Фукнция рендерит модальное окно для создание нового recipient'a. + * @param {String/Number} item_id - "new" для нового хука либо id для существующего хука. + */ +pmHooks.renderNewRecipientModal = function (item_id) +{ + var html=spajs.just.render('new-recipient-modal', {item_id:item_id}); + return html; +} + + +/** + * Фукнция добавляет нового recipient'a к списку других recipient'ов. + * @param {String/Number} item_id - "new" для нового хука либо id для существующего хука. + */ +pmHooks.addNewRecipient = function (item_id) +{ + if($("#new_recipient").val().trim() == "") + { + $.notify("Recipient field should not be empty.", "error"); + return false; + } + + if(item_id == "new") + { + var recipients_arr = pmHooks.parseRecipientsFromStrToArr(pmHooks.model.newItem.recipients); + recipients_arr.push($('#new_recipient').val().trim()); + pmHooks.model.newItem.recipients = pmHooks.parseRecipientsFromArrToStr(recipients_arr); + } + else + { + var recipients_arr = pmHooks.parseRecipientsFromStrToArr(pmHooks.model.items[item_id].recipients); + recipients_arr.push($('#new_recipient').val().trim()); + pmHooks.model.items[item_id].recipients = pmHooks.parseRecipientsFromArrToStr(recipients_arr); + } + $("#modal-new-recipient").modal('hide'); +} + + +/** + * Фукнция отрисовывает форму для редактирования списка recipient'ов. + * @param {String/Number} item_id - "new" для нового хука либо id для существующего хука. + */ +pmHooks.showEditRecipientsForm = function(item_id) +{ + if(!item_id) + { + throw "Error in pmHooks.showEditRecipientsForm with item_id = `" + item_id + "`" + } + + $("#edit_hook_recipients").remove() + $(".content").appendTpl(spajs.just.render('edit_hook_recipients', {item_id:item_id})) + var scroll_el = "#edit_hook_recipients"; + if ($(scroll_el).length != 0) { + $('html, body').animate({ scrollTop: $(scroll_el).offset().top }, 1000); + } + $("#polemarch-model-recipients-select").select2({ width: '100%' }); +} + + +/** + * Фукнция запоминает изменения в списке recipient'ов. + * @param {String/Number} item_id - "new" для нового хука либо id для существующего хука. + * @param {Array} recipients - массив с новым списком recipient'ов. + */ +pmHooks.setRecipients = function(item_id, recipients) +{ + if(!item_id) + { + throw "Error in pmHooks.setRecipients with item_id = `" + item_id + "`" + } + + if(!recipients) + { + recipients = ""; + } + + + if(item_id == "new") + { + pmHooks.model.newItem.recipients = pmHooks.parseRecipientsFromArrToStr(recipients); + } + else + { + return spajs.ajax.Call({ + url: hostname + "/api/v1/hooks/"+item_id+"/", + type: "PATCH", + contentType:'application/json', + data:JSON.stringify({recipients:pmHooks.parseRecipientsFromArrToStr(recipients)}), + success: function(data) + { + pmHooks.model.items[item_id].recipients = data.recipients; + }, + error:function(e) + { + console.warn("Hook "+item_id+" update error - " + JSON.stringify(e)); + polemarch.showErrors(e.responseJSON) + } + }); + } +} + + +tabSignal.connect("polemarch.start", function() +{ + spajs.addMenu({ + id:"hooks", + urlregexp:[/^hooks/, /^hooks\/search\/?$/, /^hooks\/page\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHooks.openSomeHookPage(holder, menuInfo, data, 'showList');} + }) + + spajs.addMenu({ + id:"hooks-item", + urlregexp:[/^hook\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHooks.openSomeHookPage(holder, menuInfo, data, 'showItem');} + }) + + spajs.addMenu({ + id:"newHook", + urlregexp:[/^new-hook/, /^([A-z0-9_]+)\/([0-9]+)\/new-hook/], + onOpen:function(holder, menuInfo, data){return pmHooks.openSomeHookPage(holder, menuInfo, data, 'showNewItemPage');} + }) + + spajs.addMenu({ + id:"hooks-search", + urlregexp:[/^hooks\/search\/([A-z0-9 %\-.:,=]+)$/, /^hooks\/search\/([A-z0-9 %\-.:,=]+)\/page\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmHooks.openSomeHookPage(holder, menuInfo, data, 'showSearchResults');} + }) +}) \ No newline at end of file diff --git a/polemarch/static/js/pmHosts.js b/polemarch/static/js/pmHosts.js index 61be26db..a183d2bc 100644 --- a/polemarch/static/js/pmHosts.js +++ b/polemarch/static/js/pmHosts.js @@ -11,9 +11,9 @@ pmHosts.model.page_list = { { class:'btn btn-primary', function:function(){ return "spajs.open({ menuId:'new-"+this.model.page_name+"'}); return false;"}, - title:'Create', - link:function(){ return '/?new-'+this.model.page_name}, - }, + title:'Create', + link:function(){ return '/?new-'+this.model.page_name}, + }, ], title: "Hosts", short_title: "Hosts", @@ -26,24 +26,23 @@ pmHosts.model.page_list = { title:'Type', name:'type', style:function(item){ return 'style="width: 70px"'}, - class:function(item){ return 'class="hidden-xs"'}, + class:function(item){ return 'class="hidden-xs"'}, } ], actions:[ { - class:'btn btn-danger', function:function(item){ return 'spajs.showLoader('+this.model.className+'.deleteItem('+item.id+')); return false;'}, title:'Delete', link:function(){ return '#'} } ] } - - + + pmHosts.fileds = [ [ { - filed: new filedsLib.filed.text(), + filed: new filedsLib.filed.text(), title:'Name', name:'name', placeholder:'Enter host or range name', @@ -54,11 +53,19 @@ pmHosts.fileds = [ { return true; } - - $.notify("Invalid value in field `name` it mast be valid host or range name", "error"); + + $.notify("Invalid value in field `name` it mast be valid host or range name", "error"); return false; - }, - fast_validator:function(value){ return this.validateRangeName(value) || this.validateHostName(value)} + }, + fast_validator:function(value){ return this.validateRangeName(value) || this.validateHostName(value)} + }, + ], + [ + { + filed: new filedsLib.filed.textarea(), + title:'Notes', + name:'notes', + placeholder:'Not required field, just for your notes' }, ] ] @@ -73,26 +80,26 @@ pmHosts.model.page_new = { } ], onBeforeSave:function(data) - { + { if(this.validateHostName(data.name)) { data.type = 'HOST' } else if(this.validateRangeName(data.name)) - { + { data.type = 'RANGE' } else { - $.notify("Error in host or range name", "error"); + $.notify("Error in host or range name", "error"); return undefined; } - + data.vars = jsonEditor.jsonEditorGetValues() return data; }, onCreate:function(result, status, xhr, callOpt) - { + { var def = new $.Deferred(); $.notify("Host created", "success"); @@ -136,19 +143,19 @@ pmHosts.model.page_new = { def.resolve() }) } - + return def.promise(); } } - + pmHosts.model.page_item = { buttons:[ { class:'btn btn-primary', function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.updateItem('+item_id+')); return false;'}, - title:'Save', - link:function(){ return '#'}, - }, + title:'Save', + link:function(){ return '#'}, + }, { class:'btn btn-default copy-btn', function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.copyAndEdit('+item_id+')); return false;'}, @@ -160,45 +167,45 @@ pmHosts.model.page_item = { class:'btn btn-danger danger-right', function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.deleteItem('+item_id+')); return false;'}, title:' ', - link:function(){ return '#'}, + link:function(){ return '#'}, }, ], sections:[ - function(section, item_id){ + function(section, item_id){ return jsonEditor.editor(this.model.items[item_id].vars, {block:this.model.name}); } ], - title: function(item_id){ + title: function(item_id){ return "Host "+pmHosts.model.items[item_id].justText('name') }, - short_title: function(item_id){ + short_title: function(item_id){ return "Host "+pmHosts.model.items[item_id].justText('name', function(v){return v.slice(0, 20)}) }, fileds:pmHosts.fileds, onUpdate:function(result) - { + { return true; }, onBeforeSave:function(data, item_id) - { + { data.vars = jsonEditor.jsonEditorGetValues() if(this.validateHostName(data.name)) { data.type = 'HOST' } else if(this.validateRangeName(data.name)) - { + { data.type = 'RANGE' } else { - $.notify("Error in host or range name", "error"); + $.notify("Error in host or range name", "error"); return undefined; } return data; }, } - + pmHosts.copyItem = function(item_id) { var def = new $.Deferred(); @@ -211,11 +218,11 @@ pmHosts.copyItem = function(item_id) { delete data.id; spajs.ajax.Call({ - url: "/api/v1/"+thisObj.model.name+"/", + url: hostname + "/api/v1/"+thisObj.model.name+"/", type: "POST", contentType:'application/json', data: JSON.stringify(data), - success: function(data) + success: function(data) { thisObj.model.items[data.id] = data def.resolve(data.id) @@ -229,7 +236,7 @@ pmHosts.copyItem = function(item_id) { def.reject(e) }) - + }).fail(function(e) { def.reject(e) @@ -237,8 +244,8 @@ pmHosts.copyItem = function(item_id) return def.promise(); -} - +} + /* * @@ -251,41 +258,41 @@ setTimeout(function(){ name = Math.random()+"-"+Math.random() name = name.replace(/\./g, "") spajs.ajax.Call({ - url: "/api/v1/hosts/", + url: hostname + "/api/v1/hosts/", type: "POST", contentType:'application/json', data: JSON.stringify({name:name, type:"HOST"}), }) }, i*400); } - */ + */ - tabSignal.connect("polemarch.start", function() - { +tabSignal.connect("polemarch.start", function() +{ // hosts spajs.addMenu({ - id:"hosts", + id:"hosts", urlregexp:[/^hosts$/, /^host$/, /^hosts\/search\/?$/, /^hosts\/page\/([0-9]+)$/], onOpen:function(holder, menuInfo, data){return pmHosts.showList(holder, menuInfo, data);} }) - + spajs.addMenu({ - id:"hosts-search", + id:"hosts-search", urlregexp:[/^hosts\/search\/([A-z0-9 %\-.:,=]+)$/, /^hosts\/search\/([A-z0-9 %\-.:,=]+)\/page\/([0-9]+)$/], onOpen:function(holder, menuInfo, data){return pmHosts.showSearchResults(holder, menuInfo, data);} }) - + spajs.addMenu({ - id:"host", + id:"host", urlregexp:[/^host\/([0-9]+)$/, /^hosts\/([0-9]+)$/], onOpen:function(holder, menuInfo, data){return pmHosts.showItem(holder, menuInfo, data);} }) spajs.addMenu({ - id:"newHost", + id:"newHost", urlregexp:[/^new-host$/, /^([A-z0-9_]+)\/([0-9]+)\/new-host$/], onOpen:function(holder, menuInfo, data){return pmHosts.showNewItemPage(holder, menuInfo, data);} - }) - }) - - //изменение типа input'a на file при выборе \ No newline at end of file + }) +}) + +//изменение типа input'a на file при выборе \ No newline at end of file diff --git a/polemarch/static/js/pmInventories.js b/polemarch/static/js/pmInventories.js index 7495ef59..b415117e 100644 --- a/polemarch/static/js/pmInventories.js +++ b/polemarch/static/js/pmInventories.js @@ -122,7 +122,13 @@ pmInventories.parseHostLine = function(index, line, section, inventory) var host = { name:name, + notes:"", type:type, + all_only:false, + matches:false, + match_id_arr:[], + match_arr:[], + extern:false, vars:pmInventories.parseVarsLine(index, line) } @@ -201,9 +207,14 @@ pmInventories.addGroupIfNotExists = function(inventory, group_name) if(!inventory.groups[group_name]) { inventory.groups[group_name] = { + notes:"", vars:{}, groups:[], hosts:[], + matches:false, + match_id_arr:[], + match_arr:[], + extern:false, } return true; @@ -227,7 +238,8 @@ pmInventories.parseFromText = function(text) hosts:[], groups:{}, vars:{}, - name:new Date().toString() + name:new Date().toString(), + notes:"" } for(var i in lines) @@ -372,6 +384,7 @@ pmInventories.importFromFile = function(files_event) this.model.files = files_event this.model.importedInventories = {} var thisObj = this; + pmInventories.model.importedInventoriesIsReady=false; for(var i=0; i', { id: "edit_imported_subitem", class: "modal fade in"}).appendTo(t); + $('#edit_imported_subitem').insertTpl(pmInventories.renderEditItemModal(subItemType, index)); + $("#edit_imported_subitem").modal('show'); + } +} + +/** + * Функция сохраняет изменения внесенные в модальном окне для редактирования нового subitem(host or group), + * вложенного в импортируемый инвенторий. + * @param {string} subItemType - типа subitem(group or host) + * @param {integer/string} index - индекс данного subitem в массиве + */ +pmInventories.saveChangesFromEditItemModal = function(subItemType, index) +{ + pmInventories.model.importedSubItem.notes=$("#"+subItemType+"_notes").val(); + pmInventories.model.importedInventories.inventory[subItemType][index]=JSON.parse(JSON.stringify(pmInventories.model.importedSubItem)); + var html=spajs.just.render('change_imported_subitem_vars', {vars:pmInventories.model.importedInventories.inventory[subItemType][index].vars}); + $("#"+subItemType+"-"+index+"-vars").html(html); + $("#edit_imported_subitem").modal('hide'); + pmInventories.updateImportedInventoryPage(); +} + +/** + * Функция открывает модальное окно, в котором пользователь может выбрать + * какой конкретно из subitems с одинаковым именем использовать в данном инвентории. + * Так же данная функция посылает bulk запрос, который подгружает свойства созданных ранее + * subitems с таким же именем. + * @param {string} subItemType - типа subitem(group or host) + * @param {integer/string} index - индекс данного subitem в массиве + */ +pmInventories.openChooseMatchingModal = function(subItemType, index) +{ + var def = new $.Deferred(); + var bulkHosts=[]; + var type1=subItemType.slice(0,-1); + var imported_subitem=pmInventories.model.importedInventories.inventory[subItemType][index]; + imported_subitem=JSON.parse(JSON.stringify(imported_subitem)); + for(var i in imported_subitem.match_id_arr) + { + bulkHosts.push({ + type:"get", + item: type1, + pk: imported_subitem.match_id_arr[i] + }) + } + return $.when( + spajs.ajax.Call({ + url: "/api/v1/_bulk/", + type: "POST", + contentType:'application/json', + data:JSON.stringify(bulkHosts), + success: function(data) + { + for(var i in data) + { + imported_subitem.match_arr.push(data[i].data); + } + pmInventories.model.importedSubItem=JSON.parse(JSON.stringify(imported_subitem)); + if($('div').is('#choose_matching_modal')) + { + $('#choose_matching_modal').empty(); + $('#choose_matching_modal').insertTpl(pmInventories.renderChooseMatchingModal(subItemType, index)); + $("#choose_matching_modal").modal('show'); + } + else + { + var t=$(".content")[0]; + $('
    ', { id: "choose_matching_modal", class: "modal fade in"}).appendTo(t); + $('#choose_matching_modal').insertTpl(pmInventories.renderChooseMatchingModal(subItemType, index)); + $("#choose_matching_modal").modal('show'); + } + + }, + error: function(e) + { + $.notify("Error with getting matching "+subItemType+"' data.", "error"); + } + })).done(function(){def.promise();}).fail(function(){def.reject();}).promise(); + +} + + +/** + * Функция рендерит модальное окно, в котором пользователь может выбрать + * какой конкретно из subitems с одинаковым именем использовать в данном инвентории. + * @param {string} subItemType - типа subitem(group or host) + * @param {integer/string} index - индекс данного subitem в массиве + */ +pmInventories.renderChooseMatchingModal = function(subItemType, index) +{ + var html=spajs.just.render('choose_matching_modal', {val:pmInventories.model.importedSubItem, subItemType:subItemType, index:index}); + return html; +} + +/** + * Пользователь выбрал какой из subitems с одинаковым именем ему использовать. + * Данная функция снимает выделение со всех других subitems, + * и выделяет выбранный пользователем subitem. + * @param {object} thisEl - конкретный элемент + */ +pmInventories.toggleSelectMatchSubItem = function (thisEl) +{ + var mode=$(thisEl).parent().hasClass('selected'); + var match_subitem_arr=$(".match-subitem"); + for(var i=0; i - $.notify("Error in field ansible_ssh_private_key_file invalid value", "error"); - pmInventories.showHostVarsModal({group:'all', name:val.name}); - def2.reject() - return def2.promise(); + if (val.vars.ansible_ssh_private_key_file !== undefined && !/-----BEGIN RSA PRIVATE KEY-----/.test(val.vars.ansible_ssh_private_key_file)) { + // + $.notify("Error in field ansible_ssh_private_key_file invalid value", "error"); + //pmInventories.showHostVarsModal({group: 'all', name: val.name}); + var scroll_el = "#imported_hosts"; + if ($(scroll_el).length != 0) { + $('html, body').animate({ scrollTop: $(scroll_el).offset().top }, 700); + } + def2.reject() + return def2.promise(); + } } } for(var i in inventory.groups) { var val = inventory.groups[i] - if(val.vars.ansible_ssh_private_key_file !== undefined && !/-----BEGIN RSA PRIVATE KEY-----/.test(val.vars.ansible_ssh_private_key_file)) - { - // - $.notify("Error in field ansible_ssh_private_key_file invalid value", "error"); - pmInventories.showGroupVarsModal({name:i}); - def2.reject() - return def2.promise(); - } - - for(var j in val.hosts) + if(val.id===undefined) { - var hval = val.hosts[j] - if(hval.vars.ansible_ssh_private_key_file !== undefined && !/-----BEGIN RSA PRIVATE KEY-----/.test(hval.vars.ansible_ssh_private_key_file)) + if(val.vars.ansible_ssh_private_key_file !== undefined && !/-----BEGIN RSA PRIVATE KEY-----/.test(val.vars.ansible_ssh_private_key_file)) { // $.notify("Error in field ansible_ssh_private_key_file invalid value", "error"); - pmInventories.showHostVarsModal({group:i, name:hval.name}); + //pmInventories.showGroupVarsModal({name:i}); + var scroll_el = "#imported_groups"; + if ($(scroll_el).length != 0) { + $('html, body').animate({ scrollTop: $(scroll_el).offset().top }, 700); + } def2.reject() return def2.promise(); } + + } + //проверяем, что пользователь не пытается создать инвенторий с уже существующей ранее группой с children==true + //которая так же включает в себя группы, название которых идентично тем, что пользователь хочет создать сейчас заново + if(inventory.groups[i].children!==undefined && inventory.groups[i].children==true && inventory.groups[i].id!==undefined) + { + for(var j in inventory.groups[i].groups) + { + if(inventory.groups[inventory.groups[i].groups[j].name]!==undefined && inventory.groups[inventory.groups[i].groups[j].name].id==undefined) + { + $.notify('It is impossible to replace subitem "'+inventory.groups[i].groups[j].name+'" in existing group "'+i+'" with new one.', "error"); + var scroll_el = "#imported_groups"; + if ($(scroll_el).length != 0) { + $('html, body').animate({ scrollTop: $(scroll_el).offset().top }, 700); + } + def2.reject() + return def2.promise(); + + } + + if(inventory.groups[inventory.groups[i].groups[j].name]!==undefined && inventory.groups[inventory.groups[i].groups[j].name].id!=inventory.groups[i].groups[j].id) + { + $.notify('It is impossible to replace subitem "'+inventory.groups[i].groups[j].name+'" in existing group "'+i+'" with another existing subitem.', "error"); + var scroll_el = "#imported_groups"; + if ($(scroll_el).length != 0) { + $('html, body').animate({ scrollTop: $(scroll_el).offset().top }, 700); + } + def2.reject() + return def2.promise(); + + } + + } } + } var def = new $.Deferred(); - if($("#inventory_name").val() != "") + if($("#inventory_name").val() != "" && $("#inventory_name").val()!==undefined) { inventory.name = $("#inventory_name").val(); } @@ -501,7 +1739,8 @@ pmInventories.importInventory = function(inventory) var inventoryObject = { name:inventory.name, - vars:inventory.vars + vars:inventory.vars, + notes:$("#filed_notes").val() } var deleteBulk = [] @@ -512,56 +1751,32 @@ pmInventories.importInventory = function(inventory) item:'inventory', pk:inventory_id }) - var bulkdata = [] - // Сбор групп и вложенных в них хостов - for(var i in inventory.groups) - { - var val = inventory.groups[i] - bulkdata.push({ - type:"add", - item:'group', - data:{ - name:i, - children:val.children, - vars:val.vars - } - }) - for(var j in val.hosts) + // Сбор хостов вложенных к инвенторию + var bulkHosts = []; + for(var i in inventory.hosts) + { + var val = inventory.hosts[i] + if(val.id===undefined) { - var hval = val.hosts[j] - bulkdata.push({ + bulkHosts.push({ type:"add", item:'host', data:{ - name:hval.name, - type:hval.type, - vars:hval.vars + name:val.name, + notes:val.notes, + type:val.type, + vars:val.vars } }) } } - // Сбор хостов вложенных к инвенторию - var bulkHosts = [] - for(var i in inventory.hosts) - { - var val = inventory.hosts[i] - bulkHosts.push({ - type:"add", - item:'host', - data:{ - name:val.name, - type:val.type, - vars:val.vars - } - }) - } - // Добавление хостов вложенных к инвенторию + // Добавление новых хостов вложенных к инвенторию spajs.ajax.Call({ - url: "/api/v1/_bulk/", + url: hostname + "/api/v1/_bulk/", type: "POST", contentType:'application/json', data:JSON.stringify(bulkHosts), @@ -569,6 +1784,7 @@ pmInventories.importInventory = function(inventory) { var hasError = false; var hosts_ids = [] + var just_added_hosts = []; for(var i in data) { var val = data[i] @@ -578,27 +1794,106 @@ pmInventories.importInventory = function(inventory) hasError = true; continue; } - hosts_ids.push(val.data.id) - deleteBulk.push({ - type:"del", - item:'host', - pk:val.data.id - }) + for(var j in inventory.hosts) + { + if(inventory.hosts[j].name==val.data.name) + { + if(just_added_hosts[val.data.name]===undefined) + { + just_added_hosts[val.data.name]=val.data; + deleteBulk.push({ + type:"del", + item:'host', + pk:val.data.id + }) + if(inventory.hosts[j].all_only==false) + { + hosts_ids.push(val.data.id); + + } + } + } + } + } + //добавление существующих ранее хостов + for(var i in pmInventories.model.importedInventories.inventory.hosts) + { + if(pmInventories.model.importedInventories.inventory.hosts[i].id) + { + var hostName=pmInventories.model.importedInventories.inventory.hosts[i].name; + just_added_hosts[hostName]=pmInventories.model.importedInventories.inventory.hosts[i]; + + //добавление существующих ранее хостов, вложенных непосредственно к инвенторию + if(pmInventories.model.importedInventories.inventory.hosts[i].all_only==false) + { + hosts_ids.push(+pmInventories.model.importedInventories.inventory.hosts[i].id); + } + } } if(hasError) { // По меньшей мере в одной операции была ошибка вставки. - // Инвенторий импортирован не полностью + // Инвенторий импортирован не полностью def.reject(deleteBulk); return; } + var bulkdata = []; + var groups_with_just_added_hosts=[]; + // Сбор групп и вложенных в них хостов + for(var i in inventory.groups) + { + var val = inventory.groups[i] + //если нужно создать новую группу(id===undefined, следовательно, она не была создана ранее) + //то доавляем ее в bulk запрос + if(val.id===undefined) + { + bulkdata.push({ + type: "add", + item: 'group', + data: { + name: i, + notes: val.notes, + children: val.children, + vars: val.vars + } + }) + + //собираем хосты в нужные группы + for(var j in val.hosts) + { + var hval = val.hosts[j]; + if(just_added_hosts[hval.name]) + { + if(!groups_with_just_added_hosts[i]) + { + groups_with_just_added_hosts[i]=[]; + } + groups_with_just_added_hosts[i].push(just_added_hosts[hval.name].id); + } + else + { + bulkdata.push({ + type:"add", + item:'host', + data:{ + name:hval.name, + notes:hval.notes, + type:hval.type, + vars:hval.vars + } + }) + } + } + } + } + $.when(pmInventories.addSubHosts(inventory_id, hosts_ids)).done(function() { // Добавление групп и вложенных в них хостов spajs.ajax.Call({ - url: "/api/v1/_bulk/", + url: hostname + "/api/v1/_bulk/", type: "POST", contentType:'application/json', data:JSON.stringify(bulkdata), @@ -607,6 +1902,15 @@ pmInventories.importInventory = function(inventory) var igroups_ids = [] var bulk_update = [] var hasError = false; + for(var i in pmInventories.model.importedInventories.inventory.groups) + { + //собираем в igroups_ids группы, которые существовали в системе ранее + if(pmInventories.model.importedInventories.inventory.groups[i].id) + { + //igroups_ids - массив с id групп, которые будут добавлены в инвенторий в качестве subitems + igroups_ids.push(+pmInventories.model.importedInventories.inventory.groups[i].id); + } + } for(var i in data) { deleteBulk.push({ @@ -631,7 +1935,7 @@ pmInventories.importInventory = function(inventory) { if(inventory.groups[val.data.name].groups.length) { - // Добавление подгрупп + // Добавление подгрупп var groups_ids = [] for(var j in inventory.groups[val.data.name].groups) { @@ -644,6 +1948,16 @@ pmInventories.importInventory = function(inventory) break; } } + + //добавляем подгруппы, которые являются созданными ранее группами + for(var l in pmInventories.model.importedInventories.inventory.groups) + { + if(l==inventory.groups[val.data.name].groups[j] && pmInventories.model.importedInventories.inventory.groups[l].id) + { + groups_ids.push(pmInventories.model.importedInventories.inventory.groups[l].id); + } + } + } bulk_update.push({ type: "mod", @@ -657,6 +1971,7 @@ pmInventories.importInventory = function(inventory) } else { + // Это хост if(inventory.groups[val.data.name].hosts.length) { // Добавление хостов @@ -673,6 +1988,17 @@ pmInventories.importInventory = function(inventory) } } } + + for(var z in groups_with_just_added_hosts) + { + if(z==val.data.name){ + for(var y in groups_with_just_added_hosts[z]) + { + hosts_ids.push(groups_with_just_added_hosts[z][y]); + } + } + } + bulk_update.push({ type: "mod", item:'group', @@ -684,16 +2010,12 @@ pmInventories.importInventory = function(inventory) } } } - else - { - // Это хост - } } if(hasError) { // По меньшей мере в одной операции была ошибка вставки. - // Инвенторий импортирован не полностью + // Инвенторий импортирован не полностью def.reject(deleteBulk); return; } @@ -703,7 +2025,7 @@ pmInventories.importInventory = function(inventory) if(bulk_update.length) { spajs.ajax.Call({ - url: "/api/v1/_bulk/", + url: hostname + "/api/v1/_bulk/", type: "POST", contentType:'application/json', data:JSON.stringify(bulk_update), @@ -724,7 +2046,7 @@ pmInventories.importInventory = function(inventory) if(hasError) { // По меньшей мере в одной операции была ошибка обновления. - // Инвенторий импортирован не полностью + // Инвенторий импортирован не полностью def.reject(deleteBulk); return; } @@ -782,7 +2104,7 @@ pmInventories.importInventory = function(inventory) }).fail(function(delete_bulk) { $.when(spajs.ajax.Call({ - url: "/api/v1/_bulk/", + url: hostname + "/api/v1/_bulk/", type: "POST", contentType:'application/json', data:JSON.stringify(delete_bulk), @@ -831,7 +2153,7 @@ pmInventories.copyItem = function(item_id) $.when(encryptedCopyModal.replace(data)).done(function(data) { spajs.ajax.Call({ - url: "/api/v1/"+thisObj.model.name+"/", + url: hostname + "/api/v1/"+thisObj.model.name+"/", type: "POST", contentType:'application/json', data: JSON.stringify(data), @@ -900,13 +2222,11 @@ pmInventories.model.page_list = { ], actions:[ { - class:'btn btn-danger', function:function(item){ return 'spajs.showLoader('+this.model.className+'.deleteItem('+item.id+')); return false;'}, title:'Delete', link:function(){ return '#'} }, { - class:'btn btn-default', function:function(item){ return '';}, title:'Create sub group', link:function(item) @@ -915,7 +2235,6 @@ pmInventories.model.page_list = { }, }, { - class:'btn btn-default', function:function(item){ return '';}, title:'Create sub host', link:function(item) @@ -941,6 +2260,14 @@ pmInventories.model.page_new = { }, fast_validator:filedsLib.validator.notEmpty }, + ], + [ + { + filed: new filedsLib.filed.textarea(), + title:'Notes', + name:'notes', + placeholder:'Not required field, just for your notes' + }, ] ], sections:[ @@ -1033,6 +2360,14 @@ pmInventories.model.page_item = { }, fast_validator:function(value){ return value != '' && value} }, + ], + [ + { + filed: new filedsLib.filed.textarea(), + title:'Notes', + name:'notes', + placeholder:'Not required field, just for your notes' + }, ] ], onUpdate:function(result) @@ -1171,7 +2506,7 @@ pmInventories.setSubGroups = function(item_id, groups_ids) } return spajs.ajax.Call({ - url: "/api/v1/inventories/"+item_id+"/groups/", + url: hostname + "/api/v1/inventories/"+item_id+"/groups/", type: "PUT", contentType:'application/json', data:JSON.stringify(groups_ids), @@ -1210,7 +2545,7 @@ pmInventories.setSubHosts = function(item_id, hosts_ids) } return spajs.ajax.Call({ - url: "/api/v1/inventories/"+item_id+"/hosts/", + url: hostname + "/api/v1/inventories/"+item_id+"/hosts/", type: "PUT", contentType:'application/json', data:JSON.stringify(hosts_ids), @@ -1245,7 +2580,7 @@ pmInventories.addSubGroups = function(item_id, groups_ids) var def = new $.Deferred(); spajs.ajax.Call({ - url: "/api/v1/inventories/"+item_id+"/groups/", + url: hostname + "/api/v1/inventories/"+item_id+"/groups/", type: "POST", contentType:'application/json', data:JSON.stringify(groups_ids), @@ -1302,7 +2637,7 @@ pmInventories.addSubHosts = function(item_id, hosts_ids) } spajs.ajax.Call({ - url: "/api/v1/inventories/"+item_id+"/hosts/", + url: hostname + "/api/v1/inventories/"+item_id+"/hosts/", type: "POST", contentType:'application/json', data:JSON.stringify(hosts_ids), @@ -1440,12 +2775,6 @@ tabSignal.connect("polemarch.start", function() onOpen:function(holder, menuInfo, data){return pmInventories.showItem(holder, menuInfo, data);} }) - spajs.addMenu({ - id:"inventory-history", - urlregexp:[/^inventory\/([0-9]+)\/history$/, /^inventory\/([0-9]+)\/history\/page\/([0-9]+)$/], - onOpen:function(holder, menuInfo, data){return pmHistory.showListInInventory(holder, menuInfo, data);} - }) - spajs.addMenu({ id:"newInventory", urlregexp:[/^new-inventory$/, /^([A-z0-9_]+)\/([0-9]+)\/new-inventory$/], diff --git a/polemarch/static/js/pmItems.js b/polemarch/static/js/pmItems.js index e6ffdd2c..8d23240b 100644 --- a/polemarch/static/js/pmItems.js +++ b/polemarch/static/js/pmItems.js @@ -156,6 +156,7 @@ pmItems.validateRangeName = function (name) */ pmItems.showList = function (holder, menuInfo, data) { + setActiveMenuLi(); var thisObj = this; var offset = 0 var limit = this.pageSize; @@ -271,7 +272,7 @@ pmItems.copyItem = function (item_id) $.when(encryptedCopyModal.replace(data)).done(function (data) { spajs.ajax.Call({ - url: "/api/v1/" + thisObj.model.name + "/", + url: hostname + "/api/v1/" + thisObj.model.name + "/", type: "POST", contentType: 'application/json', data: JSON.stringify(data), @@ -302,7 +303,7 @@ pmItems.importItem = function (data) var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/" + thisObj.model.name + "/", + url: hostname + "/api/v1/" + thisObj.model.name + "/", type: "POST", contentType: 'application/json', data: JSON.stringify(data), @@ -344,6 +345,7 @@ pmItems.copyAndEdit = function (item_id) pmItems.showItem = function (holder, menuInfo, data) { + setActiveMenuLi(); var thisObj = this; //console.log(menuInfo, data) @@ -364,6 +366,7 @@ pmItems.showItem = function (holder, menuInfo, data) pmItems.showNewItemPage = function (holder, menuInfo, data) { + setActiveMenuLi(); var def = new $.Deferred(); var tpl = this.model.name + '_new_page' @@ -413,7 +416,7 @@ pmItems.loadItems = function (limit, offset) var thisObj = this; return spajs.ajax.Call({ - url: "/api/v1/" + this.model.name + "/", + url: hostname + "/api/v1/" + this.model.name + "/", type: "GET", contentType: 'application/json', data: "limit=" + encodeURIComponent(limit) + "&offset=" + encodeURIComponent(offset), @@ -523,7 +526,7 @@ pmItems.sendSearchQuery = function (query, limit, offset, ordering) var thisObj = this; return spajs.ajax.Call({ - url: "/api/v1/" + this.model.name + "/?" + q.join("&"), + url: hostname + "/api/v1/" + this.model.name + "/?" + q.join("&"), type: "GET", contentType: 'application/json', success: function (data) @@ -591,7 +594,7 @@ pmItems.loadItem = function (item_id) } spajs.ajax.Call({ - url: "/api/v1/" + this.model.name + "/" + item_id + "/", + url: hostname + "/api/v1/" + this.model.name + "/" + item_id + "/", type: "GET", contentType: 'application/json', data: "", @@ -680,7 +683,7 @@ pmItems.deleteRows = function (elements) var thisObj = this; return $.when(spajs.ajax.Call({ - url: "/api/v1/_bulk/", + url: hostname + "/api/v1/_bulk/", type: "POST", contentType: 'application/json', data: JSON.stringify(deleteBulk) @@ -723,7 +726,7 @@ pmItems.deleteSelected = function () } return $.when(spajs.ajax.Call({ - url: "/api/v1/_bulk/", + url: hostname + "/api/v1/_bulk/", type: "POST", contentType: 'application/json', data: JSON.stringify(deleteBulk) @@ -788,7 +791,7 @@ pmItems.deleteItemQuery = function (item_id) this.toggleSelect(item_id, false); return spajs.ajax.Call({ - url: "/api/v1/" + this.model.name + "/" + item_id + "/", + url: hostname + "/api/v1/" + this.model.name + "/" + item_id + "/", type: "DELETE", contentType: 'application/json', success: function (data) @@ -939,7 +942,7 @@ pmItems.addItem = function (parent_type, parent_item, opt) var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/" + this.model.name + "/", + url: hostname + "/api/v1/" + this.model.name + "/", type: "POST", contentType: 'application/json', data: JSON.stringify(data), @@ -1001,7 +1004,7 @@ pmItems.updateItem = function (item_id, opt) var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/" + this.model.name + "/" + item_id + "/", + url: hostname + "/api/v1/" + this.model.name + "/" + item_id + "/", type: "PATCH", contentType: 'application/json', data: JSON.stringify(data), @@ -1009,6 +1012,7 @@ pmItems.updateItem = function (item_id, opt) { thisObj.model.items[item_id] = data $.when(thisObj.model.page_item.onUpdate.apply(thisObj, arguments)).always(function () { + $.notify("Changes in "+thisObj.model.name+" were successfully saved", "success"); def.resolve() }) }, @@ -1071,4 +1075,4 @@ pmItems.checkSubItemsAndAdd=function(thisObj, ObjToAdd, data, itemId, itemType, } } debugger; - }*/ \ No newline at end of file + }*/ diff --git a/polemarch/static/js/pmModuleTemplates.js b/polemarch/static/js/pmModuleTemplates.js index 82a898e3..82f55da8 100644 --- a/polemarch/static/js/pmModuleTemplates.js +++ b/polemarch/static/js/pmModuleTemplates.js @@ -91,6 +91,13 @@ pmModuleTemplates.model.page_item = { link:function(){ return '#'}, help:'Create new option' }, + { + class:'btn btn-info', + function:function(){ return 'return spajs.openURL(this.href);'}, + title:'History', + link:function(item_id){ return polemarch.opt.host +'/?template/'+this.model.kind+'/'+ item_id + '/history'}, + help:'Template execution history' + }, { class:'btn btn-default copy-btn', function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.copyAndEdit('+item_id+')); return false;'}, @@ -131,11 +138,20 @@ pmModuleTemplates.model.page_item = { }, fast_validator:function(value){ return value != '' && value} }, - ],[ + ], + [ { filed: new pmModuleTemplates.filed.selectProjectInventoryGroupAndModule(), name:'inventory', }, + ], + [ + { + filed: new filedsLib.filed.textarea(), + title:'Notes', + name:'notes', + placeholder:'Not required field, just for your notes' + }, ] ], onUpdate:function(result) @@ -163,7 +179,7 @@ pmModuleTemplates.model.page_item_new_option = { buttons:[ { class:'btn btn-primary', - function:function(item_id){ return 'spajs.showLoader(pmModuleTemplates.saveOption('+item_id+')); return false;'}, + function:function(item_id){ return 'spajs.showLoader(pmModuleTemplates.saveNewOption('+item_id+')); return false;'}, title:'Create', link:function(){ return '#'}, } @@ -280,11 +296,40 @@ pmModuleTemplates.saveAndExecute = function(item_id) return def.promise() } +/** + *Функция открывает страницу для создания новой опции. + */ pmModuleTemplates.setNewOption = function(item_id) { return spajs.openURL(window.location.href+"/new-option"); } +/** + *Функция сохраняет новую опцию. + */ +pmModuleTemplates.saveNewOption = function(item_id) +{ + var def = new $.Deferred(); + var optionName=$('#filed_option_name').val(); + optionName=optionName.trim(); + optionName=optionName.replace( /\s/g, "-" ); + var templateOptionList=this.model.items[item_id].options_list; + for (var i=0; i' + item.playbook + '
    '; }, @@ -700,9 +834,9 @@ pmTasksTemplates.showNewItemPage = function(holder, menuInfo, data) term = term.toLowerCase(); var matches = [] - for(var i in pmTasks.model.items) + for(var i in pmTasks.model.itemslist.results) { - var val = pmTasks.model.items[i] + var val=pmTasks.model.itemslist.results[i]; if(val.name.toLowerCase().indexOf(term) != -1 && thisObj.model.selectedProject == val.project) { matches.push(val) @@ -714,6 +848,7 @@ pmTasksTemplates.showNewItemPage = function(holder, menuInfo, data) } } }); + pmTasksTemplates.selectProject($("#projects-autocomplete").val()); def.resolve(); }).fail(function(e) @@ -753,9 +888,11 @@ pmTasksTemplates.addItem = function() return def.promise(); } + data.notes=$("#filed_notes").val(); + var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/templates/", + url: hostname + "/api/v1/templates/", type: "POST", contentType:'application/json', data:JSON.stringify(data), @@ -816,4 +953,4 @@ tabSignal.connect("polemarch.start", function() onOpen:function(holder, menuInfo, data){return pmTasksTemplates.showOptionPage(holder, menuInfo, data);} }) -}) \ No newline at end of file +}) diff --git a/polemarch/static/js/pmTemplates.js b/polemarch/static/js/pmTemplates.js index d56b0afb..2edc3470 100644 --- a/polemarch/static/js/pmTemplates.js +++ b/polemarch/static/js/pmTemplates.js @@ -48,7 +48,7 @@ pmTemplates.execute = function (item_id, option) } var def = new $.Deferred(); spajs.ajax.Call({ - url: "/api/v1/" + this.model.name + "/" + item_id + "/execute/", + url: hostname + "/api/v1/" + this.model.name + "/" + item_id + "/execute/", type: "POST", data: JSON.stringify(option), contentType: 'application/json', @@ -100,7 +100,7 @@ pmTemplates.exportToFile = function (item_ids) var thisObj = this; spajs.ajax.Call({ - url: "/api/v1/" + this.model.name + "/filter/?detail=1", + url: hostname + "/api/v1/" + this.model.name + "/filter/?detail=1", type: "POST", contentType: 'application/json', data: JSON.stringify(data), @@ -191,7 +191,7 @@ pmTemplates.importFromFile = function (files_event, project_id) console.log(bulkdata) spajs.ajax.Call({ - url: "/api/v1/_bulk/", + url: hostname + "/api/v1/_bulk/", type: "POST", contentType: 'application/json', data: JSON.stringify(bulkdata), diff --git a/polemarch/static/js/pmUsers.js b/polemarch/static/js/pmUsers.js index 176490d9..ab4418df 100644 --- a/polemarch/static/js/pmUsers.js +++ b/polemarch/static/js/pmUsers.js @@ -10,9 +10,9 @@ pmUsers.model.page_list = { { class:'btn btn-primary', function:function(){ return "spajs.open({ menuId:'new-"+this.model.page_name+"'}); return false;"}, - title:'Create', - link:function(){ return '/?new-'+this.model.page_name}, - }, + title:'Create', + link:function(){ return '/?new-'+this.model.page_name}, + }, ], title: "Users", short_title: "Users", @@ -20,25 +20,62 @@ pmUsers.model.page_list = { { title:'Name', name:'username', + }, + { + title:'Active', + name:'is_active', + style:function(item, opt){ return 'style="width: 80px"'}, + class:function(item, opt) + { + if(!item || !item.id) + { + return 'class="hidden-xs hidden-sm"'; + } + + return 'class="hidden-xs hidden-sm user-status '+'user-status-'+item.id+ '"'; + }, + value:function(item, filed_name, opt){ + if(this.model.items[item.id].is_active) + { + return '' + } + else + { + return ''; + } + }, } ], actions:[ { - class:'btn btn-danger', function:function(item){ return 'spajs.showLoader('+this.model.className+'.deleteItem('+item.id+')); return false;'}, title:'Delete', link:function(){ return '#'} - } + }, + { + class:function(item){return 'change-activation-'+item.id;}, + function:function(item){ return 'spajs.showLoader('+this.model.className+'.changeItemActivation('+item.id+')); return false;'}, + title:function(item){ + if(this.model.items[item.id].is_active==true) + { + return "Deactivate"; + } + else { + return "Activate"; + } + }, + link:function(){ return '#'} + }, ] } - + pmUsers.model.page_new = { title: "New user", short_title: "New user", fileds:[ [ { - filed: new filedsLib.filed.text(), + filed: new filedsLib.filed.text(), title:'User name', name:'username', placeholder:'Enter user name', @@ -49,7 +86,18 @@ pmUsers.model.page_new = { fast_validator:function(value){ return value != '' && value} }, { - filed: new filedsLib.filed.password(), + filed: new filedsLib.filed.text(), + title:'Email', + name:'email', + placeholder:'Enter user email', + help:'', + } + + ], + + [ + { + filed: new filedsLib.filed.password(), title:'Password', name:'password', placeholder:'Enter user password', @@ -59,86 +107,123 @@ pmUsers.model.page_new = { }, fast_validator:function(value){ return value != '' && value} }, - ],[ { - filed: new filedsLib.filed.text(), - title:'Email', - name:'email', - placeholder:'Enter user email', + filed: new filedsLib.filed.password(), + title:'Confirm password', + name:'confirm_password', + placeholder:'Enter user password again', help:'', - }, + validator:function(value){ + return filedsLib.validator.notEmpty(value, 'Confirm password') + }, + fast_validator:function(value){ return value != '' && value} + } + ], + + [ { - filed: new filedsLib.filed.text(), + filed: new filedsLib.filed.text(), title:'First name', name:'first_name', placeholder:'Enter user first name', help:'', }, - ],[ { - filed: new filedsLib.filed.text(), + filed: new filedsLib.filed.text(), title:'Last name', name:'last_name', placeholder:'Enter user last name', help:'', - }, + } + ],[ { - filed: new filedsLib.filed.boolean(), + filed: new filedsLib.filed.boolean(), title:'Is active', - name:'is_active', + name:'is_active', default:true, } ] ], onBeforeSave:function(data, item_id) { - data.is_staff = true - return data; + if(data.password == data.confirm_password) + { + data.is_staff = true + delete data.confirm_password; + return data; + } + else + { + $.notify("Confirm password value is not the same as password one", "error"); + return false; + } }, onCreate:function(result) - { + { var def = new $.Deferred(); $.notify("User created", "success"); $.when(spajs.open({ menuId:pmUsers.model.page_name+"/"+result.id})).always(function(){ def.resolve() }) - + return def.promise(); } } - + pmUsers.model.page_item = { buttons:[ { class:'btn btn-primary', function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.updateItem('+item_id+')); return false;'}, - title:'Save', - link:function(){ return '#'}, - }, + title:'Save', + link:function(){ return '#'}, + }, + { + class:'btn btn-info', + function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.openChangePasswordForm('+item_id+')); return false;'}, + title:'Change password', + link:function(){ return '#'} + }, { - class:'btn btn-default copy-btn', - function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.copyAndEdit('+item_id+')); return false;'}, - title:'', + class:function(item_id){return 'btn btn-warning change-activation-'+item_id;}, + function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.changeItemActivation('+item_id+')); return false;'}, + title:function(item_id){ + if(this.model.items[item_id].is_active==true) + { + return "Deactivate"; + } + else { + return "Activate"; + } + }, link:function(){ return '#'}, - help:'Copy' + help:function(item_id){ + if(this.model.items[item_id].is_active==true) + { + return "Deactivate account"; + } + else { + return "Activate account"; + } + } }, { class:'btn btn-danger danger-right', function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.deleteItem('+item_id+')); return false;'}, title:' ', - link:function(){ return '#'}, + link:function(){ return '#'}, }, ], - title: function(item_id){ + title: function(item_id){ return "User "+pmUsers.model.items[item_id].justText('username') }, - short_title: function(item_id){ + short_title: function(item_id){ return "User "+pmUsers.model.items[item_id].justText('username', function(v){return v.slice(0, 20)}) }, fileds:[ [ { - filed: new filedsLib.filed.text(), + filed: new filedsLib.filed.text(), title:'User name', name:'username', placeholder:'Enter user name', @@ -149,59 +234,160 @@ pmUsers.model.page_item = { fast_validator:function(value){ return value != '' && value} }, { - filed: new filedsLib.filed.password(), - title:'Password', - name:'password', - placeholder:'Enter user password', - help:'', - }, - ],[ - { - filed: new filedsLib.filed.text(), + filed: new filedsLib.filed.text(), title:'Email', name:'email', placeholder:'Enter user email', help:'', - }, + } + + ],[ + { - filed: new filedsLib.filed.text(), + filed: new filedsLib.filed.text(), title:'First name', name:'first_name', placeholder:'Enter user first name', help:'', }, - ],[ { - filed: new filedsLib.filed.text(), + filed: new filedsLib.filed.text(), title:'Last name', name:'last_name', placeholder:'Enter user last name', help:'', + } + ] + ], + onUpdate:function(result) + { + return true; + }, + onBeforeSave:function(data, item_id) + { + if(!data.password) + { + delete data.password + } + data.is_staff = true + + return data; + }, +} + +pmUsers.model.profile_page = { + buttons:[ + { + class:'btn btn-primary', + function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.updateProfile('+item_id+')); return false;'}, + title:'Save', + link:function(){ return '#'}, + }, + { + class:'btn btn-info', + function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.openChangePasswordForm('+item_id+')); return false;'}, + title:'Change password', + link:function(){ return '#'} + }, + { + class:function(item_id){return 'btn btn-warning change-activation-'+item_id;}, + function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.changeItemActivation('+item_id+')); return false;'}, + title:function(item_id){ + if(this.model.items[item_id].is_active==true) + { + return "Deactivate"; + } + else { + return "Activate"; + } + }, + link:function(){ return '#'}, + help:function(item_id){ + if(this.model.items[item_id].is_active==true) + { + return "Deactivate account"; + } + else { + return "Activate account"; + } + } + }, + { + class:'btn btn-danger danger-right', + function:function(item_id){ return 'spajs.showLoader('+this.model.className+'.deleteItem('+item_id+')); return false;'}, + title:' ', + link:function(){ return '#'}, + }, + ], + title: function(item_id){ + return pmUsers.model.items[item_id].justText('username').replace(/\b\w/g, function(l){ return l.toUpperCase() }) + " profile"; + }, + short_title: function(item_id){ + return pmUsers.model.items[item_id].justText('username', function(v){return v.slice(0, 20)}).replace(/\b\w/g, function(l){ return l.toUpperCase() }) + " profile"; + }, + fileds:[ + [ + { + filed: new filedsLib.filed.text(), + title:'User name', + name:'username', + placeholder:'Enter user name', + help:'', + validator:function(value){ + return filedsLib.validator.notEmpty(value, 'Name') + }, + fast_validator:function(value){ return value != '' && value} }, { - filed: new filedsLib.filed.boolean(), - title:'Is active', - name:'is_active', + filed: new filedsLib.filed.text(), + title:'Email', + name:'email', + placeholder:'Enter user email', + help:'', + } + + ],[ + + { + filed: new filedsLib.filed.text(), + title:'First name', + name:'first_name', + placeholder:'Enter user first name', + help:'', + }, + { + filed: new filedsLib.filed.text(), + title:'Last name', + name:'last_name', + placeholder:'Enter user last name', + help:'', } ] ], + sections:[ + function(){ + return spajs.just.render("WidgetsSettingsFromProfile"); + } + ], onUpdate:function(result) - { + { return true; }, onBeforeSave:function(data, item_id) - { + { if(!data.password) { - delete data.password + delete data.password } data.is_staff = true return data; }, } - - + +/** + *Функиця создает копию пользователя. + */ pmUsers.copyItem = function(item_id) { var def = new $.Deferred(); @@ -212,15 +398,15 @@ pmUsers.copyItem = function(item_id) var data = thisObj.model.items[item_id]; delete data.id; data.username = "copy-from-" + data.username - + $.when(encryptedCopyModal.replace(data)).done(function(data) { spajs.ajax.Call({ - url: "/api/v1/"+thisObj.model.name+"/", + url: hostname + "/api/v1/"+thisObj.model.name+"/", type: "POST", contentType:'application/json', data: JSON.stringify(data), - success: function(data) + success: function(data) { thisObj.model.items[data.id] = data def.resolve(data.id) @@ -233,7 +419,7 @@ pmUsers.copyItem = function(item_id) }).fail(function(e) { def.reject(e) - }) + }) }).fail(function(e) { def.reject(e) @@ -243,9 +429,229 @@ pmUsers.copyItem = function(item_id) return def.promise(); } - - tabSignal.connect("polemarch.start", function() - { +/** + *Данная функция в зависимости от текущего статуса пользователя + *вызывает функцию активации/деактивации пользователя соответственно. + */ +pmUsers.changeItemActivation = function(item_id) { + if(pmUsers.model.items[item_id].is_active==true) + { + return pmUsers.deactivateItem(item_id); + } + else + { + return pmUsers.activateItem(item_id); + } +} + +/** + *Функция деактивирует пользователя, переводя значение поля is_active из true в false. + */ +pmUsers.deactivateItem = function(item_id, force) { + var thisObj = this; + var def = new $.Deferred(); + if (!force && !confirm("Are you sure, that you want to deactivate this account? Only superuser will be able to activate it again.")) + { + def.reject(); + return def.promise() + } + var data={ "is_active": false } + spajs.ajax.Call({ + url: hostname + "/api/v1/"+thisObj.model.name+"/"+item_id+"/", + type: "PATCH", + contentType:'application/json', + data: JSON.stringify(data), + success: function(data) + { + if(my_user_id==item_id) + { + spajs.openURL(window.location.protocol+"//"+window.location.host); + } + else { + $.notify("Account was deactivated", "success"); + pmUsers.model.items[item_id].is_active=!pmUsers.model.items[item_id].is_active; + var t=$(".change-activation-"+item_id)[0]; + $(t).html("Activate"); + $(t).attr("title", "Activate account"); + var s=$(".user-status-"+item_id)[0]; + $(s).html(""); + } + def.resolve() + }, + error:function(e) + { + if(e.status==403) + { + $.notify("You do not have permission to perform this action.", "error"); + } + def.reject(e) + } + }); + + return def.promise(); +} + +/** + *Функция активирует пользователя, переводя значение поля is_active из false в true. + */ +pmUsers.activateItem = function(item_id, force) { + var thisObj = this; + var def = new $.Deferred(); + var data={ "is_active": true } + spajs.ajax.Call({ + url: hostname + "/api/v1/"+thisObj.model.name+"/"+item_id+"/", + type: "PATCH", + contentType:'application/json', + data: JSON.stringify(data), + success: function(data) + { + $.notify("Account was activated", "success"); + pmUsers.model.items[item_id].is_active=!pmUsers.model.items[item_id].is_active; + var t=$(".change-activation-"+item_id)[0]; + $(t).html("Deactivate"); + $(t).attr("title", "Deactivate account"); + var s=$(".user-status-"+item_id)[0]; + $(s).html(''); + def.resolve() + }, + error:function(e) + { + if(e.status==403) + { + $.notify("You do not have permission to perform this action.", "error"); + } + def.reject(e) + } + }); + + return def.promise(); +} + +/** + *Функция открывает модальное окно с формой для изменения пароля. + */ +pmUsers.openChangePasswordForm = function(item_id) +{ + if($('div').is('#change_password_form')) + { + $('#change_password_form').empty(); + $('#change_password_form').html(pmUsers.renderModalWindow(item_id)); + $("#change_password_form").modal('show'); + } + else + { + var t=$(".content")[0]; + $('
    ', { id: "change_password_form", class: "modal fade in"}).appendTo(t); + $('#change_password_form').html(pmUsers.renderModalWindow(item_id)); + $("#change_password_form").modal('show'); + } +} + +/** + *Функция рендинга модального окна для смены пароля пользователя. + */ +pmUsers.renderModalWindow = function(item_id) +{ + var html=spajs.just.render('change_password_form', {item_id: item_id}); + return html; +} + +/** + *Функция смены пароля пользователя: пользователь вводит новый ароль дважды, чтобы исключить вероятность опечатки. + *Если оба введенных значения идентичны друг другу, то новый пароль сохраняется. + *В противном случае выводится сообщение об ошибке. + */ +pmUsers.changePassword = function(item_id) +{ + var thisObj = this; + var def = new $.Deferred(); + var newPassword1 = $("#new_password").val(); + var newPassword2 = $("#new_password_confirm").val(); + if(newPassword1==newPassword2 && newPassword1==""){ + $.notify("Form is empty", "error"); + def.resolve(); + return def.promise(); + } + if(newPassword1==newPassword2 && newPassword1!="") + { + var data={"password": newPassword1}; + spajs.ajax.Call({ + url: hostname + "/api/v1/"+thisObj.model.name+"/"+item_id+"/", + type: "PATCH", + contentType:'application/json', + data: JSON.stringify(data), + success: function(data) + { + + if(my_user_id==item_id) + { + return $.when(hidemodal(), $("#change_password_form").modal('hide')).done(function(){ + def.resolve(); + return spajs.openURL("/"); + }).promise(); + } + else + { + $.notify("Password was successfully changed", "success"); + $("#change_password_form").modal('hide'); + def.resolve(); + } + + }, + error:function(e) + { + def.reject(e) + } + }); + } + else + { + $.notify("Confirm password value is not the same as new password one", "error"); + def.reject(); + } + return def.promise(); +} + +/** + *Функция, открывающая страницу с профайлом пользователя. + */ +pmUsers.showProfile = function (holder, menuInfo, data) +{ + setActiveMenuLi(); + var thisObj = this; + //console.log(menuInfo, data) + + return $.when(pmDashboard.getUserWidgetSettingsFromAPI(), this.loadItem(data.reg[1])).done(function () + { + var tpl = "profile_page" + if (!spajs.just.isTplExists(tpl)) + { + tpl = 'items_page' + } + + $(holder).insertTpl(spajs.just.render(tpl, {item_id: data.reg[1], pmObj: thisObj, opt: {}})) + }).fail(function () + { + $.notify("", "error"); + }).promise() +} + +/** + *Функция, сохраняющая все настройки профиля пользоваетля. + */ +pmUsers.updateProfile = function (item_id) { + return $.when(pmUsers.updateItem(item_id), pmDashboard.saveWigdetsOptionsFromProfile()).done(function () + { + $.notify("Profile was successfully updated", "success"); + }).fail(function () + { + $.notify("Profile was not updated", "error"); + }).promise() +} + + +tabSignal.connect("polemarch.start", function() +{ // users spajs.addMenu({ id:"users", @@ -271,4 +677,10 @@ pmUsers.copyItem = function(item_id) onOpen:function(holder, menuInfo, data){return pmUsers.showNewItemPage(holder, menuInfo, data);} }) - }) + spajs.addMenu({ + id:"profile", + urlregexp:[/^profile\/([0-9]+)$/], + onOpen:function(holder, menuInfo, data){return pmUsers.showProfile(holder, menuInfo, data);} + }) + +}) diff --git a/polemarch/static/js/polemarch.js b/polemarch/static/js/polemarch.js index 1cc58a2c..fba8bdcf 100644 --- a/polemarch/static/js/polemarch.js +++ b/polemarch/static/js/polemarch.js @@ -204,3 +204,23 @@ spajs.errorPage = function(holder, menuInfo, data, error_data) $(holder).insertTpl(spajs.just.render("errorPage", {error:error, data:data, menuInfo:menuInfo})) } + + + +tabSignal.connect("loading.completed", function() +{ + var just = new JUST({ + root : { + 'app-body-gui':$("script[data-just=app-body-gui]").html() + } + }); + + $("body").appendTpl(just.render('app-body-gui', {})) + + polemarch.start({ + is_superuser:window.is_superuser, + holder:'#spajs-body' + }) + + hideLoadingProgress(); +}) \ No newline at end of file diff --git a/polemarch/static/js/tests/phantomjs/injectTest.js b/polemarch/static/js/tests/phantomjs/injectTest.js index 0973bd0b..fd2aa2e7 100644 --- a/polemarch/static/js/tests/phantomjs/injectTest.js +++ b/polemarch/static/js/tests/phantomjs/injectTest.js @@ -156,7 +156,7 @@ function startTest() console.log("Тест формы авторизации шаг 2"); spajs.ajax.Call({ - url: "/api/v1/users/", + url: hostname + "/api/v1/users/", type: "GET", contentType:'application/json', data: "", diff --git a/polemarch/static/js/tests/qUnitTest.js b/polemarch/static/js/tests/qUnitTest.js index cb9f497c..6a6d7702 100644 --- a/polemarch/static/js/tests/qUnitTest.js +++ b/polemarch/static/js/tests/qUnitTest.js @@ -211,7 +211,7 @@ function render(name, callback) var def = new $.Deferred(); var time = 4 - setTimeout(function(name){ + setTimeout(function(name){ setTimeout(function(){ if(callback) @@ -235,9 +235,9 @@ function saveReport() /** * В этом массиве должны быть qunit тесты для приложения - */ + */ window.qunitTestsArray = [] - + /** * Вставляет Qunit и запускает выполнение тестов. */ @@ -276,7 +276,7 @@ function injectQunit() "Skipped": details.skipped, "Runtime": details.runtime }; - + if(!syncQUnit.nextTest()) { saveReport() @@ -345,25 +345,25 @@ window.qunitTestsArray.push({ assert.equal(trim(' x y '), 'x y', 'Табы и пробелы внутри строки не трогаем'); render(done); - }); - + }); + syncQUnit.addTest('trim', function ( assert ) { var done = assert.async(); - + var stringArr = [ "abc", "A \" A", "A ' \\ \\\\ \t \\ \ g \" ' \' ", '"', - 'a"b\' g \" \t \ \\ \\\ c', + 'a"b\' g \" \t \ \\ \\\ c', ' \' \\ " \t \r \b \e \c \d \n' , ]; for(var i in stringArr) { - assert.equal(stripslashes(addslashes(stringArr[i])), stringArr[i], "i:"+stringArr[i]); + assert.equal(stripslashes(addslashes(stringArr[i])), stringArr[i], "i:"+stringArr[i]); } render(done); - }); + }); }}) /** @@ -376,63 +376,63 @@ window.qunitTestsArray.push({ syncQUnit.addTest('crontabEditor', function ( assert ) { var done = assert.async(); - + var cronString = "1 * * * *" - - crontabEditor.parseCronString(undefined) + + crontabEditor.parseCronString(undefined) assert.ok(cronString != crontabEditor.getCronString(), 'getCronString'); - - crontabEditor.parseCronString("1 5") + + crontabEditor.parseCronString("1 5") assert.ok(cronString != crontabEditor.getCronString(), 'getCronString'); - + crontabEditor.parseCronString(cronString) assert.ok(cronString == crontabEditor.getCronString(), 'getCronString'); - + cronString = "1 1 1 1 1" - crontabEditor.parseCronString(cronString) + crontabEditor.parseCronString(cronString) assert.ok("1 1 1 1 1" == crontabEditor.getCronString(), 'getCronString'); - + crontabEditor.setDaysOfWeek("1-2") assert.ok("1 1 1 1 1,2" == crontabEditor.getCronString(), 'getCronString'); - + crontabEditor.setMonths("1-2") assert.ok("1 1 1 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + crontabEditor.setDayOfMonth("1-2") assert.ok("1 1 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + crontabEditor.setHours("1-2") assert.ok("1 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + crontabEditor.setMinutes("1-2") - assert.ok("1,2 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("1,2 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + crontabEditor.setMinutes("1,2,7") - assert.ok("1,2,7 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("1,2,7 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + crontabEditor.setMinutes("1,2,*/7") - assert.ok("*/7,1,2 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("*/7,1,2 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + crontabEditor.setMinutes("1,2,3,4,*/7") - assert.ok("*/7,1-4 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("*/7,1-4 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + crontabEditor.setMinutes("1,2,3,4,*/7,45-51") - assert.ok("*/7,1-4,45-48,50,51 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("*/7,1-4,45-48,50,51 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + crontabEditor.setMinutes("1,2,3,4,*/7,45-51,17-30/2") - assert.ok("*/7,*/23,*/25,1-4,17,19,27,29,45,47,48,51 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("*/7,*/23,*/25,1-4,17,19,27,29,45,47,48,51 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + crontabEditor.setMinutes("1,2,3,4,*/7,45-51,17-380/2") - assert.ok("0-4,7,14,17,19,21,23,25,27-29,31,33,35,37,39,41-43,45-51,53,55-57,59 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("0-4,7,14,17,19,21,23,25,27-29,31,33,35,37,39,41-43,45-51,53,55-57,59 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + crontabEditor.setMinutes("1,2,3,4,*/7,45-51,170-38/2") - assert.ok("*/7,*/12,*/16,1-4,6,8,10,18,20,22,26,30,34,38,45-47,50,51 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("*/7,*/12,*/16,1-4,6,8,10,18,20,22,26,30,34,38,45-47,50,51 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + crontabEditor.setMinutes("1,2,3,4,5/5,45-51,170-38/2") - assert.ok("*/5,*/12,*/16,1-4,6,8,14,18,22,26,28,34,38,46,47,49,51 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); - + assert.ok("*/5,*/12,*/16,1-4,6,8,14,18,22,26,28,34,38,46,47,49,51 1,2 1,2 1,2 1,2" == crontabEditor.getCronString(), 'getCronString'); + render(done) - }); + }); }}) /** @@ -459,11 +459,11 @@ window.qunitTestsArray.push({ }); syncQUnit.addTest('Страница списка пользователей toggleSelectEachItem', function ( assert ) - { + { var done = assert.async(); - - pmUsers.toggleSelectAll($('.multiple-select tr'), true); - + + pmUsers.toggleSelectAll($('.multiple-select tr'), true); + $.when(pmUsers.toggleSelectEachItem(true)).done(function() { assert.ok(true, 'ok:toggleSelectEachItem'); @@ -475,7 +475,7 @@ window.qunitTestsArray.push({ render(done) }) }) - + syncQUnit.addTest('Открытие страницы добавления пользователя', function ( assert ) { var done = assert.async(); @@ -520,7 +520,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Создание пользователя с ошибкой 2', function ( assert ) { // Предполагается что мы от прошлого теста попали на страницу создания пользователя @@ -545,7 +545,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Создание пользователя', function ( assert ) { // Предполагается что мы от прошлого теста попали на страницу создания пользователя @@ -554,10 +554,11 @@ window.qunitTestsArray.push({ // Заполнение формы с данными пользователя $("#filed_username").val("test-user-"+t); $("#filed_password").val("test-user-"+t); + $("#filed_confirm_password").val("test-user-"+t); $("#filed_email").val("test2@user.ru"); $("#filed_first_name").val("test"); $("#filed_last_name").val("user"); - + // Отправка формы с данными пользователя $.when(pmUsers.addItem()).done(function() { @@ -581,11 +582,10 @@ window.qunitTestsArray.push({ userId = /user\/([0-9]+)/.exec(window.location.href)[1] $("#filed_username").val("admin"); - $("#filed_password").val("test2-user-"+t); $("#filed_email").val("test2@user.ru"); $("#filed_first_name").val("test2-"+t); $("#filed_last_name").val("user2-"+t); - + $.when(pmUsers.updateItem(userId)).done(function() { debugger; @@ -600,13 +600,12 @@ window.qunitTestsArray.push({ syncQUnit.addTest('Изменение пользователя', function ( assert ) { var done = assert.async(); - + // Предполагается что мы от прошлого теста попали на страницу редактирования пользователя // с адресом http://192.168.0.12:8080/?user-5 userId = /user\/([0-9]+)/.exec(window.location.href)[1] $("#filed_username").val("test2-user-"+t); - $("#filed_password").val("test2-user-"+t); $("#filed_email").val("test2@user.ru"); $("#filed_first_name").val("test2-"+t); $("#filed_last_name").val("user2-"+t); @@ -623,9 +622,10 @@ window.qunitTestsArray.push({ }) }); + /* syncQUnit.addTest('Копирование пользователя с ошибкой', function ( assert ) { - var done = assert.async(); + var done = assert.async(); $.when(pmUsers.copyAndEdit(999999)).done(function() { debugger; @@ -635,10 +635,10 @@ window.qunitTestsArray.push({ assert.ok(true, 'Ошибка при copyAndEdit add Item, как и задумано'); render(done) }) - }); - + }); + syncQUnit.addTest('Копирование пользователя', function ( assert ) - { + { var done = assert.async(); $.when(pmUsers.copyAndEdit(userId)).done(function() @@ -656,7 +656,7 @@ window.qunitTestsArray.push({ syncQUnit.addTest('Удаление копии пользователя с ошибкой', function ( assert ) { var done = assert.async(); - + // Удаление пользователя. $.when(pmUsers.deleteItem(999999, true)).done(function() { @@ -689,7 +689,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + */ syncQUnit.addTest('Удаление пользователя', function ( assert ) { var done = assert.async(); @@ -767,14 +767,14 @@ window.qunitTestsArray.push({ var t = new Date(); t = t.getTime() - + syncQUnit.addTest('Создание хоста', function ( assert ) { // Предполагается что мы от прошлого теста попали на страницу создания хоста var done = assert.async(); // Заполнение формы с данными хоста - $("#filed_name").val("test-host-"+t); + $("#filed_name").val("test-host-"+t); $("#new_json_nameprefix").val("test1"); $("#new_json_valueprefix").val("val1"); @@ -947,7 +947,7 @@ window.qunitTestsArray.push({ t = t.getTime() syncQUnit.addTest('Сохранение группы', function ( assert ) - { + { // Предполагается что мы от прошлого теста попали на страницу создания группы var done = assert.async(); @@ -1011,8 +1011,8 @@ window.qunitTestsArray.push({ // с адресом http://192.168.0.12:8080/?group-5 var itemId = /group\/([0-9]+)/.exec(window.location.href)[1] - - assert.ok(!pmGroups.hasGroups(itemId, 99999999999), 'pmGroups.hasGroups() вернула не тот результат'); + + assert.ok(!pmGroups.hasGroups(itemId, 99999999999), 'pmGroups.hasGroups() вернула не тот результат'); $.when(pmGroups.setSubGroups(itemId, [99999999999])).done(function() { @@ -1025,7 +1025,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Обновление группы setSubHosts', function ( assert ) { var done = assert.async(); @@ -1034,7 +1034,7 @@ window.qunitTestsArray.push({ // с адресом http://192.168.0.12:8080/?group-5 var itemId = /group\/([0-9]+)/.exec(window.location.href)[1] - + assert.ok(!pmGroups.hasHosts(itemId, 99999999999), 'pmGroups.hasHosts() вернула не тот результат'); $.when(pmGroups.setSubHosts(itemId, [99999999999])).done(function() @@ -1055,7 +1055,7 @@ window.qunitTestsArray.push({ // Предполагается что мы от прошлого теста попали на страницу редактирования группы // с адресом http://192.168.0.12:8080/?group-5 var itemId = /group\/([0-9]+)/.exec(window.location.href)[1] - + $.when(pmGroups.showAddSubGroupsForm(itemId)).done(function() { assert.ok(true, 'Успешно showAddSubGroupsForm'); @@ -1067,7 +1067,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('pmGroups.showAddSubHostsForm', function ( assert ) { var done = assert.async(); @@ -1075,7 +1075,7 @@ window.qunitTestsArray.push({ // Предполагается что мы от прошлого теста попали на страницу редактирования группы // с адресом http://192.168.0.12:8080/?group-5 var itemId = /group\/([0-9]+)/.exec(window.location.href)[1] - + $.when(pmGroups.showAddSubHostsForm(itemId)).done(function() { assert.ok(true, 'Успешно showAddSubHostsForm'); @@ -1087,7 +1087,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Открытие страницы создания подгруппы', function ( assert ) { var done = assert.async(); @@ -1109,7 +1109,7 @@ window.qunitTestsArray.push({ }) }); - var itemId = undefined + var itemId = undefined syncQUnit.addTest('Сохранение подгруппы', function ( assert ) { // Предполагается что мы от прошлого теста попали на страницу создания группы @@ -1125,10 +1125,10 @@ window.qunitTestsArray.push({ $("#new_json_nameprefix").val("test2"); $("#new_json_valueprefix").val("val2"); jsonEditor.jsonEditorAddVar(); - + var master_group_itemId = /group\/([0-9]+)/.exec(window.location.href)[1] itemId = master_group_itemId - + // Отправка формы с данными группы $.when(pmGroups.addItem('group', master_group_itemId)).done(function() { @@ -1241,10 +1241,10 @@ window.qunitTestsArray.push({ render(done) }) }); - - - - + + + + syncQUnit.addTest('Страница создания группы не children', function ( assert ) { var done = assert.async(); @@ -1280,14 +1280,14 @@ window.qunitTestsArray.push({ $("#new_json_nameprefix").val("test4"); $("#new_json_valueprefix").val("val4"); jsonEditor.jsonEditorAddVar(); - + // Отправка формы с данными группы $.when(pmGroups.addItem()).done(function() { assert.ok(true, 'Успешно group add Item'); render(done) }).fail(function() - { + { debugger; assert.ok(false, 'Ошибка при group add Item'); render(done) @@ -1295,15 +1295,15 @@ window.qunitTestsArray.push({ }); syncQUnit.addTest('Обновление группы не children', function ( assert ) - { + { var done = assert.async(); // Предполагается что мы от прошлого теста попали на страницу редактирования группы // с адресом http://192.168.0.12:8080/?group-5 itemId = /group\/([0-9]+)/.exec(window.location.href)[1] - - assert.ok(!pmGroups.hasGroups(itemId, 99999999999), 'pmGroups.hasGroups() вернула не тот результат'); + + assert.ok(!pmGroups.hasGroups(itemId, 99999999999), 'pmGroups.hasGroups() вернула не тот результат'); $.when(pmGroups.setSubGroups(itemId, [99999999999])).done(function() { @@ -1315,10 +1315,10 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Обновление группы не children', function ( assert ) - { - var done = assert.async(); + { + var done = assert.async(); assert.ok(!pmGroups.hasHosts(itemId, 99999999999), 'pmGroups.hasHosts() вернула не тот результат'); $.when(pmGroups.setSubHosts(itemId, [99999999999])).done(function() @@ -1347,10 +1347,10 @@ window.qunitTestsArray.push({ render(done) }) }); - - - - + + + + }}) @@ -1394,7 +1394,7 @@ window.qunitTestsArray.push({ }) }); - + var t = new Date(); t = t.getTime() @@ -1543,12 +1543,12 @@ window.qunitTestsArray.push({ // Предполагается что мы от прошлого теста попали на страницу редактирования группы // с адресом http://192.168.0.12:8080/?group-5 var itemId = /inventory\/([0-9]+)/.exec(window.location.href)[1] - + pmGroups.groupsAutocompleteMatcher("A", function(res){ - assert.ok(true, 'Успешно'); - }, itemId) + assert.ok(true, 'Успешно'); + }, itemId) }); - + syncQUnit.addTest('Удаление копии Inventory', function ( assert ) { var done = assert.async(); @@ -1571,7 +1571,7 @@ window.qunitTestsArray.push({ syncQUnit.addTest('Страница списка inventory History', function ( assert ) { - var done = assert.async(); + var done = assert.async(); $.when(spajs.open({ menuId:'inventory/'+itemId+'/history'})).done(function() { assert.ok(true, 'Страница открылась'); @@ -1583,8 +1583,8 @@ window.qunitTestsArray.push({ render(done) }) }) - - + + syncQUnit.addTest('Удаление inventory', function ( assert ) { var done = assert.async(); @@ -1702,11 +1702,11 @@ os-controller-2.vst.lan ansible_host=10.20.0.8 }) }); - var etalon = {"hosts":[{"name":"1.2.3.[1:255]","type":"RANGE","vars":{}},{"name":"124.3.4.[44:55]","type":"RANGE","vars":{}},{"name":"124.3.5.[1:250]","type":"RANGE","vars":{"ansible_host":"10.20.0.2","ansible_user":"root","ansible_ssh_pass":"eadgbe","ansible_ssh_private_key_file":"/root/f.txt"}},{"name":"124.3.5.[1:251]","type":"RANGE","vars":{"ansible_host":"10.20.0.2","ansible_user":"root","ansible_ssh_pass":"eadgbe"}},{"name":"124.3.5.[1:252]","type":"RANGE","vars":{"ansible_host":"10.20.0.12","ansible_user":"r\"o\'ot","ansible_ssh_pass":"eadgbe"}}],"groups":{"git":{"vars":{},"groups":["ci","git-servers"],"hosts":[],"children":true,"dataLevel":{"level":2,"parents":["all","cloud","git"]}},"ci":{"vars":{},"groups":[],"hosts":[{"name":"git-ci-1","type":"HOST","vars":{"ansible_host":"172.16.1.13","ansible_ssh_private_key_file":"/root/f.txt"}},{"name":"git-ci-2","type":"HOST","vars":{"ansible_host":"172.16.1.14"}}],"dataLevel":{"level":3,"parents":["all","cloud","git","ci"]}},"git-servers":{"vars":{},"groups":[],"hosts":[{"name":"git.vst.lan","type":"HOST","vars":{}}],"dataLevel":{"level":3,"parents":["all","cloud","git","git-servers"]}},"cloud":{"vars":{},"groups":["git","services","test"],"hosts":[],"children":true,"dataLevel":{"level":1,"parents":["all","cloud"]}},"services":{"vars":{},"groups":[],"hosts":[{"name":"chat.vstconsulting.net","type":"HOST","vars":{"ansible_host":"172.16.1.16"}},{"name":"pipc.vst.lan","type":"HOST","vars":{}},{"name":"redmine.vst.lan","type":"HOST","vars":{}}],"dataLevel":{"level":2,"parents":["all","cloud","services"]}},"test":{"vars":{"ansible_ssh_private_key_file":"/root/f.txt"},"groups":[],"hosts":[{"name":"test.vst.lan","type":"HOST","vars":{"ansible_user":"centos"}},{"name":"test2.vst.lan","type":"HOST","vars":{"ansible_host":"172.16.1.26"}}],"dataLevel":{"level":2,"parents":["all","cloud","test"]}},"openstack":{"vars":{},"groups":[],"hosts":[{"name":"fuel.vst.lan","type":"HOST","vars":{"ansible_host":"10.20.0.2","ansible_user":"root","ansible_ssh_pass":"eadgbe"}},{"name":"os-compute-1.vst.lan","type":"HOST","vars":{"ansible_host":"10.20.0.9"}},{"name":"os-compute-2.vst.lan","type":"HOST","vars":{"ansible_host":"10.20.0.13","ansible_ssh_private_key_file":"/root/f.txt"}},{"name":"os-controller-1.vst.lan","type":"HOST","vars":{"ansible_host":"10.20.0.6"}},{"name":"os-controller-2.vst.lan","type":"HOST","vars":{"ansible_host":"10.20.0.8"}}],"dataLevel":{"level":1,"parents":["all","openstack"]}}},"vars":{"ansible_user":"grey","ansible_ssh_private_key_file":"/root/f.txt","ansible_ssh_extra_args":"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"},"name":"inventory"} + var etalon = {"hosts":[{"name":"1.2.3.[1:255]","notes":"","type":"RANGE","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{}},{"name":"124.3.4.[44:55]","notes":"","type":"RANGE","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{}},{"name":"124.3.5.[1:250]","notes":"","type":"RANGE","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"10.20.0.2","ansible_user":"root","ansible_ssh_pass":"eadgbe","ansible_ssh_private_key_file":"/root/f.txt"}},{"name":"124.3.5.[1:251]","notes":"","type":"RANGE","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"10.20.0.2","ansible_user":"root","ansible_ssh_pass":"eadgbe"}},{"name":"124.3.5.[1:252]","notes":"","type":"RANGE","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"10.20.0.12","ansible_user":"r\"o'ot","ansible_ssh_pass":"eadgbe"}}],"groups":{"git":{"notes":"","vars":{},"groups":["ci","git-servers"],"hosts":[],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"children":true,"dataLevel":{"level":2,"parents":["all","cloud","git"]}},"ci":{"notes":"","vars":{},"groups":[],"hosts":[{"name":"git-ci-1","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"172.16.1.13","ansible_ssh_private_key_file":"/root/f.txt"}},{"name":"git-ci-2","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"172.16.1.14"}}],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"dataLevel":{"level":3,"parents":["all","cloud","git","ci"]}},"git-servers":{"notes":"","vars":{},"groups":[],"hosts":[{"name":"git.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{}}],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"dataLevel":{"level":3,"parents":["all","cloud","git","git-servers"]}},"cloud":{"notes":"","vars":{},"groups":["git","services","test"],"hosts":[],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"children":true,"dataLevel":{"level":1,"parents":["all","cloud"]}},"services":{"notes":"","vars":{},"groups":[],"hosts":[{"name":"chat.vstconsulting.net","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"172.16.1.16"}},{"name":"pipc.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{}},{"name":"redmine.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{}}],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"dataLevel":{"level":2,"parents":["all","cloud","services"]}},"test":{"notes":"","vars":{"ansible_ssh_private_key_file":"/root/f.txt"},"groups":[],"hosts":[{"name":"test.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_user":"centos"}},{"name":"test2.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"172.16.1.26"}}],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"dataLevel":{"level":2,"parents":["all","cloud","test"]}},"openstack":{"notes":"","vars":{},"groups":[],"hosts":[{"name":"fuel.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"10.20.0.2","ansible_user":"root","ansible_ssh_pass":"eadgbe"}},{"name":"os-compute-1.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"10.20.0.9"}},{"name":"os-compute-2.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"10.20.0.13","ansible_ssh_private_key_file":"/root/f.txt"}},{"name":"os-controller-1.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"10.20.0.6"}},{"name":"os-controller-2.vst.lan","notes":"","type":"HOST","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{"ansible_host":"10.20.0.8"}}],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"dataLevel":{"level":1,"parents":["all","openstack"]}}},"vars":{"ansible_user":"grey","ansible_ssh_private_key_file":"/root/f.txt","ansible_ssh_extra_args":"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"},"name":"inventory","notes":""} var inventory = undefined; syncQUnit.addTest('Парсинг inventory', function ( assert ) - { + { var done = assert.async(); inventory = pmInventories.parseFromText(pmInventoriesText) inventory.name = "inventory" @@ -1715,19 +1715,19 @@ os-controller-2.vst.lan ansible_host=10.20.0.8 inventory:inventory, text:pmInventoriesText } - - var res = deepEqual(etalon, inventory) + + var res = deepEqual(etalon, inventory) assert.ok(res, 'Сравнение инвентория распарсенного и оригинального'); render(done) }); - + syncQUnit.addTest('Импорт не валидного inventory 1', function ( assert ) - { + { var done = assert.async(); - + $.when(spajs.open({ menuId:"inventories/import"})).done(function() { - assert.ok(true, 'Успешно открыто меню inventories/import'); + assert.ok(true, 'Успешно открыто меню inventories/import'); $("#inventory_name").val("inventory") $.when(pmInventories.importInventory(inventory)).done(function() @@ -1736,7 +1736,7 @@ os-controller-2.vst.lan ansible_host=10.20.0.8 assert.ok(false, 'Успешно импортирован не валидный инвенторий (а это не правильно)'); render(done) }).fail(function() - { + { assert.ok(true, 'Ошибка в импорте не валидного инвентория (как и задумано)'); render(done) }) @@ -1745,33 +1745,32 @@ os-controller-2.vst.lan ansible_host=10.20.0.8 debugger; assert.ok(false, 'Ошибка при открытиии меню inventories/import'); render(done) - }) + }) }); syncQUnit.addTest('Импорт валидного inventory', function ( assert ) - { + { delete inventory.vars.ansible_ssh_private_key_file for(var i in inventory.hosts) - { - delete inventory.hosts[i].vars.ansible_ssh_private_key_file + { + delete inventory.hosts[i].vars.ansible_ssh_private_key_file } for(var i in inventory.groups) { - delete inventory.groups[i].vars.ansible_ssh_private_key_file + delete inventory.groups[i].vars.ansible_ssh_private_key_file for(var j in inventory.groups[i].hosts) { - delete inventory.groups[i].hosts[j].vars.ansible_ssh_private_key_file + delete inventory.groups[i].hosts[j].vars.ansible_ssh_private_key_file } } - var done = assert.async(); $("#inventory_name").val("inventory") $.when(pmInventories.importInventory(inventory)).done(function() { - assert.ok(true, 'Успешно импортирован инвенторий'); + assert.ok(true, 'Успешно импортирован инвенторий'); render(done) }).fail(function() - { + { debugger; assert.ok(false, 'Ошибка в импорте инвентория'); render(done) @@ -1779,7 +1778,7 @@ os-controller-2.vst.lan ansible_host=10.20.0.8 }); syncQUnit.addTest('Импорт не валидного inventory', function ( assert ) - { + { var done = assert.async(); inventory.groups["error group"] = { "vars": {}, @@ -1787,15 +1786,15 @@ os-controller-2.vst.lan ansible_host=10.20.0.8 "hosts": [], "children": true } - - $("#inventory_name").val("inventory") + + $("#inventory_name").val("inventory") $.when(pmInventories.importInventory(inventory)).done(function() - { + { debugger; assert.ok(false, 'Успешно импортирован инвенторий а должна быть ошибка'); render(done) }).fail(function() - { + { assert.ok(true, 'Ошибка в импорте инвентория как и задумано'); render(done) }) @@ -1810,43 +1809,15 @@ oxNzIuMTYuMS5bMzA6MzFdClt1bnVzdWFsXQpbc2VydmVyczp2YXJzXQphbnNpYmxlX3VzZXI9Y2Vud\ G9zCmFuc2libGVfc3NoX3ByaXZhdGVfa2V5X2ZpbGU9L2hvbWUvY2VwcmV1L2RlZmF1bHQucGVtCmFu\ c2libGVfYmVjb21lPXRydWU=" inventoryText = Base64.decode(inventoryText) - + inventory = pmInventories.parseFromText(inventoryText) - var etalon = { - "hosts":[], - "groups":{ - "servers":{ - "vars":{"ansible_user":"centos","ansible_ssh_private_key_file":"/home/cepreu/default.pem","ansible_become":"true"}, - "groups":["usual","unusual"], - "hosts":[], - "children":true, - "dataLevel":{"level":1,"parents":["all","servers"]} - }, - "usual":{ - "vars":{}, - "groups":[], - "hosts":[ - {"name":"172.16.1.[30:31]","type":"RANGE","vars":{}} - ], - "dataLevel":{"level":2,"parents":["all","servers","usual"]} - }, - "unusual":{ - "vars":{}, - "groups":[], - "hosts":[], - "dataLevel":{"level":2,"parents":["all","servers","unusual"]} - } - }, - "vars":{}, - "name":"inventory" - } - + var etalon = {"hosts":[],"groups":{"servers":{"notes":"","vars":{"ansible_user":"centos","ansible_ssh_private_key_file":"/home/cepreu/default.pem","ansible_become":"true"},"groups":["usual","unusual"],"hosts":[],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"children":true,"dataLevel":{"level":1,"parents":["all","servers"]}},"usual":{"notes":"","vars":{},"groups":[],"hosts":[{"name":"172.16.1.[30:31]","notes":"","type":"RANGE","all_only":false,"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"vars":{}}],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"dataLevel":{"level":2,"parents":["all","servers","usual"]}},"unusual":{"notes":"","vars":{},"groups":[],"hosts":[],"matches":false,"match_id_arr":[],"match_arr":[],"extern":false,"dataLevel":{"level":2,"parents":["all","servers","unusual"]}}},"vars":{},"name":"inventory","notes":""} inventory.name = "inventory" - + var res = deepEqual(etalon, inventory) assert.ok(res, 'Сравнение инвентория 2 распарсенного и оригинального'); render(done) - }); + }); }}) /** @@ -1945,20 +1916,82 @@ window.qunitTestsArray.push({ assert.ok(true, 'Успешно update add Item'); render(done) }).fail(function() - { + { + debugger; + assert.ok(false, 'Ошибка при update add Item'); + render(done) + }) + }); + + syncQUnit.addTest('Изменение проекта repo_sync_on_run true', function ( assert ) + { + var done = assert.async(); + + // Предполагается что мы от прошлого теста попали на страницу редактирования project + // с адресом http://192.168.0.12:8080/?group-5 + + $("#filed_repo_sync_on_run").addClass("selected"); + + + $.when(pmProjects.updateItem(project_id)).done(function() + { + if(pmProjects.model.items[project_id].vars.repo_sync_on_run == true) + { + assert.ok(true, 'Успешно update add Item'); + render(done) + } + else + { + debugger; + assert.ok(false, 'Ошибка при update add Item'); + render(done) + } + }).fail(function() + { + debugger; + assert.ok(false, 'Ошибка при update add Item'); + render(done) + }) + }); + + syncQUnit.addTest('Изменение проекта repo_sync_on_run false', function ( assert ) + { + var done = assert.async(); + + // Предполагается что мы от прошлого теста попали на страницу редактирования project + // с адресом http://192.168.0.12:8080/?group-5 + + $("#filed_repo_sync_on_run").removeClass("selected"); + + + $.when(pmProjects.updateItem(project_id)).done(function() + { + if(pmProjects.model.items[project_id].vars.repo_sync_on_run === undefined) + { + assert.ok(true, 'Успешно update add Item'); + render(done) + } + else + { + debugger; + assert.ok(false, 'Ошибка при update add Item'); + render(done) + } + }).fail(function() + { debugger; assert.ok(false, 'Ошибка при update add Item'); render(done) }) }); - + syncQUnit.addTest('pmProjects.syncRepo', function ( assert ) { var done = assert.async(); // Предполагается что мы от прошлого теста попали на страницу редактирования project // с адресом http://192.168.0.12:8080/?group-5 - var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] $.when(pmProjects.syncRepo(itemId)).done(function() { assert.ok(true, 'Успешно syncRepo'); @@ -1970,16 +2003,16 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('pmProjects.executePlaybook', function ( assert ) - { + { var done = assert.async(); $('#inventories-autocomplete').val('') $('#playbook-autocomplete').val('') - + // Предполагается что мы от прошлого теста попали на страницу редактирования project // с адресом http://192.168.0.12:8080/?group-5 - var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] $.when(pmProjects.executePlaybook(itemId)).done(function() { debugger; @@ -1990,7 +2023,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Страница Run playbook', function ( assert ) { var done = assert.async(); @@ -2008,12 +2041,12 @@ window.qunitTestsArray.push({ }) syncQUnit.addTest('run playbooke с ошибкой 1', function ( assert ) - { + { // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - - var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] - + + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] + // Отправка формы с данными project $.when(pmTasks.execute(itemId, 99999, "main.yml")).done(function() { @@ -2024,20 +2057,20 @@ window.qunitTestsArray.push({ { assert.ok(true, 'Ошибка Execute ansible module, как и задумано'); render(done) - }) + }) }); syncQUnit.addTest('run playbook с ошибкой 2', function ( assert ) - { + { // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - - jsonEditor.__devAddVar("test1", "test1", "playbook") - - var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] - var inventoryId = $("#inventories-autocomplete option")[1].value // Надеемся что там есть хоть один инвенторий + + jsonEditor.__devAddVar("test1", "test1", "playbook") + + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] + var inventoryId = $("#inventories-autocomplete option")[1].value // Надеемся что там есть хоть один инвенторий $('#inventories-autocomplete').val(inventoryId) - + // Отправка формы с данными project $.when(pmTasks.execute(itemId, $('#inventories-autocomplete').val(), "main.yml")).done(function() { @@ -2053,15 +2086,15 @@ window.qunitTestsArray.push({ }); syncQUnit.addTest('run playbook', function ( assert ) - { + { // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - + jsonEditor.jsonEditorRmVar("test1") - - var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] + + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] var inventoryId = $("#inventories-autocomplete option")[1].value // Надеемся что там есть хоть один инвенторий - + $('#inventories-autocomplete').val(inventoryId) // Отправка формы с данными project $.when(pmTasks.execute(itemId, $('#inventories-autocomplete').val(), "main.yml")).done(function() @@ -2073,9 +2106,9 @@ window.qunitTestsArray.push({ debugger; assert.ok(false, 'Ошибка Execute ansible module, а не должно было'); render(done) - }) + }) }); - + var projectId = undefined syncQUnit.addTest('Страница Execute ansible module', function ( assert ) { @@ -2101,13 +2134,13 @@ window.qunitTestsArray.push({ // Заполнение формы с данными project $("#module-autocomplete").val("test"); jsonEditor.jsonEditorRmVar("test1") - + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] - + var inventoryId = $("#inventories-autocomplete option")[1].value // Надеемся что там есть хоть один инвенторий $.when(pmModuleTemplates.selectInventory(inventoryId)).done(function() - { + { pmAnsibleModule.selectInventory(inventoryId) // Отправка формы с данными project $.when(pmAnsibleModule.execute(itemId, 99999, "All", $('#module-autocomplete').val())).done(function() @@ -2134,16 +2167,16 @@ window.qunitTestsArray.push({ // Заполнение формы с данными project $("#module-autocomplete").val("test"); - - jsonEditor.__devAddVar("test1", "test1", "playbook") - + + jsonEditor.__devAddVar("test1", "test1", "playbook") + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] - + var inventoryId = $("#inventories-autocomplete option")[1].value // Надеемся что там есть хоть один инвенторий $.when(pmModuleTemplates.selectInventory(inventoryId)).done(function() - { + { $('#inventories-autocomplete').val(inventoryId) // Отправка формы с данными project $.when(pmAnsibleModule.execute(itemId, $('#inventories-autocomplete').val(), "All", $('#module-autocomplete').val())).done(function() @@ -2171,13 +2204,13 @@ window.qunitTestsArray.push({ // Заполнение формы с данными project $("#module-autocomplete").val("test"); jsonEditor.jsonEditorRmVar("test1") - + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] - + var inventoryId = $("#inventories-autocomplete option")[1].value // Надеемся что там есть хоть один инвенторий $.when(pmModuleTemplates.selectInventory(inventoryId)).done(function() - { + { $('#inventories-autocomplete').val(inventoryId) // Отправка формы с данными project $.when(pmAnsibleModule.execute(itemId, $('#inventories-autocomplete').val(), "All", $('#module-autocomplete').val())).done(function() @@ -2196,10 +2229,10 @@ window.qunitTestsArray.push({ assert.ok(false, 'Ошибка при selectInventory'); }) }); - + syncQUnit.addTest('Страница periodic-tasks', function ( assert ) - { - var done = assert.async(); + { + var done = assert.async(); $.when(spajs.open({ menuId:'project/'+projectId+'/periodic-tasks'})).done(function() { assert.ok(true, 'Страница открылась'); @@ -2211,10 +2244,10 @@ window.qunitTestsArray.push({ render(done) }) }) - + syncQUnit.addTest('Страница periodic-tasks.toggleSelectEachItem', function ( assert ) - { - var done = assert.async(); + { + var done = assert.async(); $.when(pmPeriodicTasks.toggleSelectEachItem(true, projectId)).done(function() { assert.ok(true, 'ok:toggleSelectEachItem'); @@ -2226,10 +2259,10 @@ window.qunitTestsArray.push({ render(done) }) }) - + syncQUnit.addTest('Страница periodic-tasks.search', function ( assert ) - { - var done = assert.async(); + { + var done = assert.async(); $.when(pmPeriodicTasks.search("test", {project_id:projectId})).done(function() { assert.ok(true, 'ok:periodic-tasks.search'); @@ -2241,9 +2274,9 @@ window.qunitTestsArray.push({ render(done) }) }) - + // pmPeriodicTasks.showSearchResults - + /* syncQUnit.addTest('Страница нового inventory для проекта', function ( assert ) { @@ -2323,8 +2356,8 @@ window.qunitTestsArray.push({ // Заполнение формы с данными inventory $("#filed_name").val("test-inventory-"+t); - jsonEditor.__devAddVar("test1", "test1", "inventory") - jsonEditor.__devAddVar("test2", "test2", "inventory") + jsonEditor.__devAddVar("test1", "test1", "inventory") + jsonEditor.__devAddVar("test2", "test2", "inventory") // Отправка формы с данными inventory @@ -2359,7 +2392,7 @@ window.qunitTestsArray.push({ syncQUnit.addTest('Создание не валидного periodic task', function ( assert ) - { + { // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); @@ -2368,9 +2401,9 @@ window.qunitTestsArray.push({ $("#new_periodic-tasks_playbook").val("test-project-"+t); $("#new_periodic-tasks_schedule_INTERVAL").val(t); - - jsonEditor.__devAddVar("test1", "test1", "periodic_playbook", 'PLAYBOOK') - + + jsonEditor.__devAddVar("test1", "test1", "periodic_playbook", 'PLAYBOOK') + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] var inventoryId = $("#inventories-autocomplete option")[0].value @@ -2398,12 +2431,12 @@ window.qunitTestsArray.push({ }); syncQUnit.addTest('Создание periodic task', function ( assert ) - { + { // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); jsonEditor.jsonEditorRmVar('test1', 'PLAYBOOK') - + // Заполнение формы с данными project $("#new_periodic-tasks_name").val("test-project-"+t); $("#new_periodic-tasks_playbook").val("test-project-"+t); @@ -2412,12 +2445,12 @@ window.qunitTestsArray.push({ $("#new_json_namePLAYBOOK").val("become-method"); $("#new_json_valuePLAYBOOK").val("val1"); - jsonEditor.jsonEditorAddVar('periodic_playbook', "PLAYBOOK"); - + jsonEditor.jsonEditorAddVar('periodic_playbook', "PLAYBOOK"); + var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] var inventoryId = $("#inventories-autocomplete option")[0].value - + $.when(pmPeriodicTasks.selectInventory(inventoryId)).done(function() { $("#inventories-autocomplete").val(inventoryId) @@ -2439,9 +2472,9 @@ window.qunitTestsArray.push({ assert.ok(false, 'Ошибка при selectInventory'); }) }); - + syncQUnit.addTest('Изменение не валидного periodic task', function ( assert ) - { + { var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] var taskId = /periodic-task\/([0-9]+)/.exec(window.location.href)[1] // Предполагается что мы от прошлого теста попали на страницу создания inventory @@ -2449,9 +2482,9 @@ window.qunitTestsArray.push({ // Заполнение формы с данными inventory $("#periodic-tasks_"+taskId+"_name").val("test-task2-"+t); - - jsonEditor.__devAddVar("test1", "test1", "periodic_playbook", "PLAYBOOK") - + + jsonEditor.__devAddVar("test1", "test1", "periodic_playbook", "PLAYBOOK") + // Отправка формы с данными inventory $.when(pmPeriodicTasks.updateItem(taskId, {project_id:itemId})).done(function() { @@ -2466,7 +2499,7 @@ window.qunitTestsArray.push({ }); syncQUnit.addTest('Изменение periodic task', function ( assert ) - { + { var itemId = /project\/([0-9]+)/.exec(window.location.href)[1] var taskId = /periodic-task\/([0-9]+)/.exec(window.location.href)[1] // Предполагается что мы от прошлого теста попали на страницу создания inventory @@ -2474,13 +2507,13 @@ window.qunitTestsArray.push({ // Заполнение формы с данными inventory $("#periodic-tasks_"+taskId+"_name").val("test-task2-"+t); - + jsonEditor.jsonEditorRmVar('test1', 'PLAYBOOK') - + $("#new_json_namePLAYBOOK").val("private-key"); $("#new_json_valuePLAYBOOK").val("PLAYBOOK"); jsonEditor.jsonEditorAddVar('periodic_playbook', 'PLAYBOOK'); - + // Отправка формы с данными inventory $.when(pmPeriodicTasks.updateItem(taskId, {project_id:itemId})).done(function() { @@ -2534,11 +2567,11 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('execute для Periodic Task', function ( assert ) { var done = assert.async(); - + // Удаление пользователя. $.when(pmPeriodicTasks.execute(projectId, taskId)).done(function() { @@ -2600,7 +2633,7 @@ window.qunitTestsArray.push({ render(done) }) }) - + syncQUnit.addTest('Удаление проекта', function ( assert ) { var done = assert.async(); @@ -2673,12 +2706,12 @@ window.qunitTestsArray.push({ // Заполнение формы с данными project $("#Templates-name").val("!2 d#"); - - jsonEditor.__devAddVar("syntax-check32", "syntax-check32") - + + jsonEditor.__devAddVar("syntax-check32", "syntax-check32") + jsonEditor.jsonEditorImportVars("playbook", "prefix", "syntax-check=\n") jsonEditor.jsonEditorImportVars("playbook", "prefix", "syntax-check:\n") - + // Отправка формы с данными project $.when(pmTasksTemplates.addItem()).done(function() { @@ -2693,7 +2726,7 @@ window.qunitTestsArray.push({ }); syncQUnit.addTest('Сохранение шаблона задачи', function ( assert ) - { + { // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); @@ -2714,301 +2747,284 @@ window.qunitTestsArray.push({ }) }); - syncQUnit.addTest('Изменение не валидного шаблона', function ( assert ) + //тестируем опции шаблона + var itemIdForOptionTest=undefined; + syncQUnit.addTest('Открытие страницы создания новой опции шаблона задачи', function ( assert ) { + // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); + itemIdForOptionTest = /template\/Task\/([0-9]+)/.exec(window.location.href)[1]; - // Предполагается что мы от прошлого теста попали на страницу редактирования project - // с адресом http://192.168.0.12:8080/?group-5 - var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1] - - $("#playbook-autocomplete").val("test2-playbook-"+t); - - jsonEditor.__devAddVar("syntax-check22", "syntax-check22") - - $.when(pmTasksTemplates.updateItem(itemId)).done(function() + $.when(spajs.open({ menuId:"template/Task/"+itemIdForOptionTest+"/new-option"})).done(function() { - debugger; - assert.ok(false, 'Успешно template update Item, а не должно было'); + assert.ok(true, 'Успешно открыто меню templates/Task/item_id/new-option'); render(done) }).fail(function() { - assert.ok(true, 'Ошибка при template update Item, как и задумано'); + debugger; + assert.ok(false, 'Ошибка при открытиии меню templates/Task/item_id/new-option'); render(done) }) }); - syncQUnit.addTest('Изменение шаблона', function ( assert ) + syncQUnit.addTest('Сохранение новой невалидной опции шаблона задачи', function ( assert ) { var done = assert.async(); - // Предполагается что мы от прошлого теста попали на страницу редактирования project - // с адресом http://192.168.0.12:8080/?group-5 - var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1] - $("#playbook-autocomplete").val("test2-playbook-"+t); - $("#projects-autocomplete").val($("#projects-autocomplete option")[0].value).trigger('change.select2'); - $("#inventories-autocomplete").val($("#inventories-autocomplete option")[0].value).trigger('change.select2'); - - jsonEditor.jsonEditorRmVar("syntax-check22"); - $("#new_json_nameprefix").val("new-vault-password-file"); - $("#new_json_valueprefix").val("syntax-check"); - jsonEditor.jsonEditorAddVar(); - - - $.when(pmTasksTemplates.updateItem(itemId)).done(function() + //пытаемся сохранить пустую опцию, даже не забав имя + $.when(pmTasksTemplates.saveNewOption(itemIdForOptionTest)).done(function() { - assert.ok(true, 'Успешно update add Item'); - render(done) - }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при update add Item'); + assert.ok(false, 'Успешно сохранено, а не должно было'); + render(done) + }).fail(function(){ + assert.ok(true, 'Ошибка при сохранении, как и задумано'); render(done) }) }); - - var itemId = undefined - syncQUnit.addTest('Копирование template Task', function ( assert ) - { - // Предполагается что мы от прошлого теста попали на страницу редактирования группы - // с адресом http://192.168.0.12:8080/?group-5 - itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1] + syncQUnit.addTest('Сохранение новой невалидной опции шаблона задачи 2', function ( assert ) + { + // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - $.when(pmTasksTemplates.copyAndEdit(itemId)).done(function() + + $("#filed_option_name").val("test-option"); + + //пытаемся сохранить пустую опцию, с именем, но идентичную шаблону + $.when(pmTasksTemplates.saveOption(itemIdForOptionTest)).done(function() { - assert.ok(true, 'Успешно copyAndEdit add Item'); + debugger; + assert.ok(false, 'Успешно сохранено, а не должно было'); render(done) }).fail(function(){ - debugger; - assert.ok(false, 'Ошибка при copyAndEdit add Item'); + assert.ok(true, 'Ошибка при сохранении, как и задумано'); render(done) }) }); - syncQUnit.addTest('Удаление копии template Task', function ( assert ) + syncQUnit.addTest('Сохранение новой валидной опции шаблона задачи', function ( assert ) { + // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - // Предполагается что мы от прошлого теста попали на страницу редактирования пользователя - // с адресом http://192.168.0.12:8080/?user-5 - var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1] + jsonEditor.jsonEditorRmVar("become2") + $("#new_json_nameprefix").val("become"); + jsonEditor.jsonEditorAddVar(); - // Удаление пользователя. - $.when(pmTasksTemplates.deleteItem(itemId, true)).done(function() + $.when(pmTasksTemplates.saveNewOption(itemIdForOptionTest)).done(function() { - assert.ok(true, 'Успешно delete add Item'); + assert.ok(true, 'Опция успешно сохранена'); render(done) }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при delete add Item'); + assert.ok(false, 'Ошибка при сохранении опции'); render(done) }) }); - syncQUnit.addTest('Удаление шаблона', function ( assert ) - { - var done = assert.async(); - - $.when(spajs.open({ menuId:"template/Task/"+itemId})).done(function() - { - $.when(pmTasksTemplates.saveAndExecute(itemId)).done(function() - { - assert.ok(true, 'Успешно pmTasksTemplates.saveAndExecute'); - render(done) - }).fail(function(){ - debugger; - assert.ok(false, 'Ошибка при pmTasksTemplates.saveAndExecute'); - render(done) - }) - }).fail(function() - { - debugger; - assert.ok(false, 'Ошибка при открытиии меню template/Module/'+itemId); - render(done) - }) - }); - - syncQUnit.addTest('Удаление шаблона', function ( assert ) + syncQUnit.addTest('Изменение опции шаблона задачи', function ( assert ) { + // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - // Удаление project. - $.when(pmTasksTemplates.deleteItem(itemId, true)).done(function() + jsonEditor.jsonEditorRmVar("check2") + $("#new_json_nameprefix").val("check"); + jsonEditor.jsonEditorAddVar(); + + $.when(pmTasksTemplates.saveOption(itemIdForOptionTest)).done(function() { - assert.ok(true, 'Успешно delete Item'); + assert.ok(true, 'Опция успешно изменена'); render(done) }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при delete Item'); + assert.ok(false, 'Ошибка при изменении опции'); render(done) }) }); - syncQUnit.addTest('Список шаблонов', function ( assert ) + syncQUnit.addTest('Сохранение и запуск опции шаблона задачи', function ( assert ) { + // Предполагается что мы от прошлого теста попали на страницу просмотра/редактирования опции шаблона var done = assert.async(); - $.when(spajs.open({ menuId:"templates"})).done(function() + $.when(pmTasksTemplates.saveAndExecuteOption(itemIdForOptionTest)).done(function() { - assert.ok(true, 'Успешно открыто меню templates'); + assert.ok(true, 'Опция успешно сохранена'); render(done) - }).fail(function() - { + }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при открытиии меню templates'); + assert.ok(false, 'Ошибка при сохранении опции'); render(done) }) }); - syncQUnit.addTest('Новый template/new-task', function ( assert ) + //открываем стрницу для создания новой опции для дальнейших тестов + syncQUnit.addTest('Открытие страницы создания новой опции шаблона задачи', function ( assert ) { var done = assert.async(); - - // Открытие пункта меню new-project - $.when(spajs.open({ menuId:"template/new-task"})).done(function() + $.when(spajs.open({ menuId:"template/Task/"+itemIdForOptionTest+"/new-option"})).done(function() { - assert.ok(true, 'Успешно открыто меню new-project'); + assert.ok(true, 'Успешно открыто меню templates/Task/item_id/new-option'); render(done) }).fail(function() { debugger; - assert.ok(false, 'Ошибка при открытиии меню new-project'); + assert.ok(false, 'Ошибка при открытиии меню templates/Task/item_id/new-option'); render(done) }) }); - var t = new Date(); - t = t.getTime() - - syncQUnit.addTest('Сохранение не валидного шаблона задачи', function ( assert ) + syncQUnit.addTest('Сохранение новой опции с уже существующим именем', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - // Заполнение формы с данными project - $("#Templates-name").val("!2 d#"); - - jsonEditor.__devAddVar("syntax-check32", "syntax-check32") + $("#filed_option_name").val("test-option"); + jsonEditor.jsonEditorRmVar("become2") + $("#new_json_nameprefix").val("become"); + jsonEditor.jsonEditorAddVar(); - jsonEditor.jsonEditorImportVars("playbook", "prefix", "syntax-check=\n") - jsonEditor.jsonEditorImportVars("playbook", "prefix", "syntax-check:\n") - // Отправка формы с данными project - $.when(pmTasksTemplates.addItem()).done(function() + $.when(pmTasksTemplates.saveNewOption(itemIdForOptionTest)).done(function() { debugger; - assert.ok(false, 'Успешно template add Item, а не должно было'); + assert.ok(false, 'Успешно сохранено, а не должно было'); render(done) - }).fail(function() - { - assert.ok(true, 'Ошибка при template add Item, как и задумано'); + }).fail(function(){ + assert.ok(true, 'Ошибка при сохранении, как и задумано'); render(done) }) }); - syncQUnit.addTest('Сохранение шаблона задачи', function ( assert ) + syncQUnit.addTest('Сохранение новой опции с новым именем', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - // Заполнение формы с данными project - $("#Templates-name").val("test-template-"+t); - jsonEditor.jsonEditorRmVar("syntax-check32"); + $("#filed_option_name").val("test-option2"); - // Отправка формы с данными project - $.when(pmTasksTemplates.addItem()).done(function() + $.when(pmTasksTemplates.saveNewOption(itemIdForOptionTest)).done(function() { - assert.ok(true, 'Успешно template add Item'); + assert.ok(true, 'Успешно сохранено'); render(done) - }).fail(function() - { + }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при template add Item'); + assert.ok(false, 'Ошибка при сохранении'); render(done) }) }); - syncQUnit.addTest('Открытие страницы создания новой опции шаблона задачи', function ( assert ) + syncQUnit.addTest('Удаление опции', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1]; - $.when(spajs.open({ menuId:"template/Task/"+itemId+"/new-option"})).done(function() + $.when(pmTasksTemplates.removeOption(itemIdForOptionTest)).done(function() { - assert.ok(true, 'Успешно открыто меню templates/Task/item_id/new-option'); + assert.ok(true, 'Опция успешно удалена'); render(done) - }).fail(function() - { + }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при открытиии меню templates/Task/item_id/new-option'); + assert.ok(false, 'Ошибка при удалении опции'); render(done) }) }); - syncQUnit.addTest('Сохранение новой невалидной опции шаблона задачи', function ( assert ) + //конец тестирования опции шаблона + + syncQUnit.addTest('Изменение не валидного шаблона', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - var itemId = /template\/Task\/([0-9]+)\/new-option/.exec(window.location.href)[1]; - $("#filed_option_name").val("test-option"); + // Предполагается что мы от прошлого теста попали на страницу редактирования project + // с адресом http://192.168.0.12:8080/?group-5 + var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1] + + $("#playbook-autocomplete").val("test2-playbook-"+t); + + jsonEditor.__devAddVar("syntax-check22", "syntax-check22") - $.when(pmTasksTemplates.saveOption(itemId)).done(function() + $.when(pmTasksTemplates.updateItem(itemId)).done(function() { debugger; - assert.ok(false, 'Успешно сохранено, а не должно было'); + assert.ok(false, 'Успешно template update Item, а не должно было'); render(done) - }).fail(function(){ - assert.ok(true, 'Ошибка при сохранении, как и задумано'); + }).fail(function() + { + assert.ok(true, 'Ошибка при template update Item, как и задумано'); render(done) }) - - }); - syncQUnit.addTest('Сохранение новой валидной опции шаблона задачи', function ( assert ) + syncQUnit.addTest('Изменение шаблона', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - var itemId = /template\/Task\/([0-9]+)\/new-option/.exec(window.location.href)[1]; - jsonEditor.jsonEditorRmVar("become2") - $("#new_json_nameprefix").val("become"); + // Предполагается что мы от прошлого теста попали на страницу редактирования project + // с адресом http://192.168.0.12:8080/?group-5 + var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1] + $("#playbook-autocomplete").val("test2-playbook-"+t); + $("#projects-autocomplete").val($("#projects-autocomplete option")[0].value).trigger('change.select2'); + $("#inventories-autocomplete").val($("#inventories-autocomplete option")[0].value).trigger('change.select2'); + + jsonEditor.jsonEditorRmVar("syntax-check22"); + $("#new_json_nameprefix").val("new-vault-password-file"); + $("#new_json_valueprefix").val("syntax-check"); jsonEditor.jsonEditorAddVar(); - $.when(pmTasksTemplates.saveOption(itemId)).done(function() + + $.when(pmTasksTemplates.updateItem(itemId)).done(function() { - assert.ok(true, 'Опция успешно сохранена'); + assert.ok(true, 'Успешно update add Item'); render(done) }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при сохранении опции'); + assert.ok(false, 'Ошибка при update add Item'); render(done) }) }); - syncQUnit.addTest('Сохранение и запуск опции шаблона задачи', function ( assert ) + var itemId = undefined + syncQUnit.addTest('Копирование template Task', function ( assert ) + { + // Предполагается что мы от прошлого теста попали на страницу редактирования группы + // с адресом http://192.168.0.12:8080/?group-5 + itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1] + + var done = assert.async(); + $.when(pmTasksTemplates.copyAndEdit(itemId)).done(function() + { + assert.ok(true, 'Успешно copyAndEdit add Item'); + render(done) + }).fail(function(){ + debugger; + assert.ok(false, 'Ошибка при copyAndEdit add Item'); + render(done) + }) + }); + + syncQUnit.addTest('Удаление копии template Task', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу просмотра/редактирования опции шаблона var done = assert.async(); - var itemId = /template\/Task\/([0-9]+)\/option\/([A-z0-9 %\-.:,=]+)/.exec(window.location.href)[1]; - $.when(pmTasksTemplates.saveAndExecuteOption(itemId)).done(function() + // Предполагается что мы от прошлого теста попали на страницу редактирования пользователя + // с адресом http://192.168.0.12:8080/?user-5 + var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1] + + // Удаление пользователя. + $.when(pmTasksTemplates.deleteItem(itemId, true)).done(function() { - assert.ok(true, 'Опция успешно сохранена'); + assert.ok(true, 'Успешно delete add Item'); render(done) }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при сохранении опции'); + assert.ok(false, 'Ошибка при delete add Item'); render(done) }) }); - syncQUnit.addTest('Удаление шаблона', function ( assert ) + + syncQUnit.addTest('Сохранение и запуск шаблона', function ( assert ) { var done = assert.async(); - var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1]; + $.when(spajs.open({ menuId:"template/Task/"+itemId})).done(function() { $.when(pmTasksTemplates.saveAndExecute(itemId)).done(function() @@ -3028,10 +3044,43 @@ window.qunitTestsArray.push({ }) }); + var tt_history_id = undefined; + syncQUnit.addTest('Страница списка task template History', function ( assert ) + { + + tt_history_id=/history\/([0-9]+)/.exec(window.location.href)[1]; + var done = assert.async(); + $.when(spajs.open({ menuId:'template/Task/'+itemId+'/history'})).done(function() + { + assert.ok(true, 'Страница открылась'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Страница не открылась'); + render(done) + }) + }) + + syncQUnit.addTest('Страница task template History Item', function ( assert ) + { + var done = assert.async(); + $.when(spajs.open({ menuId:'template/Task/'+itemId+'/history/'+tt_history_id})).done(function() + { + assert.ok(true, 'Страница открылась'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Страница не открылась'); + render(done) + }) + }) + syncQUnit.addTest('Удаление шаблона', function ( assert ) { var done = assert.async(); - var itemId = /template\/Task\/([0-9]+)/.exec(window.location.href)[1]; + // Удаление project. $.when(pmTasksTemplates.deleteItem(itemId, true)).done(function() { @@ -3095,9 +3144,9 @@ window.qunitTestsArray.push({ // Заполнение формы с данными project $("#Templates-name").val("test-template-"+t); - - jsonEditor.__devAddVar("new-vault-password-file2", "syntax-check") - + + jsonEditor.__devAddVar("new-vault-password-file2", "syntax-check") + // Отправка формы с данными project $.when(pmModuleTemplates.addItem()).done(function() { @@ -3110,9 +3159,9 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Сохранение шаблона модуля', function ( assert ) - { + { // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); @@ -3120,10 +3169,10 @@ window.qunitTestsArray.push({ $("#new_json_nameprefix").val("new-vault-password-file"); $("#new_json_valueprefix").val("syntax-check"); jsonEditor.jsonEditorAddVar(); - + $("#inventories-autocomplete").val($("#inventories-autocomplete option")[0].value).trigger('change.select2'); - - + + // Заполнение формы с данными project $("#Templates-name").val("test-template-"+t); @@ -3140,330 +3189,307 @@ window.qunitTestsArray.push({ }) }); - syncQUnit.addTest('Изменение не валидного шаблона модуля', function ( assert ) - { + //начало тестирования опций шаблона + var itemIdForOptionTest1=undefined; + syncQUnit.addTest('Открытие страницы создания новой опции шаблона модуля', function ( assert ) + { + // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); + itemIdForOptionTest1 = /template\/Module\/([0-9]+)/.exec(window.location.href)[1]; - // Предполагается что мы от прошлого теста попали на страницу редактирования project - // с адресом http://192.168.0.12:8080/?group-5 - var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] - - $("#module-autocomplete").val("test2-playbook-"+t); - jsonEditor.__devAddVar("new-vault-password-file2", "syntax-check") - - - $.when(pmModuleTemplates.updateItem(itemId)).done(function() + $.when(spajs.open({ menuId:"template/Module/"+itemIdForOptionTest1+"/new-option"})).done(function() + { + assert.ok(true, 'Успешно открыто меню templates/Module/item_id/new-option'); + render(done) + }).fail(function() { debugger; - assert.ok(false, 'Успешно update add Item, а не должно было'); - render(done) - }).fail(function(){ - assert.ok(true, 'Ошибка при update add Item, как и задумано'); + assert.ok(false, 'Ошибка при открытиии меню templates/Module/item_id/new-option'); render(done) }) }); - syncQUnit.addTest('Изменение шаблона модуля', function ( assert ) + syncQUnit.addTest('Сохранение новой невалидной опции шаблона модуля', function ( assert ) { var done = assert.async(); - - var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] - - jsonEditor.jsonEditorRmVar("new-vault-password-file2") - $("#module-autocomplete").val("test2-playbook-"+t); - $("#new_json_nameprefix").val("new-vault-password-file"); - $("#new_json_valueprefix").val("syntax-check"); - jsonEditor.jsonEditorAddVar(); - - - $.when(pmModuleTemplates.updateItem(itemId)).done(function() + //пытаемся сохранить пустую опцию, даже не забав имя + $.when(pmModuleTemplates.saveNewOption(itemIdForOptionTest1)).done(function() { - assert.ok(true, 'Успешно update add Item'); + assert.ok(false, 'Опция успешно сохранена, а не должна была'); + debugger; render(done) }).fail(function(){ - debugger; - assert.ok(false, 'Ошибка при update add Item'); + assert.ok(true, 'Ошибка при сохранении опции, что и ожидалось'); render(done) }) }); - var itemId = undefined - syncQUnit.addTest('Копирование template Module', function ( assert ) - { - itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] - + syncQUnit.addTest('Сохранение новой невалидной опции шаблона модуля 2', function ( assert ) + { + // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - $.when(pmModuleTemplates.copyAndEdit(itemId)).done(function() + + $("#filed_option_name").val("test-option"); + + //пытаемся сохранить пустую опцию, с именем, но идентичную шаблону + $.when(pmModuleTemplates.saveOption(itemIdForOptionTest1)).done(function() { - assert.ok(true, 'Успешно copyAndEdit add Item'); + debugger; + assert.ok(false, 'Успешно сохранено, а не должно было'); render(done) }).fail(function(){ - debugger; - assert.ok(false, 'Ошибка при copyAndEdit add Item'); + assert.ok(true, 'Ошибка при сохранении, как и задумано'); render(done) }) }); -/* - syncQUnit.addTest('Выполнение template Module', function ( assert ) - { - var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] - debugger; + + syncQUnit.addTest('Сохранение новой валидной опции шаблона модуля', function ( assert ) + { + // Предполагается что мы от прошлого теста попали на страницу опции var done = assert.async(); - $.when(pmModuleTemplates.execute(itemId)).done(function() + + jsonEditor.jsonEditorRmVar('new-vault-password-file', 'prefix'); + jsonEditor.jsonEditorRmVar("become2") + $("#new_json_nameprefix").val("become"); + jsonEditor.jsonEditorAddVar(); + + $.when(pmModuleTemplates.saveNewOption(itemIdForOptionTest1)).done(function() { - assert.ok(true, 'Успешно pmModuleTemplates.execute'); + assert.ok(true, 'Опция успешно сохранена'); render(done) }).fail(function(){ - assert.ok(false, 'Ошибка при pmModuleTemplates.execute'); + debugger; + assert.ok(false, 'Ошибка при сохранении опции'); render(done) }) - });*/ - - syncQUnit.addTest('pmTemplates.exportToFile', function ( assert ) + }); + + syncQUnit.addTest('Изменение опции шаблона модуля', function ( assert ) { + // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - $.when(pmTemplates.exportToFile([itemId])).done(function() + jsonEditor.jsonEditorRmVar("check2") + $("#new_json_nameprefix").val("check"); + jsonEditor.jsonEditorAddVar(); + + $.when(pmModuleTemplates.saveOption(itemIdForOptionTest1)).done(function() { - assert.ok(true, 'pmTemplates.exportToFile ok'); + assert.ok(true, 'Опция успешно изменена'); render(done) - }).fail(function() - { + }).fail(function(){ debugger; - assert.ok(false, 'pmTemplates.exportToFile error'); + assert.ok(false, 'Ошибка при изменении опции'); render(done) }) }); - syncQUnit.addTest('Удаление копии template Module', function ( assert ) + syncQUnit.addTest('Сохранение и запуск опции шаблона модуля', function ( assert ) { + // Предполагается что мы от прошлого теста попали на страницу просмотра/редактирования опции шаблона var done = assert.async(); - // Предполагается что мы от прошлого теста попали на страницу редактирования пользователя - // с адресом http://192.168.0.12:8080/?user-5 - var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] - - // Удаление пользователя. - $.when(pmModuleTemplates.deleteItem(itemId, true)).done(function() + $.when(pmModuleTemplates.saveAndExecuteOption(itemIdForOptionTest1)).done(function() { - assert.ok(true, 'Успешно delete add Item'); + assert.ok(true, 'Опция успешно сохранена'); render(done) }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при delete add Item'); + assert.ok(false, 'Ошибка при сохранении опции'); render(done) }) }); - syncQUnit.addTest('Удаление шаблона Module', function ( assert ) + //открываем страницу создания новой опции для дальнейших тестов + syncQUnit.addTest('Открытие страницы создания новой опции шаблона модуля', function ( assert ) { var done = assert.async(); - - $.when(spajs.open({ menuId:"template/Module/"+itemId})).done(function() - { - $.when(pmModuleTemplates.saveAndExecute(itemId)).done(function() - { - assert.ok(true, 'Успешно pmModuleTemplates.saveAndExecute'); - render(done) - }).fail(function(){ - debugger; - assert.ok(false, 'Ошибка при pmModuleTemplates.saveAndExecute'); - render(done) - }) + + $.when(spajs.open({ menuId:"template/Module/"+itemIdForOptionTest1+"/new-option"})).done(function() + { + assert.ok(true, 'Успешно открыто меню templates/Module/item_id/new-option'); + render(done) }).fail(function() { debugger; - assert.ok(false, 'Ошибка при открытиии меню template/Module/'+itemId); + assert.ok(false, 'Ошибка при открытиии меню templates/Module/item_id/new-option'); render(done) - }) + }) }); - - syncQUnit.addTest('Удаление шаблона', function ( assert ) + + syncQUnit.addTest('Сохранение опции шаблона модуля с уже существующем именем', function ( assert ) { var done = assert.async(); - // Удаление project. - $.when(pmModuleTemplates.deleteItem(itemId, true)).done(function() + jsonEditor.jsonEditorRmVar("become2") + $("#new_json_nameprefix").val("become"); + jsonEditor.jsonEditorAddVar(); + $("#filed_option_name").val("test-option"); + + $.when(pmModuleTemplates.saveNewOption(itemIdForOptionTest1)).done(function() { - assert.ok(true, 'Успешно delete Item'); + assert.ok(false, 'Опция успешно сохранена, а не должна была'); + debugger; render(done) }).fail(function(){ - debugger; - assert.ok(false, 'Ошибка при delete Item'); + assert.ok(true, 'Ошибка при сохранении опции, что и ожидалось'); render(done) }) }); - syncQUnit.addTest('Список шаблонов', function ( assert ) + syncQUnit.addTest('Сохранение опции шаблона модуля с новым именем', function ( assert ) { var done = assert.async(); - $.when(spajs.open({ menuId:"templates"})).done(function() + $("#filed_option_name").val("test-option2"); + + $.when(pmModuleTemplates.saveNewOption(itemIdForOptionTest1)).done(function() { - assert.ok(true, 'Успешно открыто меню templates'); + assert.ok(true, 'Опция успешно сохранена'); render(done) - }).fail(function() - { + }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при открытиии меню templates'); + assert.ok(false, 'Ошибка при сохранении опции'); render(done) }) }); - syncQUnit.addTest('Новый template/new-module', function ( assert ) + syncQUnit.addTest('Удаление опции шаблона модуля', function ( assert ) { var done = assert.async(); - // Открытие пункта меню new-project - $.when(spajs.open({ menuId:"template/new-module"})).done(function() + $.when(pmModuleTemplates.removeOption(itemIdForOptionTest1)).done(function() { - assert.ok(true, 'Успешно открыто меню new-project'); + assert.ok(true, 'Опция успешно удалена'); render(done) - }).fail(function() - { + }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при открытиии меню new-project'); + assert.ok(false, 'Ошибка при удалении опции'); render(done) }) }); + //конец тестирования опции шаблона - var t = new Date(); - t = t.getTime() - - syncQUnit.addTest('Сохранение не валидного шаблона модуля', function ( assert ) + syncQUnit.addTest('Изменение не валидного шаблона модуля', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - // Заполнение формы с данными project - $("#Templates-name").val("test-template-"+t); + // Предполагается что мы от прошлого теста попали на страницу редактирования project + // с адресом http://192.168.0.12:8080/?group-5 + var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] + $("#module-autocomplete").val("test2-playbook-"+t); jsonEditor.__devAddVar("new-vault-password-file2", "syntax-check") - // Отправка формы с данными project - $.when(pmModuleTemplates.addItem()).done(function() + + $.when(pmModuleTemplates.updateItem(itemId)).done(function() { debugger; - assert.ok(false, 'Успешно template add Item, а не должно было'); + assert.ok(false, 'Успешно update add Item, а не должно было'); render(done) - }).fail(function() - { - assert.ok(true, 'Ошибка при template add Item, как и задумано'); + }).fail(function(){ + assert.ok(true, 'Ошибка при update add Item, как и задумано'); render(done) }) }); - syncQUnit.addTest('Сохранение шаблона модуля', function ( assert ) + syncQUnit.addTest('Изменение шаблона модуля', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); + var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] + jsonEditor.jsonEditorRmVar("new-vault-password-file2") + $("#module-autocomplete").val("test2-playbook-"+t); + $("#new_json_nameprefix").val("new-vault-password-file"); $("#new_json_valueprefix").val("syntax-check"); jsonEditor.jsonEditorAddVar(); - $("#inventories-autocomplete").val($("#inventories-autocomplete option")[0].value).trigger('change.select2'); - - - // Заполнение формы с данными project - $("#Templates-name").val("test-template-"+t); - - // Отправка формы с данными project - $.when(pmModuleTemplates.addItem()).done(function() + $.when(pmModuleTemplates.updateItem(itemId)).done(function() { - assert.ok(true, 'Успешно template add Item'); + assert.ok(true, 'Успешно update add Item'); render(done) - }).fail(function() - { + }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при template add Item'); + assert.ok(false, 'Ошибка при update add Item'); render(done) }) }); - syncQUnit.addTest('Открытие страницы создания новой опции шаблона модуля', function ( assert ) + var itemId = undefined + syncQUnit.addTest('Копирование template Module', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project - var done = assert.async(); - var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1]; + itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] - $.when(spajs.open({ menuId:"template/Module/"+itemId+"/new-option"})).done(function() + var done = assert.async(); + $.when(pmModuleTemplates.copyAndEdit(itemId)).done(function() { - assert.ok(true, 'Успешно открыто меню templates/Module/item_id/new-option'); + assert.ok(true, 'Успешно copyAndEdit add Item'); render(done) - }).fail(function() - { + }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при открытиии меню templates/Module/item_id/new-option'); + assert.ok(false, 'Ошибка при copyAndEdit add Item'); render(done) }) }); - - syncQUnit.addTest('Сохранение новой невалидной опции шаблона модуля', function ( assert ) +/* + syncQUnit.addTest('Выполнение template Module', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project + var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] + debugger; var done = assert.async(); - var itemId = /template\/Module\/([0-9]+)\/new-option/.exec(window.location.href)[1]; - - $("#filed_option_name").val("test-option"); - - $.when(pmModuleTemplates.saveOption(itemId)).done(function() + $.when(pmModuleTemplates.execute(itemId)).done(function() { - debugger; - assert.ok(false, 'Успешно сохранено, а не должно было'); + assert.ok(true, 'Успешно pmModuleTemplates.execute'); render(done) }).fail(function(){ - assert.ok(true, 'Ошибка при сохранении, как и задумано'); + assert.ok(false, 'Ошибка при pmModuleTemplates.execute'); render(done) }) + });*/ - - }); - - syncQUnit.addTest('Сохранение новой валидной опции шаблона модуля', function ( assert ) + syncQUnit.addTest('pmTemplates.exportToFile', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу создания project var done = assert.async(); - var itemId = /template\/Module\/([0-9]+)\/new-option/.exec(window.location.href)[1]; - - jsonEditor.jsonEditorRmVar('new-vault-password-file', 'prefix'); - jsonEditor.jsonEditorRmVar("become2") - $("#new_json_nameprefix").val("become"); - jsonEditor.jsonEditorAddVar(); - $.when(pmModuleTemplates.saveOption(itemId)).done(function() + $.when(pmTemplates.exportToFile([itemId])).done(function() { - assert.ok(true, 'Опция успешно сохранена'); + assert.ok(true, 'pmTemplates.exportToFile ok'); render(done) - }).fail(function(){ + }).fail(function() + { debugger; - assert.ok(false, 'Ошибка при сохранении опции'); + assert.ok(false, 'pmTemplates.exportToFile error'); render(done) }) }); - syncQUnit.addTest('Сохранение и запуск опции шаблона модуля', function ( assert ) + syncQUnit.addTest('Удаление копии template Module', function ( assert ) { - // Предполагается что мы от прошлого теста попали на страницу просмотра/редактирования опции шаблона var done = assert.async(); - var itemId = /template\/Module\/([0-9]+)\/option\/([A-z0-9 %\-.:,=]+)/.exec(window.location.href)[1]; - $.when(pmModuleTemplates.saveAndExecuteOption(itemId)).done(function() + // Предполагается что мы от прошлого теста попали на страницу редактирования пользователя + // с адресом http://192.168.0.12:8080/?user-5 + var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] + + // Удаление пользователя. + $.when(pmModuleTemplates.deleteItem(itemId, true)).done(function() { - assert.ok(true, 'Опция успешно сохранена'); + assert.ok(true, 'Успешно delete add Item'); render(done) }).fail(function(){ debugger; - assert.ok(false, 'Ошибка при сохранении опции'); + assert.ok(false, 'Ошибка при delete add Item'); render(done) }) }); - syncQUnit.addTest('Удаление шаблона Module', function ( assert ) + + syncQUnit.addTest('Сохранение и запуск шаблона Module', function ( assert ) { var done = assert.async(); - var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] + $.when(spajs.open({ menuId:"template/Module/"+itemId})).done(function() { $.when(pmModuleTemplates.saveAndExecute(itemId)).done(function() @@ -3481,12 +3507,45 @@ window.qunitTestsArray.push({ assert.ok(false, 'Ошибка при открытиии меню template/Module/'+itemId); render(done) }) - }); + }); + + var tt_history_id_1 = undefined; + syncQUnit.addTest('Страница списка Module template History', function ( assert ) + { + + tt_history_id_1=/history\/([0-9]+)/.exec(window.location.href)[1]; + var done = assert.async(); + $.when(spajs.open({ menuId:'template/Module/'+itemId+'/history'})).done(function() + { + assert.ok(true, 'Страница открылась'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Страница не открылась'); + render(done) + }) + }) + + syncQUnit.addTest('Страница Module template History Item', function ( assert ) + { + var done = assert.async(); + $.when(spajs.open({ menuId:'template/Module/'+itemId+'/history/'+tt_history_id_1})).done(function() + { + assert.ok(true, 'Страница открылась'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Страница не открылась'); + render(done) + }) + }) syncQUnit.addTest('Удаление шаблона', function ( assert ) { var done = assert.async(); - var itemId = /template\/Module\/([0-9]+)/.exec(window.location.href)[1] + // Удаление project. $.when(pmModuleTemplates.deleteItem(itemId, true)).done(function() { @@ -3499,7 +3558,7 @@ window.qunitTestsArray.push({ }) }); }}) - + /** * Тестирование pmDashboard */ @@ -3507,7 +3566,7 @@ window.qunitTestsArray.push({ step:900, test:function() { - + syncQUnit.addTest('Страница dashboard', function ( assert ) { var done = assert.async(); @@ -3515,15 +3574,15 @@ window.qunitTestsArray.push({ $.when(spajs.open({ menuId:"home"})).done(function() { assert.ok(true, 'Успешно открыто меню pmDashboard'); - + setTimeout(function(){// Ждём завершения всех асинхронных запросов на странице - - tabSignal.emit('pmLocalSettings.hideMenu', {type:'set', name:'hideMenu', value:false}) + + tabSignal.emit('pmLocalSettings.hideMenu', {type:'set', name:'hideMenu', value:false}) setTimeout(function() - { - render(done) - }, 500) - }, 5000) + { + render(done) + }, 500) + }, 5000) }).fail(function() { debugger; @@ -3541,7 +3600,7 @@ window.qunitTestsArray.push({ step:1000, test:function() { - + syncQUnit.addTest('Поиск projects', function ( assert ) { var done = assert.async(); @@ -3557,7 +3616,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Поиск templates', function ( assert ) { var done = assert.async(); @@ -3573,7 +3632,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Поиск hosts', function ( assert ) { var done = assert.async(); @@ -3589,7 +3648,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Поиск groups', function ( assert ) { var done = assert.async(); @@ -3605,7 +3664,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Поиск history', function ( assert ) { var done = assert.async(); @@ -3621,7 +3680,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Поиск inventories', function ( assert ) { var done = assert.async(); @@ -3637,7 +3696,7 @@ window.qunitTestsArray.push({ render(done) }) }); - + syncQUnit.addTest('Поиск users', function ( assert ) { var done = assert.async(); @@ -3653,8 +3712,24 @@ window.qunitTestsArray.push({ render(done) }) }); - - syncQUnit.addTest('Страница ошибки 400 в history', function ( assert ) + + syncQUnit.addTest('Поиск hooks', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"hooks/search/hook-example"})).done(function() + { + assert.ok(true, 'Успешно открыто меню hooks/search/hook-example'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Ошибка при открытиии меню hooks/search/hook-example'); + render(done) + }) + }); + + syncQUnit.addTest('Страница ошибки 400 в project history', function ( assert ) { var done = assert.async(); @@ -3670,7 +3745,152 @@ window.qunitTestsArray.push({ }) }); - + syncQUnit.addTest('Страница ошибки 400 в project history search', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"project/9999999999/history/search/name"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню project/9999999999/history/search/name'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню project/9999999999/history/search/name'); + render(done) + }) + }); + + syncQUnit.addTest('Страница ошибки 400 в project history search page', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"project/9999999999/history/search/name/page/1"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню project/9999999999/history/search/name/page/1'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню project/9999999999/history/search/name/page/1'); + render(done) + }) + }); + + syncQUnit.addTest('Страница ошибки 400 в inventory history', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"inventory/9999999999/history"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню inventory/9999999999/history'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню inventory/9999999999/history'); + render(done) + }) + }); + + syncQUnit.addTest('Страница ошибки 400 в inventory history search', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"inventory/9999999999/history/search/name"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню inventory/9999999999/history/search/name'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню inventory/9999999999/history/search/name'); + render(done) + }) + }); + + syncQUnit.addTest('Страница ошибки 400 в inventory history search page', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"inventory/9999999999/history/search/name/page/1"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню inventory/9999999999/history/search/name/page/1'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню inventory/9999999999/history/search/name/page/1'); + render(done) + }) + }); + + syncQUnit.addTest('Страница ошибки 400 в task template history search', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"template/Task/9999999999/history/search/name"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню template/Task/9999999999/history/search/name'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню template/Task/9999999999/history/search/name'); + render(done) + }) + }); + + syncQUnit.addTest('Страница ошибки 400 в module template history search', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"template/Module/9999999999/history/search/name"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню template/Module/9999999999/history/search/name'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню template/Module/9999999999/history/search/name'); + render(done) + }) + }); + + + syncQUnit.addTest('Страница ошибки 400 в task template history search page', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"template/Task/9999999999/history/search/name/page/1"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню template/Task/9999999999/history/search/name/page/1'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню template/Task/9999999999/history/search/name/page/1'); + render(done) + }) + }); + + syncQUnit.addTest('Страница ошибки 400 в module template history search page', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"template/Module/9999999999/history/search/name/page/1"})).done(function() + { + debugger; + assert.ok(false, 'Успешно открыто меню template/Module/9999999999/history/search/name/page/1'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при открытиии меню template/Module/9999999999/history/search/name/page/1'); + render(done) + }) + }); + + }}) @@ -3697,13 +3917,13 @@ window.qunitTestsArray.push({ }) }); - + syncQUnit.addTest('Страница history toggleSelectEachItem', function ( assert ) - { + { var done = assert.async(); - - pmHistory.toggleSelectAll($('.multiple-select tr'), true); - + + pmHistory.toggleSelectAll($('.multiple-select tr'), true); + $.when(pmHistory.toggleSelectEachItem(true)).done(function() { assert.ok(true, 'ok:toggleSelectEachItem'); @@ -3715,13 +3935,13 @@ window.qunitTestsArray.push({ render(done) }) }) - + syncQUnit.addTest('Страница history toggleSelectEachItem', function ( assert ) - { + { var done = assert.async(); - - pmHistory.toggleSelectAll($('.multiple-select tr'), false); - + + pmHistory.toggleSelectAll($('.multiple-select tr'), false); + $.when(pmHistory.toggleSelectEachItem(false)).done(function() { $.when(pmHistory.deleteSelected()).done(function() @@ -3734,7 +3954,7 @@ window.qunitTestsArray.push({ assert.ok(false, 'error:deleteSelected'); render(done) }) - + }).fail(function() { debugger; @@ -3742,15 +3962,15 @@ window.qunitTestsArray.push({ render(done) }) }) - + syncQUnit.addTest('Страница history deleteRows', function ( assert ) - { + { var done = assert.async(); - + $.when(pmHistory.deleteRows([])).done(function() - { + { assert.ok(true, 'ok:deleteRows'); - render(done) + render(done) }).fail(function() { debugger; @@ -3758,15 +3978,15 @@ window.qunitTestsArray.push({ render(done) }) }) - + syncQUnit.addTest('Страница history multiOperationsOnEachRow.loadItem', function ( assert ) - { + { var done = assert.async(); - + $.when(pmHistory.multiOperationsOnEachRow([], 'loadItem', true)).done(function() - { + { assert.ok(true, 'ok:multiOperationsOnEachRow.loadItem'); - render(done) + render(done) }).fail(function() { debugger; @@ -3774,9 +3994,9 @@ window.qunitTestsArray.push({ render(done) }) }) - + syncQUnit.addTest('Страница history 2', function ( assert ) - { + { var done = assert.async(); if(!pmHistory.model.itemslist.results.length) @@ -3812,6 +4032,139 @@ window.qunitTestsArray.push({ render(done) }) }); - + }}) - \ No newline at end of file + + +/** + * Тестирование hooks + */ +window.qunitTestsArray.push({ + step:1200, + test:function() +{ + syncQUnit.addTest('Страница hooks', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"hooks"})).done(function() + { + assert.ok(true, 'Успешно открыто меню hooks'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Ошибка при открытиии меню hooks'); + render(done) + }) + }); + + syncQUnit.addTest('Открытие cтраницы new hook', function ( assert ) + { + var done = assert.async(); + + $.when(spajs.open({ menuId:"new-hook"})).done(function() + { + assert.ok(true, 'Успешно открыто меню new-hook'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Ошибка при открытиии меню new-hook'); + render(done) + }) + }); + + syncQUnit.addTest('Сохранение невалидного hook', function ( assert ) + { + var done = assert.async(); + + $.when(pmHooks.addItem()).done(function() + { + debugger; + assert.ok(false, 'Хук успешно сохранен, а не должен был'); + render(done) + }).fail(function() + { + assert.ok(true, 'Ошибка при сохранении хука, как и задумано'); + render(done) + }) + }); + + syncQUnit.addTest('Сохранение валидного hook', function ( assert ) + { + var done = assert.async(); + + $('#filed_name').val('test-hook'); + pmHooks.model.newItem.recipients='test-recipient'; + + $.when(pmHooks.addItem()).done(function() + { + assert.ok(true, 'Hook успешно создан'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Ошибка при создании hook'); + render(done) + }) + }); + + syncQUnit.addTest('Изменение имени hook', function ( assert ) + { + var done = assert.async(); + var item_id=/hook\/([0-9]+)/.exec(window.location.href)[1] + + $('#filed_name').val('test-hook2'); + + $.when(pmHooks.updateItem(item_id)).done(function() + { + assert.ok(true, 'Hook успешно изменен'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Ошибка при изменении hook'); + render(done) + }) + }); + + syncQUnit.addTest('Изменение when hook на null', function ( assert ) + { + var done = assert.async(); + var item_id=/hook\/([0-9]+)/.exec(window.location.href)[1] + + $('#hook-'+item_id+'-when').val('null'); + + $.when(pmHooks.updateItem(item_id)).done(function() + { + assert.ok(true, 'Hook успешно изменен'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Ошибка при изменении hook'); + render(done) + }) + }); + + syncQUnit.addTest('Удаление hook', function ( assert ) + { + var done = assert.async(); + var item_id=/hook\/([0-9]+)/.exec(window.location.href)[1] + + $('#filed_name').val('test-hook2'); + + $.when(pmHooks.deleteItem(item_id, true)).done(function() + { + assert.ok(true, 'Hook успешно удален'); + render(done) + }).fail(function() + { + debugger; + assert.ok(false, 'Ошибка при удалении hook'); + render(done) + }) + }); + +}}) \ No newline at end of file diff --git a/polemarch/static/templates/filedsLib.html b/polemarch/static/templates/filedsLib.html index 4569dcc5..f3d47eb7 100644 --- a/polemarch/static/templates/filedsLib.html +++ b/polemarch/static/templates/filedsLib.html @@ -74,4 +74,24 @@
    <%- filed.help || '' %>
    + + + + \ No newline at end of file diff --git a/polemarch/static/templates/pmDashboard.html b/polemarch/static/templates/pmDashboard.html index 0ad87284..ea375dd9 100644 --- a/polemarch/static/templates/pmDashboard.html +++ b/polemarch/static/templates/pmDashboard.html @@ -129,12 +129,69 @@ + + + + + + + + + + + diff --git a/polemarch/static/templates/pmHooks.html b/polemarch/static/templates/pmHooks.html new file mode 100644 index 00000000..86c83219 --- /dev/null +++ b/polemarch/static/templates/pmHooks.html @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/polemarch/static/templates/pmInventories.html b/polemarch/static/templates/pmInventories.html index e605087e..fcb8cbff 100644 --- a/polemarch/static/templates/pmInventories.html +++ b/polemarch/static/templates/pmInventories.html @@ -1,4 +1,4 @@ - + - - + + + + + + + + - + + + + + + + + + + + \ No newline at end of file diff --git a/polemarch/static/templates/pmTasksTemplates.html b/polemarch/static/templates/pmTasksTemplates.html index e460b89d..751cd1bd 100644 --- a/polemarch/static/templates/pmTasksTemplates.html +++ b/polemarch/static/templates/pmTasksTemplates.html @@ -194,13 +194,52 @@ <% } %> +
    +
    + + + + +
    +
    <% } %> - + + + + + + diff --git a/polemarch/web.ini b/polemarch/web.ini index 57aa0625..0567e5d6 100755 --- a/polemarch/web.ini +++ b/polemarch/web.ini @@ -1,6 +1,7 @@ [uwsgi] # name of prject -program_name = polemarch +lib_name = polemarch +program_name = %(lib_name) # tcp port to bind http = :8080 @@ -51,4 +52,4 @@ daemon = true endif = if-opt = daemon=true daemonize = %(log_file) -endif = \ No newline at end of file +endif = diff --git a/requirements.txt b/requirements.txt index 80ff8587..dc3b8bec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ sqlalchemy django-docs==0.2.1 # API -djangorestframework>=3.4, <=3.6.4 +djangorestframework==3.8.2 django-filter django-crispy-forms django-celery-beat==1.1.1 @@ -15,7 +15,7 @@ jsonfield==2.0.2 uWSGI==2.0.17 # Repo types -gitpython==2.1.8 +gitpython==2.1.9 # Hooks requests==2.18.4 @@ -25,6 +25,6 @@ PyMySQL python-memcached # Ansible required packages -ansible>=2.1, <=2.4.3 +ansible>=2.1, <=2.5.2 paramiko<=2.4.0 pywinrm[kerberos]==0.3.0 diff --git a/rpm.mk b/rpm.mk index 305a39d7..0ea78f3b 100755 --- a/rpm.mk +++ b/rpm.mk @@ -4,13 +4,6 @@ define RPM_SPEC %define shortname $(NAME) %define file_permissions_user $(USER) %define file_permissions_group $(USER) -%define venv_cmd $(PY) -m virtualenv --no-site-packages -%define venv_name %{name} -%define venv_install_dir /opt/%{venv_name} -%define venv_dir %{buildroot}/%{venv_install_dir} -%define venv_bin %{venv_dir}/bin -%define venv_python %{venv_bin}/python -%define venv_pip %{venv_python} %{venv_bin}/pip install $(PIPARGS) %define version $(VER) %define release $(RELEASE) %define __prelink_undo_cmd %{nil} @@ -26,8 +19,6 @@ define RPM_SPEC Name: %{name} Version: %{version} Release: %{release} -BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX) -Source0: %{name}-%{unmangled_version}.tar.gz Summary: $(SUMMARY) Group: Application/System Vendor: $(VENDOR) @@ -50,7 +41,7 @@ $(DESCRIPTION) # Blocks %files %defattr(-,%{file_permissions_user},%{file_permissions_group},-) -/%{venv_install_dir} +$(INSTALL_DIR) /etc/%{name} /var/log/%{name} /var/run/%{name} @@ -64,34 +55,10 @@ id -u %{file_permissions_user} &>/dev/null || useradd %{file_permissions_user} id -g %{file_permissions_group} &>/dev/null || groupadd %{file_permissions_group} %install -make build -%{venv_cmd} %{venv_dir} -%{venv_pip} -U -r requirements-doc.txt -%{venv_pip} dist/%{name}-%{unmangled_version}.tar.gz -r requirements.txt -%{venv_pip} -U -r requirements-git.txt +make BUILD_DIR=%{buildroot} cd %{buildroot} cd - -# RECORD files are used by wheels for checksum. They contain path names which -# match the buildroot and must be removed or the package will fail to build. -find %{buildroot} -name "RECORD" -exec rm -rf {} \; -# Change the virtualenv path to the target installation direcotry. -venvctrl-relocate --source=%{venv_dir} --destination=/%{venv_install_dir} -# Strip native modules as they contain buildroot paths in their debug information -# find %{venv_dir}/lib -type f -name "*.so" | grep -v _cffi_backend | xargs -r strip -find %{venv_dir}/lib -type f -name "*.c" -print0 | xargs -0 rm -rf -# Setup init scripts -mkdir -p $$RPM_BUILD_ROOT/etc/systemd/system -mkdir -p $$RPM_BUILD_ROOT/etc/tmpfiles.d -mkdir -p $$RPM_BUILD_ROOT/etc/%{name} -mkdir -p $$RPM_BUILD_ROOT/var/log/%{name} -mkdir -p $$RPM_BUILD_ROOT/var/run/%{name} -mkdir -p $$RPM_BUILD_ROOT/var/lock/%{name} -mkdir -p $$RPM_BUILD_ROOT/usr/bin -install -m 755 %{name}/main/settings.ini $$RPM_BUILD_ROOT/etc/%{name}/settings.ini.template -install -m 755 initbin/%{shortname}web.service $$RPM_BUILD_ROOT/etc/systemd/system/%{shortname}web.service -install -m 755 initbin/%{shortname}worker.service $$RPM_BUILD_ROOT/etc/systemd/system/%{shortname}worker.service -install -m 755 initbin/%{shortname}.conf $$RPM_BUILD_ROOT/etc/tmpfiles.d/%{shortname}.conf %post # sudo -H -u %{name} /opt/%{name}/bin/%{shortname}ctl migrate @@ -109,9 +76,9 @@ if [ "$$1" = "0" ]; then fi %prep -%setup -n %{name}-%{unmangled_version} rm -rf %{buildroot}/* -mkdir -p %{buildroot}/%{venv_install_dir} +cd %{_topdir}/BUILD +cp -rf $(SOURCE_DIR)/* . %clean rm -rf %{buildroot} diff --git a/setup.cfg b/setup.cfg index 86ffb68d..d45561bb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,8 +27,6 @@ classifiers = [build_sphinx] project = 'Polemarch' -version = attr: polemarch.__version__ -release = attr: polemarch.__version__ [aliases] compile_docs = build_sphinx -b html -s ./doc/ --build-dir ./polemarch/doc/ diff --git a/setup.py b/setup.py index 832b0625..198c485f 100644 --- a/setup.py +++ b/setup.py @@ -87,9 +87,13 @@ def load_requirements(file_name): ext_modules = [] else: ext_modules = list(Extension(m, f) for m, f in extensions_dict.items()) + ext_count = len(ext_modules) + nthreads = ext_count if ext_count < 10 else 10 if use_cython and has_cython: - ext_modules = cythonize(ext_modules) + ext_modules = cythonize( + ext_modules, nthreads=nthreads, force=True + ) class PostInstallCommand(install): @@ -131,7 +135,7 @@ def run(self): include_package_data=True, scripts=['polemarchctl'], install_requires=[ - "django>=1.11,<2.0", + "django>=1.11,<=2.0", ] + REQUIRES, dependency_links=[ ] + REQUIRES_git,