diff --git a/doc/api_schema.yaml b/doc/api_schema.yaml index cba13174..4df54f38 100755 --- a/doc/api_schema.yaml +++ b/doc/api_schema.yaml @@ -11,52 +11,52 @@ info: has_docs: true docs_url: /docs/ x-links: - Request: - - url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Ask&issue%5Btitle%5D=Ask%20about%20version%201.6.0 - name: Question - - url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Bug&issue%5Btitle%5D=Bug%20in%20version%201.6.0 - name: Bug report - - url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Feature%20request&issue%5Btitle%5D= - name: Feature request Documentation: - url: http://polemarch.readthedocs.io/ name: Official documentation + url: http://polemarch.readthedocs.io/ Repository: - url: https://gitlab.com/vstconsulting/polemarch.git name: Official repository + url: https://gitlab.com/vstconsulting/polemarch.git + Request: + - name: Question + url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Ask&issue%5Btitle%5D=Ask%20about%20version%201.6.2 + - name: Bug report + url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Bug&issue%5Btitle%5D=Bug%20in%20version%201.6.2 + - name: Feature request + url: https://gitlab.com/vstconsulting/polemarch/issues/new?issuable_template%5D=Feature%20request&issue%5Btitle%5D= x-menu: - name: Projects - url: /project span_class: fa fa-fort-awesome + url: /project - name: Community - url: /community_template span_class: fa fa-cloud + url: /community_template - name: Inventories - url: /inventory span_class: fa fa-folder sublinks: - name: Groups - url: /group span_class: fa fa-tasks + url: /group - name: Hosts - url: /host span_class: fa fa-codepen + url: /host + url: /inventory - name: History - url: /history span_class: fa fa-calendar + url: /history - name: System span_class: fa fa-cog sublinks: - name: Users - url: /user span_class: fa fa-user + url: /user - name: Hooks - url: /hook span_class: fa fa-plug + url: /hook x-versions: - application: 1.6.0 - library: 1.6.0 - vstutils: 2.12.1 + application: 1.6.2 + library: 1.6.2 + vstutils: 2.15.1 django: 2.2.7 ansible: 2.9.2 version: v2 @@ -10953,14 +10953,14 @@ definitions: type: string format: dynamic additionalProperties: - field: key choices: {} + field: key types: - ansible_ssh_pass: password - ansible_ssh_private_key_file: secretfile ansible_become: boolean - ansible_port: integer ansible_become_pass: password + ansible_port: integer + ansible_ssh_pass: password + ansible_ssh_private_key_file: secretfile Host: type: object properties: @@ -11325,15 +11325,15 @@ definitions: format: dynamic default: NONE additionalProperties: - field: type choices: GIT: - NONE - KEY - PASSWORD + field: type types: - MANUAL: hidden GIT: string + MANUAL: hidden TAR: hidden auth_data: title: Repo auth data @@ -11341,22 +11341,22 @@ definitions: format: dynamic default: '' additionalProperties: - field: repo_auth choices: {} + field: repo_auth types: KEY: secretfile - PASSWORD: password NONE: hidden + PASSWORD: password branch: title: Branch for GIT(branch/tag/SHA) or TAR(subdir) type: string format: dynamic additionalProperties: - field: type choices: {} + field: type types: - MANUAL: hidden GIT: string + MANUAL: hidden TAR: string x-nullable: true OneProject: @@ -11597,55 +11597,10 @@ definitions: $ref: '#/definitions/Playbook' value_field: playbook view_field: name - verbose: - title: Verbose - description: verbose mode (-vvv for more, -vvvv to enable connection debugging) - type: integer - default: 0 - maximum: 4 - private_key: - title: Private key - description: use this file to authenticate the connection - type: string - format: secretfile - user: - title: User - description: connect as this user (default=None) - type: string - connection: - title: Connection - description: connection type to use (default=smart) - type: string - timeout: - title: Timeout - description: override the connection timeout in seconds (default=10) - type: integer - ssh_common_args: - title: Ssh common args - description: specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand) - type: string - sftp_extra_args: - title: Sftp extra args - description: specify extra arguments to pass to sftp only (e.g. -f, -l) - type: string - scp_extra_args: - title: Scp extra args - description: specify extra arguments to pass to scp only (e.g. -l) - type: string - ssh_extra_args: - title: Ssh extra args - description: specify extra arguments to pass to ssh only (e.g. -R) + args: + title: Args + description: Playbook(s) type: string - force_handlers: - title: Force handlers - description: run handlers even if a task fails - type: boolean - default: false - flush_cache: - title: Flush cache - description: clear the fact cache for every host in inventory - type: boolean - default: false become: title: Become description: run operations with become (does not imply password prompting) @@ -11660,31 +11615,41 @@ definitions: title: Become user description: run operations as this user (default=root) type: string - tags: - title: Tags - description: only run plays and tasks tagged with these values - type: string - skip_tags: - title: Skip tags - description: only run plays and tasks whose tags do not match these values - type: string check: title: Check description: don't make any changes; instead, try to predict some of the changes that may occur type: boolean default: false - syntax_check: - title: Syntax check - description: perform a syntax check on the playbook, but do not execute it - type: boolean - default: false + connection: + title: Connection + description: connection type to use (default=smart) + type: string diff: title: Diff description: when changing (small) files and templates, show the differences in those files; works great with --check type: boolean default: false + extra_vars: + title: Extra vars + description: set additional variables as key=value or YAML/JSON, if filename + prepend with @ + type: string + flush_cache: + title: Flush cache + description: clear the fact cache for every host in inventory + type: boolean + default: false + force_handlers: + title: Force handlers + description: run handlers even if a task fails + type: boolean + default: false + forks: + title: Forks + description: specify number of parallel processes to use (default=5) + type: integer inventory: title: Inventory description: specify inventory host path or comma separated host list. --inventory-file @@ -11696,40 +11661,13 @@ definitions: $ref: '#/definitions/Inventory' value_field: id view_field: name - list_hosts: - title: List hosts - description: outputs a list of matching hosts; does not execute anything else - type: boolean - default: false limit: title: Limit description: further limit selected hosts to an additional pattern type: string - extra_vars: - title: Extra vars - description: set additional variables as key=value or YAML/JSON, if filename - prepend with @ - type: string - vault_id: - title: Vault id - description: the vault identity to use - type: string - vault_password_file: - title: Vault password file - description: vault password file - type: string - format: secretfile - forks: - title: Forks - description: specify number of parallel processes to use (default=5) - type: integer - module_path: - title: Module path - description: prepend colon-separated path(s) to module library (default=~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules) - type: string - list_tasks: - title: List tasks - description: list all tasks that would be executed + list_hosts: + title: List hosts + description: outputs a list of matching hosts; does not execute anything else type: boolean default: false list_tags: @@ -11737,19 +11675,81 @@ definitions: description: list all available tags type: boolean default: false - step: - title: Step - description: 'one-step-at-a-time: confirm each task before running' + list_tasks: + title: List tasks + description: list all tasks that would be executed type: boolean default: false + module_path: + title: Module path + description: prepend colon-separated path(s) to module library (default=~/.ansible/plugins/modules:/usr/share/ansible/plugins/modules) + type: string + private_key: + title: Private key + description: use this file to authenticate the connection + type: string + format: secretfile + scp_extra_args: + title: Scp extra args + description: specify extra arguments to pass to scp only (e.g. -l) + type: string + sftp_extra_args: + title: Sftp extra args + description: specify extra arguments to pass to sftp only (e.g. -f, -l) + type: string + skip_tags: + title: Skip tags + description: only run plays and tasks whose tags do not match these values + type: string + ssh_common_args: + title: Ssh common args + description: specify common arguments to pass to sftp/scp/ssh (e.g. ProxyCommand) + type: string + ssh_extra_args: + title: Ssh extra args + description: specify extra arguments to pass to ssh only (e.g. -R) + type: string start_at_task: title: Start at task description: start the playbook at the task matching this name type: string - args: - title: Args - description: Playbook(s) + step: + title: Step + description: 'one-step-at-a-time: confirm each task before running' + type: boolean + default: false + syntax_check: + title: Syntax check + description: perform a syntax check on the playbook, but do not execute it + type: boolean + default: false + tags: + title: Tags + description: only run plays and tasks tagged with these values + type: string + timeout: + title: Timeout + description: override the connection timeout in seconds (default=10) + type: integer + user: + title: User + description: connect as this user (default=None) + type: string + vault_id: + title: Vault id + description: the vault identity to use + type: string + vault_password_file: + title: Vault password file + description: vault password file type: string + format: secretfile + verbose: + title: Verbose + description: verbose mode (-vvv for more, -vvvv to enable connection debugging) + type: integer + default: 0 + maximum: 4 ProjectHistory: required: - mode @@ -11879,22 +11879,22 @@ definitions: type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: fk_autocomplete MODULE: fk_autocomplete + PLAYBOOK: fk_autocomplete TEMPLATE: hidden inventory: title: Inventory type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: fk_autocomplete MODULE: fk_autocomplete + PLAYBOOK: fk_autocomplete TEMPLATE: hidden save_result: title: Save result @@ -11908,11 +11908,11 @@ definitions: type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: hidden MODULE: hidden + PLAYBOOK: hidden TEMPLATE: autocomplete enabled: title: Enabled @@ -11929,8 +11929,8 @@ definitions: type: string format: dynamic additionalProperties: - field: type choices: {} + field: type types: CRONTAB: crontab INTERVAL: integer @@ -11961,22 +11961,22 @@ definitions: type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: fk_autocomplete MODULE: fk_autocomplete + PLAYBOOK: fk_autocomplete TEMPLATE: hidden inventory: title: Inventory type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: fk_autocomplete MODULE: fk_autocomplete + PLAYBOOK: fk_autocomplete TEMPLATE: hidden save_result: title: Save result @@ -11990,11 +11990,11 @@ definitions: type: string format: dynamic additionalProperties: - field: kind choices: {} + field: kind types: - PLAYBOOK: hidden MODULE: hidden + PLAYBOOK: hidden TEMPLATE: autocomplete enabled: title: Enabled @@ -12011,8 +12011,8 @@ definitions: type: string format: dynamic additionalProperties: - field: type choices: {} + field: type types: CRONTAB: crontab INTERVAL: integer @@ -12176,20 +12176,20 @@ definitions: type: string format: dynamic additionalProperties: - field: key choices: + repo_sync_on_run: + - true + - false repo_type: - MANUAL - GIT - TAR - repo_sync_on_run: - - true - - false + field: key types: - repo_password: password + ci_template: fk repo_key: secretfile + repo_password: password repo_sync_on_run_timeout: uptime - ci_template: fk Team: required: - name @@ -12421,6 +12421,13 @@ definitions: - widgetSettings type: object properties: + lang: + title: Lang + type: string + enum: + - en + - ru + default: en autoupdateInterval: title: Autoupdateinterval type: integer @@ -12429,3 +12436,9 @@ definitions: $ref: '#/definitions/ChartLineSettings' widgetSettings: $ref: '#/definitions/WidgetSettings' + selectedSkin: + title: Selectedskin + type: string + minLength: 1 + skinsSettings: + $ref: '#/definitions/Data' diff --git a/doc/installation.rst b/doc/installation.rst index 6a5abf69..3472cf49 100644 --- a/doc/installation.rst +++ b/doc/installation.rst @@ -133,8 +133,14 @@ Install from PyPI threads = 4 harakiri = 120 vacuum = True + http-keepalive = true + http-auto-chunked = true + thread-stacksize = 512 pidfile = /opt/polemarch/pid/polemarch.pid log_file = /opt/polemarch/logs/{PROG_NAME}_web.log + # Uncomment it for HTTPS and install `uwsgi` pypi package to env: + # addrport = 127.0.0.1:8080 + # https = 0.0.0.0:443,/etc/polemarch/polemarch.crt,/etc/polemarch/polemarch.key [worker] # output will be /opt/polemarch/logs/polemarch_worker.log diff --git a/docker_release.yml b/docker_release.yml index 7d855a99..d4c78822 100644 --- a/docker_release.yml +++ b/docker_release.yml @@ -59,13 +59,13 @@ - meta: flush_handlers - name: Get latest release tag - shell: echo -ne $(git describe --tags `git rev-list --tags --max-count=1`) + shell: git describe --tags `git rev-list --tags --max-count=1` register: release_latest_version - name: Set latest tag set_fact: docker_tag: 'latest' - changed_when: release_latest_version.tag == tag + changed_when: release_latest_version.stdout == tag notify: - "build registry" - "build dockerhub" diff --git a/environment/docker_data/settings.ini b/environment/docker_data/settings.ini index be45355b..e246d85a 100644 --- a/environment/docker_data/settings.ini +++ b/environment/docker_data/settings.ini @@ -5,3 +5,10 @@ hooks_dir = /hooks [uwsgi] pidfile = /run/web.pid addrport = 0.0.0.0:8080 +vacuum = True +max-requests = 1000 +max-worker-lifetime = 3600 +worker-reload-mercy = 60 +http-keepalive = true +http-auto-chunked = true +thread-stacksize = 1024 diff --git a/polemarch/__init__.py b/polemarch/__init__.py index a13a18a6..2030adcf 100644 --- a/polemarch/__init__.py +++ b/polemarch/__init__.py @@ -31,6 +31,6 @@ "VST_ROOT_URLCONF": os.getenv("VST_ROOT_URLCONF", 'vstutils.urls'), } -__version__ = "1.6.1.post2" +__version__ = "1.6.2" prepare_environment(**default_settings) diff --git a/polemarch/api/v2/filters.py b/polemarch/api/v2/filters.py index 165c4be1..ae72e6d3 100644 --- a/polemarch/api/v2/filters.py +++ b/polemarch/api/v2/filters.py @@ -2,7 +2,7 @@ from functools import reduce from operator import or_ from django.db.models import Q -from django_filters import (CharFilter, NumberFilter, IsoDateTimeFilter) +from django_filters import CharFilter, NumberFilter, IsoDateTimeFilter from vstutils.api.filters import DefaultIDFilter, extra_filter, name_filter, filters from ...main import models diff --git a/polemarch/api/v2/serializers.py b/polemarch/api/v2/serializers.py index 415c6e7d..6ecebfab 100644 --- a/polemarch/api/v2/serializers.py +++ b/polemarch/api/v2/serializers.py @@ -3,26 +3,26 @@ from typing import Dict, List import json import uuid -try: - from ruamel.ordereddict import ordereddict as OrderedDict -except ImportError: # nocv - from collections import OrderedDict -import six -from django.contrib.auth.models import User +from collections import OrderedDict +from django.contrib.auth import get_user_model from django.utils.functional import cached_property from django.db import transaction from rest_framework import serializers, exceptions, status -from rest_framework.exceptions import PermissionDenied from vstutils.api import serializers as vst_serializers, fields as vst_fields from vstutils.api.serializers import DataSerializer, EmptySerializer from vstutils.api.base import Response from ...main.utils import AnsibleArgumentsReference, AnsibleInventoryParser +from ...main.settings import LANGUAGES -from ...main.models import Inventory from ...main import models from ..signals import api_post_save, api_pre_save +User = get_user_model() + +LANG_CHOICES = [item[0] for item in LANGUAGES] + + # NOTE: we can freely remove that because according to real behaviour all our # models always have queryset at this stage, so this code actually doing # nothing @@ -33,7 +33,7 @@ class DictField(serializers.CharField): def to_internal_value(self, data): # nocv return ( data - if isinstance(data, (six.string_types, six.text_type, dict, list)) + if isinstance(data, (str, dict, list)) else self.fail("Unknown type.") ) @@ -51,7 +51,7 @@ def to_internal_value(self, data): def to_representation(self, value): return ( - value if not isinstance(value, six.class_types) + value if not isinstance(value, type) else str(value) ) @@ -68,10 +68,12 @@ class InventoryAutoCompletionField(vst_fields.AutoCompletionField): def to_internal_value(self, data): inventory = super(InventoryAutoCompletionField, self).to_internal_value(data) try: - inventory = Inventory.objects.get(id=int(inventory)) + inventory = models.Inventory.objects.get(id=int(inventory)) user = self.context['request'].user if not inventory.acl_handler.viewable_by(user): - raise PermissionDenied("You don't have permission to inventory.") # noce + raise exceptions.PermissionDenied( + "You don't have permission to inventory." + ) # noce except (ValueError, KeyError): self.check_path(inventory) return inventory @@ -120,7 +122,7 @@ class SetOwnerSerializer(DataSerializer): def update(self, instance, validated_data): if not self.instance.acl_handler.owned_by(self.current_user()): # noce - raise PermissionDenied(self.perms_msg) + raise exceptions.PermissionDenied(self.perms_msg) user = self.get_user(validated_data) self.instance.acl_handler.set_owner(user) return user @@ -132,7 +134,7 @@ def current_user(self) -> User: return self.context['request'].user def to_representation(self, value: User): - return dict(user_id=value.id) + return dict(user_id=value.pk) def to_internal_value(self, data: dict): return dict(pk=data['user_id']) @@ -245,9 +247,12 @@ class WidgetSettingsSerializer(vst_serializers.JsonObjectSerializer): class UserSettingsSerializer(vst_serializers.JsonObjectSerializer): + lang = serializers.ChoiceField(choices=LANG_CHOICES, default=LANG_CHOICES[0]) autoupdateInterval = serializers.IntegerField(default=15000) chartLineSettings = ChartLineSettingsSerializer() widgetSettings = WidgetSettingsSerializer() + selectedSkin = serializers.CharField(required=False) + skinsSettings = vst_serializers.DataSerializer(required=False) class TeamSerializer(_WithPermissionsSerializer): @@ -1050,8 +1055,7 @@ def __new__(cls, name, bases, attrs): return super(AnsibleSerializerMetaclass, cls).__new__(cls, name, bases, attrs) -@six.add_metaclass(AnsibleSerializerMetaclass) -class _AnsibleSerializer(serializers.Serializer): +class _AnsibleSerializer(serializers.Serializer, metaclass=AnsibleSerializerMetaclass): # pylint: disable=abstract-method pass @@ -1116,7 +1120,7 @@ def create(self, validated_data: Dict) -> Dict: parser = AnsibleInventoryParser() inv_json = parser.get_inventory_data(validated_data['raw_data']) - inventory = Inventory.objects.create(name=validated_data['name']) + inventory = models.Inventory.objects.create(name=validated_data['name']) inventory.vars = inv_json['vars'] created_hosts, created_groups = dict(), dict() diff --git a/polemarch/main/migrations/0002_modules_and_rename.py b/polemarch/main/migrations/0002_modules_and_rename.py index 323668b8..ed5a2921 100644 --- a/polemarch/main/migrations/0002_modules_and_rename.py +++ b/polemarch/main/migrations/0002_modules_and_rename.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.10 on 2018-07-09 07:50 from __future__ import unicode_literals -import six from django.db import migrations, models import django.db.models.deletion from ..models.base import ForeignKeyACL @@ -30,7 +29,7 @@ def set_inventories_to_project(apps, schema_editor): for templ in Template.objects.all(): try: inv = templ.inventory_object - if not isinstance(inv, (six.string_types, six.text_type)): + if not isinstance(inv, str): templ.project.inventories.add(inv) except: pass diff --git a/polemarch/main/migrations/0046_auto_20180608_0658.py b/polemarch/main/migrations/0046_auto_20180608_0658.py index 1a5f1f07..5c4bf999 100644 --- a/polemarch/main/migrations/0046_auto_20180608_0658.py +++ b/polemarch/main/migrations/0046_auto_20180608_0658.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- # Generated by Django 1.11.10 on 2018-06-08 06:58 from __future__ import unicode_literals +import io from functools import partial -import six from django.db import migrations, models from django.db.models.functions import Length def __bulking_lines(history, value, number, obj_class): - out = six.StringIO(value) + out = io.StringIO(value) for line in iter(partial(out.read, 2 * 1024 - 100), ''): yield obj_class(history=history, line_number=number, line=line) diff --git a/polemarch/main/models/__init__.py b/polemarch/main/models/__init__.py index 4ab1436f..aa831126 100644 --- a/polemarch/main/models/__init__.py +++ b/polemarch/main/models/__init__.py @@ -1,13 +1,11 @@ -# pylint: disable=unused-argument,no-member +# pylint: disable=unused-argument from __future__ import absolute_import -from typing import Any, Text, NoReturn, Iterable, Union +from typing import Any, Text, NoReturn, Iterable, Dict, Union import sys import json import logging from collections import OrderedDict -from pytz import timezone -import django_celery_beat -from django_celery_beat.models import IntervalSchedule, CrontabSchedule +from django_celery_beat.models import IntervalSchedule, CrontabSchedule, PeriodicTask as CPTask from django.db.models import signals, IntegerField from django.db import transaction from django.dispatch import receiver @@ -212,7 +210,7 @@ def clean_dirs(instance: Project, **kwargs) -> None: instance.repo_class.delete() -def compare_schedules(new_schedule_data: OrderedDict, old_schedule: Union[CrontabSchedule, IntervalSchedule]): +def compare_schedules(new_schedule_data: Dict, old_schedule: Union[CrontabSchedule, IntervalSchedule]): """ Method for compare parameters for Schedule from instance and current Periodic Task Schedule Params :param new_schedule_data: Dictionary contains data for new Schedule @@ -246,10 +244,9 @@ def save_to_beat(instance: PeriodicTask, **kwargs) -> NoReturn: interval=IntervalSchedule, crontab=CrontabSchedule, ) - manager = django_celery_beat.models.PeriodicTask.objects # Try get Celery Periodic Task, that linked with Polemarch Periodic Task - celery_task = manager.filter( + celery_task = CPTask.objects.filter( name=str(instance.id), task=settings.TASKS_HANDLERS["SCHEDUER"]["BACKEND"] ).last() @@ -262,6 +259,8 @@ def save_to_beat(instance: PeriodicTask, **kwargs) -> NoReturn: elif instance_pt_type == 'crontab': schedule_data = instance.crontab_kwargs schedule_data['timezone'] = settings.TIME_ZONE + else: + raise ValidationError("Unknown periodic task type `{}`.".format(instance.type)) # nocv if celery_task: @@ -278,12 +277,11 @@ def save_to_beat(instance: PeriodicTask, **kwargs) -> NoReturn: for type_name in filter(lambda k: k != instance_pt_type, types_dict.keys()): schedule_old = getattr(celery_task, type_name, None) if schedule_old is not None: - schedule_old_type = type_name + setattr(celery_task, type_name, None) break # Update data for old schedule type and new schedule type setattr(celery_task, instance_pt_type, schedule_new) - setattr(celery_task, schedule_old_type, None) else: # Update celery periodic task schedule setattr(celery_task, instance_pt_type, schedule_new) @@ -294,7 +292,7 @@ def save_to_beat(instance: PeriodicTask, **kwargs) -> NoReturn: schedule_old.delete() else: # Create new Celery Periodic Task, if it doesn't exist - manager.create( + CPTask.objects.create( name=str(instance.id), task=settings.TASKS_HANDLERS["SCHEDUER"]["BACKEND"], args=json.dumps([instance.id]), @@ -315,7 +313,7 @@ def delete_from_beat(instance: PeriodicTask, **kwargs) -> NoReturn: return # Get Celery Periodic Task - celery_task = django_celery_beat.models.PeriodicTask.objects.filter( + celery_task = CPTask.objects.filter( name=str(instance.id), task=settings.TASKS_HANDLERS["SCHEDUER"]["BACKEND"] ).last() diff --git a/polemarch/main/models/hosts.py b/polemarch/main/models/hosts.py index 9c23ad1c..2bd6ea91 100644 --- a/polemarch/main/models/hosts.py +++ b/polemarch/main/models/hosts.py @@ -2,7 +2,6 @@ from __future__ import unicode_literals from typing import Any, List, Tuple, Dict, Text import logging -import six from django.db.models import Q try: from yaml import dump as to_yaml, CDumper as Dumper, ScalarNode @@ -26,7 +25,7 @@ class InventoryDumper(Dumper): yaml_representers[type(None)] = lambda dumper, value: ( ScalarNode(tag=u'tag:yaml.org,2002:null', value='') ) - yaml_representers[six.text_type] = lambda dumper, value: ( + yaml_representers[str] = lambda dumper, value: ( ScalarNode(tag=u'tag:yaml.org,2002:str', value=value) ) diff --git a/polemarch/main/models/projects.py b/polemarch/main/models/projects.py index 0f686b1a..a16f2e31 100644 --- a/polemarch/main/models/projects.py +++ b/polemarch/main/models/projects.py @@ -7,7 +7,6 @@ import traceback import time import uuid -import six import requests from docutils.core import publish_parts as rst_gen from markdown2 import Markdown @@ -278,7 +277,7 @@ def execute_view_data(self) -> Dict[str, Dict[str, Dict]]: return view_data def check_path(self, inventory) -> NoReturn: - if not isinstance(inventory, (six.string_types, six.text_type)): # nocv + if not isinstance(inventory, str): # nocv return path = "{}/{}".format(self.path, inventory) path = os.path.abspath(os.path.expanduser(path)) diff --git a/polemarch/main/models/tasks.py b/polemarch/main/models/tasks.py index 87557686..1250a731 100644 --- a/polemarch/main/models/tasks.py +++ b/polemarch/main/models/tasks.py @@ -8,7 +8,7 @@ import json import re -import six +import io from celery.schedules import crontab from django.core.exceptions import ValidationError from django.db import transaction @@ -117,7 +117,7 @@ def ci_run(self): self.execute(self.project.owner) def _convert_to_data(self, value): - if isinstance(value, (six.string_types, six.text_type)): + if isinstance(value, str): return json.loads(value) # nocv elif isinstance(value, (dict, OrderedDict, list)): return value @@ -321,7 +321,7 @@ def _get_history_stats_by(self, qs, grouped_by='day') -> List: sum_by_date[hist_stat[grouped_by]] = ( sum_by_date.get(hist_stat[grouped_by], 0) + hist_stat['sum'] ) - for hist_stat in qs.order_by(grouped_by): + for hist_stat in qs.order_by(grouped_by, 'status'): hist_stat.update({'all': sum_by_date[hist_stat[grouped_by]]}) values.append(hist_stat) return values @@ -361,7 +361,7 @@ def start(self, project, kind, mod_name, inventory, **extra) -> Tuple[Any, Dict] executor=extra_options['executor'], hidden=project.hidden, options=self.__get_additional_options(extra_options) ) - if isinstance(inventory, (six.string_types, six.text_type)): + if isinstance(inventory, str): history_kwargs['inventory'] = None elif isinstance(inventory, int): history_kwargs['inventory'] = project.inventories.get(pk=inventory) # nocv @@ -531,7 +531,7 @@ def __create_line(self, gnum: int, num: int, val: str, hidden: bool = False) -> ) def __bulking_lines(self, value: str, number: int, endl: str) -> Generator: - out = six.StringIO(value) + out = io.StringIO(value) nline = 0 for line in iter(partial(out.read, 2 * 1024 - 100), ''): nline += 1 diff --git a/polemarch/main/tests/executions.py b/polemarch/main/tests/executions.py index 490e78c9..ae50ff52 100644 --- a/polemarch/main/tests/executions.py +++ b/polemarch/main/tests/executions.py @@ -9,7 +9,6 @@ import git import requests -import six from django.conf import settings from django.utils.timezone import now from django.core.management import call_command @@ -537,7 +536,7 @@ def project_workflow(self, repo_type, **kwargs): if not execute: return kwargs = getattr(self, 'wip_{}'.format(repo_type.lower()), str)(project_data) - kwargs = kwargs if not isinstance(kwargs, six.string_types) else dict() + kwargs = kwargs if not isinstance(kwargs, str) else dict() self.change_owner(project_data) self.playbook_tests(project_data, **kwargs) self.module_tests(project_data) @@ -749,7 +748,7 @@ def wip_git(self, project_data): for required_field in ['title', 'default', 'format', 'help']: self.assertIn(required_field, field.keys()) self.assertEqual(field_name.split('_')[-1], field['format'], field) - default_type = (six.string_types, six.text_type) + default_type = str if field['format'] == 'boolean': default_type = bool elif field['format'] == 'integer': diff --git a/polemarch/main/unittests/ansible.py b/polemarch/main/unittests/ansible.py index a57fcd8c..4450d30a 100644 --- a/polemarch/main/unittests/ansible.py +++ b/polemarch/main/unittests/ansible.py @@ -1,4 +1,4 @@ -import six +import io from django.test import TestCase from django.core.management import call_command from ..utils import AnsibleInventoryParser @@ -72,7 +72,7 @@ class AnsibleTestCase(TestCase): def test_modules(self): - out = six.StringIO() + out = io.StringIO() call_command('update_ansible_modules', interactive=False, stdout=out) self.assertEqual( 'The modules have been successfully updated.\n', diff --git a/polemarch/static/css/polemarch-gui.css b/polemarch/static/css/polemarch-gui.css index e02d3d71..cc41bdf6 100644 --- a/polemarch/static/css/polemarch-gui.css +++ b/polemarch/static/css/polemarch-gui.css @@ -180,39 +180,26 @@ body { --chart-axes-lines-color: #bababa; } - -.tr-status-offline .td-history-status, -.tr-status-offline .td-project_history-status, .field-status-offline { color: var(--history-status-offline); } -.tr-status-interrupted .td-history-status, -.tr-status-interrupted .td-project_history-status, .field-status-interrupted { color: var(--history-status-interrupted); } -.tr-status-run .td-history-status, -.tr-status-run .td-project_history-status, .field-status-run { color: var(--history-status-run); } -.tr-status-error .td-history-status, -.tr-status-error .td-project_history-status, .field-status-error { color: var(--history-status-error); } -.tr-status-delay .td-history-status, -.tr-status-delay .td-project_history-status, .field-status-delay { color: var(--history-status-delay); } -.tr-status-ok .td-history-status, -.tr-status-ok .td-project_history-status, .field-status-ok { color: var(--history-status-ok); } @@ -223,28 +210,22 @@ body { vertical-align: middle; } - -.tr-status-sync .td-project-status, .field-status-sync { color: var(--project-status-sync); } -.tr-status-wait_sync .td-project-status, .field-status-wait_sync { color: var(--project-status-wait-sync); } -.tr-status-new .td-project-status, .field-status-new { color: var(--project-status-new); } -.tr-status-error .td-project-status, .field-status-error { color: var(--project-status-error); } -.tr-status-ok .td-project-status, .field-status-ok { color: var(--project-status-ok); } @@ -365,7 +346,7 @@ body { float: left; } -@media (max-width: 452px) { +@media (max-width: 540px) { #period-list { width: 100%!important; margin-top: 10px; diff --git a/polemarch/static/js/pmDashboard.js b/polemarch/static/js/pmDashboard.js index 3451ba5d..6080cb1a 100644 --- a/polemarch/static/js/pmDashboard.js +++ b/polemarch/static/js/pmDashboard.js @@ -102,7 +102,10 @@ guiWidgets.history_chart = class HistoryChart extends guiWidgets.line_chart { color: guiCustomizer.skin.settings.chart_axes_lines_color, } }] - } + }, + tooltips: { + mode: 'index', + }, }; } }); @@ -528,7 +531,14 @@ customRoutesComponentsTemplates.home = { /* globals customRoutesComponentsTempla this.setWidgetsData().then(data => { this.widgets_data = data; }); - } + }, + /** + * Saves widgets' data, when chart widget was collapsed/uncollapsed. + * @param value + */ + 'widgets.pmwChartWidget.collapse': function(value) { + this.saveWidgetSettingToApi('pmwChartWidget', 'collapse', value); + }, }, created() { this.fetchData(); @@ -563,12 +573,6 @@ customRoutesComponentsTemplates.home = { /* globals customRoutesComponentsTempla this.setLoadingError(error); }); }, - /** - * Redefinition of 'getAutoUpdateInterval' method of view_with_autoupdate_mixin. - */ - getAutoUpdateInterval() { - return 15000; - }, /** * Redefinition of 'updateData' method of view_with_autoupdate_mixin. */ @@ -598,18 +602,7 @@ customRoutesComponentsTemplates.home = { /* globals customRoutesComponentsTempla */ setWidgetsData() { return this.loadStats().then(response => { - let exclude_stats = ['jobs']; - let w_data = {}; - - for(let key in response.data) { - if (response.data.hasOwnProperty(key)) { - if (exclude_stats.includes(key)) { - w_data.pmwChartWidget = response.data[key]; - } - - w_data['pmw' + capitalizeString(key) + 'Counter'] = response.data[key]; - } - } + let w_data = this.statsResponseToWidgetData(response); this.$store.commit('setWidgets', { url: this.$route.path, @@ -631,10 +624,75 @@ customRoutesComponentsTemplates.home = { /* globals customRoutesComponentsTempla formBulkStats() { return { type: 'get', - item: 'stats', + data_type: ['stats'], filters: "last=" + this.widgets.pmwChartWidget.period.query_amount, }; }, + /** + * Method, that transforms API response with stats to widgets data. + * @param {object} response API response object + */ + statsResponseToWidgetData(response) { + let w_data = {}; + let exclude_stats = ['jobs']; + + for(let key in response.data) { + if (!response.data.hasOwnProperty(key)) { + continue; + } + + if (exclude_stats.includes(key)) { + w_data.pmwChartWidget = response.data[key]; + continue; + } + + w_data['pmw' + capitalizeString(key) + 'Counter'] = response.data[key]; + } + + return w_data; + }, + /** + * Method, that updates some property of widget and sends API request for saving updated User Settings. + * @param {string} widget Widget name + * @param {string} prop Widget's property name + * @param {any} value New value of widget's property name + */ + saveWidgetSettingToApi(widget, prop, value) { + let qs = app.application.$store.state.objects["user/" + my_user_id + "/settings"]; + + if(!qs) { + return; + } + + let instance = qs.cache; + + if(!instance) { + return; + } + + if(!instance.data) { + return; + } + + if(!instance.data.widgetSettings) { + instance.data.widgetSettings = {}; + } + + if(!instance.data.widgetSettings[widget]) { + instance.data.widgetSettings[widget] = {}; + } + + let widget_setting_backup = {...instance.data.widgetSettings[widget]}; + + instance.data.widgetSettings[widget][prop] = value; + + let view = app.views["/user/{" + path_pk_key + "}/settings/edit/"]; + instance.save(view.schema.query_type).then(instance => { + guiDashboard.updateSettings(instance.data); + }).catch(error => { /*jshint unused:false*/ + instance.data.widgetSettings[widget] = widget_setting_backup; + }); + }, }, }; @@ -642,10 +700,7 @@ tabSignal.connect('app.afterInit', (obj) => { let app = obj.app; let setting_view = app.views["/profile/settings/"]; let qs = setting_view.objects.clone(); - let f_obj = {}; - f_obj[path_pk_key] = my_user_id; - // qs.url = qs.url.format({pk:my_user_id}).replace(/^\/|\/$/g, ""); - qs.url = qs.url.format(f_obj).replace(/^\/|\/$/g, ""); + qs.url = qs.url.format({[path_pk_key]: my_user_id}).replace(/^\/|\/$/g, ""); qs.get().then(instance => { guiDashboard.updateSettings(instance.data); diff --git a/polemarch/static/js/pmItems.js b/polemarch/static/js/pmItems.js index d5720cd6..b3aace6a 100644 --- a/polemarch/static/js/pmItems.js +++ b/polemarch/static/js/pmItems.js @@ -70,8 +70,8 @@ vst_vue_components.widgets.w_history_chart = Vue.component('w_history_chart', { }; }, watch: { - 'customizer.skin.name': function(value) { /* jshint unused: false */ /* TODO ask about args*/ - this.generateChart(); + 'customizer.skin.name': function(value) { /* jshint unused: false */ + this.updateChartData(); } }, computed: { diff --git a/polemarch/static/js/pmUsers.js b/polemarch/static/js/pmUsers.js index f814ee27..4ed032e6 100644 --- a/polemarch/static/js/pmUsers.js +++ b/polemarch/static/js/pmUsers.js @@ -9,7 +9,9 @@ const user_settings_page_edit_mixin = { return; } - if(this.qs_url.replace(/^\/|\/$/g, "") == 'user/' + my_user_id + '/settings') { + let is_current_user_settings = this.qs_url.replace(/^\/|\/$/g, "") == 'user/' + my_user_id + '/settings'; + + if(is_current_user_settings) { data.selectedSkin = guiCustomizer.skin.name; data.skinsSettings = guiCustomizer.skins_custom_settings; } @@ -24,7 +26,9 @@ const user_settings_page_edit_mixin = { qs.cache = instance; this.setQuerySet(this.view, this.qs_url, qs); - guiDashboard.updateSettings(instance.data); + if(is_current_user_settings) { + guiDashboard.updateSettings(instance.data); + } guiPopUp.success(this.$t('User settings were successfully saved.')); @@ -99,46 +103,64 @@ function prepareUserSettingsViews(base_path) { } /** - * Signal, that adds 'lang' field to UserSettings model's fields. - * It supposed to be first during rendering. + * Function prepares fields of User Settings Model. + * @param {string} model name of model. */ -tabSignal.connect('openapi.loaded', openapi => { - openapi.definitions.UserSettings.properties = { - lang: {format: 'choices', title: 'language', description: 'application interface language'}, - ...openapi.definitions.UserSettings.properties, - }; -}); +function prepareUserSettingsModelFields(model) { + /** + * Signal, that edits options of UserSettings model's fields. + */ + tabSignal.connect("models[" + model + "].fields.beforeInit", (fields => { + if(fields.lang) { + fields.lang.title = 'language'; + fields.lang.description = 'application interface language'; + } + + if(fields.autoupdateInterval) { + fields.autoupdateInterval.format = 'time_interval'; + fields.autoupdateInterval.required = true; + fields.autoupdateInterval.title = 'Auto update interval'; + fields.autoupdateInterval.description = 'application automatically updates pages data' + + ' with following interval (time in seconds)'; + } + + [ + {name: 'chartLineSettings', title: "Dashboard chart lines settings", }, + {name: 'widgetSettings', title: "Dashboard widgets settings"}, + ].forEach((item) => { + if(fields[item.name]) { + fields[item.name].format = 'inner_api_object'; + fields[item.name].title = item.title; + } + }); + + if(fields.selectedSkin) { + fields.selectedSkin = { + title: 'Selected skin', + format: 'hidden', + }; + } + + if(fields.skinsSettings) { + fields.skinsSettings = { + title: 'Skin settings', + format: 'hidden', + }; + } + })); +} /** - * Signal, that edits options of UserSettings model's fields. + * Variable, that stores name of user Settings model. */ -tabSignal.connect("models[UserSettings].fields.beforeInit", (fields => { - [ - {name: 'chartLineSettings', title: "Dashboard chart lines settings", }, - {name: 'widgetSettings', title: "Dashboard widgets settings"}, - ].forEach((item) => { - fields[item.name].format = 'inner_api_object'; - fields[item.name].title = item.title; - }); +let user_settings_model_name = 'UserSettings'; + +/** + * Prepares fields of user settings model. + */ +prepareUserSettingsModelFields(user_settings_model_name); - fields.autoupdateInterval.format = 'time_interval'; - fields.autoupdateInterval.required = true; - fields.autoupdateInterval.title = 'Auto update interval'; - fields.autoupdateInterval.description = 'application automatically updates pages data' + - ' with following interval (time in seconds)'; - - fields.selectedSkin = { - title: 'Selected skin', - format: 'hidden', - }; - fields.skinsSettings = { - title: 'Skin settings', - format: 'hidden', - }; - - fields.lang.enum = app.languages.map(lang => lang.code); -})); /** * Emits signals for UserSettings views. */ -prepareUserSettingsViews('/user/{' + path_pk_key + '}/settings/'); \ No newline at end of file +prepareUserSettingsViews('/user/{' + path_pk_key + '}/settings/'); diff --git a/polemarch/static/templates/pmItems.html b/polemarch/static/templates/pmItems.html index ce0aa47b..dba014e2 100644 --- a/polemarch/static/templates/pmItems.html +++ b/polemarch/static/templates/pmItems.html @@ -126,7 +126,19 @@