From 22f362aab36725e27a6bd434414395d4698e2085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=8D=83=E6=B5=81?= <40739051+jym503558564@users.noreply.github.com> Date: Tue, 21 May 2019 16:24:01 +0800 Subject: [PATCH] Dev csv (#2640) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Update] 封装JMSCSVRender和JMSCSVParser * [Update] 更改JMSCSVRender,根据请求参数控制导出csv的字段和下载csv模板的字段 * [Update] 导入空数据,提示错误消息 * [Update] 修改用户导入和导出功能代码 * [Update] 修改导入路由为动态反向解析 * [Update] 修改JMSCSVRender和JMSCSVParser以及用户导入导出代码 * [Update] 优化parsers逻辑 * [Update] 优化parsers csv代码结构 * [Update] 优化renders csv代码逻辑 * [Update] 删除parsers csv多余代码 * [Update] 删除parsers csv多余变量 * [Update] 优化renders csv代码结构 * [Update] 优化renders csv代码结构2 * [Update] 优化renders csv获取header逻辑 * [Update] 优化Cache Resources ID View逻辑 * [Update] 优化ViewSet IDCacheFilterMixin逻辑 * [Update] csv: parser render 添加异常捕获逻辑 * [Update] 删除多余代码 * [Update] 优化前端代码 * [Update] 修改小问题 * [Update] 修改前端导出用户的问题 * [Update] 前端 - 优化数据导出逻辑 APIExportData * [Update] 修复批量创建用户时发送created信号的bug * [Update] 优化导入时错误信息展示 * [Update] 优化parser、render时,对于多对多字段的处理 * [Update] 修改前端上传空文件问题 * [Update] 添加IDExportFilter,控制下载模版时的queryset * [Update] 修改判断导出模版时参数变量名 action => template * [Update] 修复导入用户数据时,用户组不生效的bug * [Update] 修改前端导入信息展示 * [Update] 抽象资源导入模版 * [Update] 优化资源导入模版 * [Update] 修改js设置url的params逻辑 * [Update] 修改users序列类控制read_only字段方式 * [Update] 资产列表采用新的导入/导出csv文件逻辑 * [Update] 修改导入资产时设置资产所在节点逻辑 * [Update] 添加用户组导入/导出功能 * [Update] 修改前端变量名 * [Update] 修改下载导入模版,不包含org字段 * [Update] 增加管理用户导入/导出功能 * [Update] 导入模版提供id字段(为了资源备份后导入直接使用); 修复资源导入时联合唯一字段不校验导致创建时报错的bug * [Update] 增加系统用户导入/导出功能 * [Update] 排序资源导入/导出字段 * [Update] 翻译导入/导出的字段和模版 * [Update] 更改csv导出和导出模版数据的控制在render实现 * [Update] 资产添加 更新导入 功能 * [Update] 用户/用户组/管理用户/系统用户/ 添加导入更新 * [Update] 翻译 * [Update] 优化资源序列化中的label * [Update] 去掉资源IDInFilterMixin过滤 * [Update] 翻译 --- apps/assets/api/admin_user.py | 4 +- apps/assets/api/asset.py | 22 +- apps/assets/api/system_user.py | 3 +- apps/assets/serializers/admin_user.py | 24 +- apps/assets/serializers/asset.py | 29 +- apps/assets/serializers/system_user.py | 34 +- .../assets/_admin_user_import_modal.html | 6 + .../assets/_admin_user_update_modal.html | 4 + .../templates/assets/_asset_import_modal.html | 33 +- .../templates/assets/_asset_update_modal.html | 4 + .../assets/_system_user_import_modal.html | 6 + .../assets/_system_user_update_modal.html | 4 + .../templates/assets/admin_user_list.html | 107 ++- apps/assets/templates/assets/asset_list.html | 237 ++++-- .../templates/assets/system_user_list.html | 99 +++ apps/assets/views/asset.py | 6 +- apps/common/api.py | 23 +- apps/common/const.py | 1 + apps/common/mixins.py | 30 +- apps/common/parsers/__init__.py | 1 + apps/common/parsers/csv.py | 101 +++ apps/common/renders/__init__.py | 1 + apps/common/renders/csv.py | 71 ++ apps/common/urls/__init__.py | 0 apps/common/urls/api_urls.py | 13 + apps/jumpserver/settings.py | 10 + apps/jumpserver/urls.py | 1 + apps/locale/zh/LC_MESSAGES/django.mo | Bin 74215 -> 72271 bytes apps/locale/zh/LC_MESSAGES/django.po | 792 ++++++++++++++---- apps/orgs/mixins.py | 16 +- apps/orgs/utils.py | 6 + apps/static/js/jumpserver.js | 85 +- apps/templates/_import_modal.html | 28 + apps/templates/_modal.html | 26 +- apps/templates/_update_modal.html | 28 + apps/users/api/group.py | 4 +- apps/users/api/user.py | 16 +- apps/users/serializers/v1.py | 36 +- apps/users/signals_handler.py | 1 - .../users/_user_groups_import_modal.html | 6 + .../users/_user_groups_update_modal.html | 4 + .../templates/users/_user_import_modal.html | 32 +- .../templates/users/_user_update_modal.html | 4 + .../templates/users/user_group_list.html | 100 ++- apps/users/templates/users/user_list.html | 219 +++-- apps/users/views/user.py | 13 +- 46 files changed, 1868 insertions(+), 422 deletions(-) create mode 100644 apps/assets/templates/assets/_admin_user_import_modal.html create mode 100644 apps/assets/templates/assets/_admin_user_update_modal.html create mode 100644 apps/assets/templates/assets/_asset_update_modal.html create mode 100644 apps/assets/templates/assets/_system_user_import_modal.html create mode 100644 apps/assets/templates/assets/_system_user_update_modal.html create mode 100644 apps/common/parsers/__init__.py create mode 100644 apps/common/parsers/csv.py create mode 100644 apps/common/renders/__init__.py create mode 100644 apps/common/renders/csv.py create mode 100644 apps/common/urls/__init__.py create mode 100644 apps/common/urls/api_urls.py create mode 100644 apps/templates/_import_modal.html create mode 100644 apps/templates/_update_modal.html create mode 100644 apps/users/templates/users/_user_groups_import_modal.html create mode 100644 apps/users/templates/users/_user_groups_update_modal.html create mode 100644 apps/users/templates/users/_user_update_modal.html diff --git a/apps/assets/api/admin_user.py b/apps/assets/api/admin_user.py index f2229022f..e84d9731a 100644 --- a/apps/assets/api/admin_user.py +++ b/apps/assets/api/admin_user.py @@ -20,7 +20,7 @@ from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet from rest_framework.pagination import LimitOffsetPagination -from common.mixins import IDInFilterMixin +from common.mixins import IDInCacheFilterMixin from common.utils import get_logger from ..hands import IsOrgAdmin from ..models import AdminUser, Asset @@ -36,7 +36,7 @@ __all__ = [ ] -class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet): +class AdminUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): """ Admin user api set, for add,delete,update,list,retrieve resource """ diff --git a/apps/assets/api/asset.py b/apps/assets/api/asset.py index a734806fb..9e21c81be 100644 --- a/apps/assets/api/asset.py +++ b/apps/assets/api/asset.py @@ -16,8 +16,9 @@ from django.urls import reverse_lazy from django.core.cache import cache from django.db.models import Q -from common.mixins import IDInFilterMixin -from common.utils import get_logger +from common.mixins import IDInCacheFilterMixin + +from common.utils import get_logger, get_object_or_none from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX from ..models import Asset, AdminUser, Node @@ -35,7 +36,7 @@ __all__ = [ ] -class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): +class AssetViewSet(IDInCacheFilterMixin, LabelFilter, BulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ @@ -47,6 +48,19 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): pagination_class = LimitOffsetPagination permission_classes = (IsOrgAdminOrAppUser,) + def set_assets_node(self, assets): + if not isinstance(assets, list): + assets = [assets] + node = Node.objects.get(value='Default') + node_id = self.request.query_params.get('node_id') + if node_id: + node = get_object_or_none(Node, pk=node_id) + node.assets.add(*assets) + + def perform_create(self, serializer): + assets = serializer.save() + self.set_assets_node(assets) + def filter_node(self, queryset): node_id = self.request.query_params.get("node_id") if not node_id: @@ -89,7 +103,7 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): return queryset -class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): +class AssetListUpdateApi(IDInCacheFilterMixin, ListBulkCreateUpdateDestroyAPIView): """ Asset bulk update api """ diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index 9805872e7..f6398e974 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -21,6 +21,7 @@ from rest_framework.pagination import LimitOffsetPagination from common.utils import get_logger from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser +from common.mixins import IDInCacheFilterMixin from ..models import SystemUser, Asset from .. import serializers from ..tasks import push_system_user_to_assets_manual, \ @@ -38,7 +39,7 @@ __all__ = [ ] -class SystemUserViewSet(BulkModelViewSet): +class SystemUserViewSet(IDInCacheFilterMixin, BulkModelViewSet): """ System user api set, for add,delete,update,list,retrieve resource """ diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index e44679995..66f25db87 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # from django.core.cache import cache +from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from common.serializers import AdaptedBulkListSerializer @@ -15,14 +16,29 @@ class AdminUserSerializer(serializers.ModelSerializer): """ 管理用户 """ - assets_amount = serializers.SerializerMethodField() - unreachable_amount = serializers.SerializerMethodField() - reachable_amount = serializers.SerializerMethodField() + password = serializers.CharField( + required=False, write_only=True, label=_('Password') + ) + unreachable_amount = serializers.SerializerMethodField(label=_('Unreachable')) + assets_amount = serializers.SerializerMethodField(label=_('Asset')) + reachable_amount = serializers.SerializerMethodField(label=_('Reachable')) class Meta: list_serializer_class = AdaptedBulkListSerializer model = AdminUser - fields = '__all__' + fields = [ + 'id', 'org_id', 'name', 'username', 'assets_amount', + 'reachable_amount', 'unreachable_amount', 'password', 'comment', + 'date_created', 'date_updated', 'become', 'become_method', + 'become_user', 'created_by', + ] + + extra_kwargs = { + 'date_created': {'label': _('Date created')}, + 'date_updated': {'label': _('Date updated')}, + 'become': {'read_only': True}, 'become_method': {'read_only': True}, + 'become_user': {'read_only': True}, 'created_by': {'read_only': True} + } def get_field_names(self, declared_fields, info): fields = super().get_field_names(declared_fields, info) diff --git a/apps/assets/serializers/asset.py b/apps/assets/serializers/asset.py index c0f435adc..3e1cf39bb 100644 --- a/apps/assets/serializers/asset.py +++ b/apps/assets/serializers/asset.py @@ -2,6 +2,9 @@ # from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from orgs.mixins import OrgResourceSerializerMixin from common.mixins import BulkSerializerMixin from common.serializers import AdaptedBulkListSerializer from ..models import Asset @@ -13,15 +16,35 @@ __all__ = [ ] -class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): +class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResourceSerializerMixin): """ 资产的数据结构 """ class Meta: model = Asset list_serializer_class = AdaptedBulkListSerializer - fields = '__all__' - validators = [] + # validators = [] # 解决批量导入时unique_together字段校验失败 + fields = [ + 'id', 'org_id', 'org_name', 'ip', 'hostname', 'protocol', 'port', + 'platform', 'is_active', 'public_ip', 'domain', 'admin_user', + 'nodes', 'labels', 'number', 'vendor', 'model', 'sn', + 'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', + 'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', + 'hostname_raw', 'comment', 'created_by', 'date_created', + 'hardware_info', 'connectivity' + ] + read_only_fields = ( + 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', + 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', + 'os', 'os_version', 'os_arch', 'hostname_raw', + 'created_by', 'date_created', + ) + extra_kwargs = { + 'hardware_info': {'label': _('Hardware info')}, + 'connectivity': {'label': _('Connectivity')}, + 'org_name': {'label': _('Org name')} + + } @classmethod def setup_eager_loading(cls, queryset): diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index c737f8cbe..88ae6eb38 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -1,5 +1,7 @@ from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + from common.serializers import AdaptedBulkListSerializer from ..models import SystemUser, Asset @@ -10,16 +12,36 @@ class SystemUserSerializer(serializers.ModelSerializer): """ 系统用户 """ - unreachable_amount = serializers.SerializerMethodField() - reachable_amount = serializers.SerializerMethodField() - unreachable_assets = serializers.SerializerMethodField() - reachable_assets = serializers.SerializerMethodField() - assets_amount = serializers.SerializerMethodField() + password = serializers.CharField( + required=False, write_only=True, label=_('Password') + ) + unreachable_amount = serializers.SerializerMethodField( + label=_('Unreachable') + ) + unreachable_assets = serializers.SerializerMethodField( + label=_('Unreachable assets') + ) + reachable_assets = serializers.SerializerMethodField( + label=_('Reachable assets') + ) + reachable_amount = serializers.SerializerMethodField(label=_('Reachable')) + assets_amount = serializers.SerializerMethodField(label=_('Asset')) class Meta: model = SystemUser - exclude = ('_password', '_private_key', '_public_key') list_serializer_class = AdaptedBulkListSerializer + fields = [ + 'id', 'org_id', 'name', 'username', 'login_mode', + 'login_mode_display', 'priority', 'protocol', 'auto_push', + 'password', 'assets_amount', 'reachable_amount', 'reachable_assets', + 'unreachable_amount', 'unreachable_assets', 'cmd_filters', 'sudo', + 'shell', 'comment', 'nodes', 'assets' + ] + extra_kwargs = { + 'login_mode_display': {'label': _('Login mode display')}, + 'created_by': {'read_only': True}, 'nodes': {'read_only': True}, + 'assets': {'read_only': True} + } def get_field_names(self, declared_fields, info): fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) diff --git a/apps/assets/templates/assets/_admin_user_import_modal.html b/apps/assets/templates/assets/_admin_user_import_modal.html new file mode 100644 index 000000000..a4afc1a14 --- /dev/null +++ b/apps/assets/templates/assets/_admin_user_import_modal.html @@ -0,0 +1,6 @@ +{% extends '_import_modal.html' %} +{% load i18n %} + +{% block modal_title%}{% trans "Import admin user" %}{% endblock %} + +{% block import_modal_download_template_url %}{% url "api-assets:admin-user-list" %}{% endblock %} diff --git a/apps/assets/templates/assets/_admin_user_update_modal.html b/apps/assets/templates/assets/_admin_user_update_modal.html new file mode 100644 index 000000000..9af051dd2 --- /dev/null +++ b/apps/assets/templates/assets/_admin_user_update_modal.html @@ -0,0 +1,4 @@ +{% extends '_update_modal.html' %} +{% load i18n %} + +{% block modal_title%}{% trans "Update admin user" %}{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/_asset_import_modal.html b/apps/assets/templates/assets/_asset_import_modal.html index ca7729e05..2460cb053 100644 --- a/apps/assets/templates/assets/_asset_import_modal.html +++ b/apps/assets/templates/assets/_asset_import_modal.html @@ -1,29 +1,6 @@ -{% extends '_modal.html' %} +{% extends '_import_modal.html' %} {% load i18n %} -{% block modal_id %}asset_import_modal{% endblock %} -{% block modal_title%}{% trans "Import asset" %}{% endblock %} -{% block modal_body %} -
- {% csrf_token %} -
- - {% trans 'Download' %} -
-
- - - - {% trans 'If set id, will use this id update asset existed' %} - -
-
-

-

-

-

-

-

-

-

-{% endblock %} -{% block modal_confirm_id %}btn_asset_import{% endblock %} + +{% block modal_title%}{% trans "Import assets" %}{% endblock %} + +{% block import_modal_download_template_url %}{% url "api-assets:asset-list" %}{% endblock %} diff --git a/apps/assets/templates/assets/_asset_update_modal.html b/apps/assets/templates/assets/_asset_update_modal.html new file mode 100644 index 000000000..68b2ff8db --- /dev/null +++ b/apps/assets/templates/assets/_asset_update_modal.html @@ -0,0 +1,4 @@ +{% extends '_update_modal.html' %} +{% load i18n %} + +{% block modal_title%}{% trans "Update assets" %}{% endblock %} diff --git a/apps/assets/templates/assets/_system_user_import_modal.html b/apps/assets/templates/assets/_system_user_import_modal.html new file mode 100644 index 000000000..b8687d696 --- /dev/null +++ b/apps/assets/templates/assets/_system_user_import_modal.html @@ -0,0 +1,6 @@ +{% extends '_import_modal.html' %} +{% load i18n %} + +{% block modal_title%}{% trans "Import system user" %}{% endblock %} + +{% block import_modal_download_template_url %}{% url "api-assets:system-user-list" %}{% endblock %} diff --git a/apps/assets/templates/assets/_system_user_update_modal.html b/apps/assets/templates/assets/_system_user_update_modal.html new file mode 100644 index 000000000..9e2920e6a --- /dev/null +++ b/apps/assets/templates/assets/_system_user_update_modal.html @@ -0,0 +1,4 @@ +{% extends '_update_modal.html' %} +{% load i18n %} + +{% block modal_title%}{% trans "Update system user" %}{% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/admin_user_list.html b/apps/assets/templates/assets/admin_user_list.html index 605e89060..c182ba4c4 100644 --- a/apps/assets/templates/assets/admin_user_list.html +++ b/apps/assets/templates/assets/admin_user_list.html @@ -1,8 +1,5 @@ {% extends '_base_list.html' %} {% load i18n static %} -{% block table_search %} -{% endblock %} - {% block help_message %}
{# 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#} @@ -12,6 +9,30 @@ {% trans 'You can set any one for Windows or other hardware.' %}
{% endblock %} +{% block table_search %} +
+
+ + +
+
+{% endblock %} {% block table_container %}
@@ -36,6 +57,8 @@ + {% include 'assets/_admin_user_import_modal.html' %} + {% include 'assets/_admin_user_update_modal.html' %} {% endblock %} {% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} @@ -107,6 +130,82 @@ $(document).ready(function(){ $data_table.ajax.reload(); }, 3000); -}); +}) +.on('click', '.btn_export', function(){ + var data_table = $('#admin_user_list_table').DataTable(); + var rows = data_table.rows('.selected').data(); + var admin_users = []; + $.each(rows, function (index, obj) { + admin_users.push(obj.id) + }); + var data = { + 'resources': admin_users + }; + var search = $("input[type='search']").val(); + var props = { + method: "POST", + body: JSON.stringify(data), + success_url: "{% url 'api-assets:admin-user-list' %}", + format: "csv", + params: { + search: search + } + }; + APIExportData(props); +}).on('click', '#btn_import_confirm',function () { + var url = "{% url 'api-assets:admin-user-list' %}"; + var file = document.getElementById('id_file').files[0]; + if(!file){ + toastr.error("{% trans "Please select file" %}"); + return + } + var data_table = $('#admin_user_list_table').DataTable(); + APIImportData({ + url: url, + method: "POST", + body: file, + data_table: data_table + }); +}) +.on('click', '#download_update_template', function () { + var $data_table = $('#admin_user_list_table').DataTable(); + var rows = $data_table.rows('.selected').data(); + + var admin_users = []; + $.each(rows, function (index, obj) { + admin_users.push(obj.id) + }); + + var data = { + 'resources': admin_users + }; + var search = $("input[type='search']").val(); + var props = { + method: "POST", + body: JSON.stringify(data), + success_url: "{% url 'api-assets:admin-user-list' %}?format=csv&template=update", + format: 'csv', + params: { + search: search + } + }; + APIExportData(props); +}) +.on('click', '#btn_update_confirm', function () { + var file = document.getElementById('update_file').files[0]; + if(!file){ + toastr.error("{% trans "Please select file" %}"); + return + } + var url = "{% url 'api-assets:admin-user-list' %}"; + var data_table = $('#admin_user_list_table').DataTable(); + + APIImportData({ + url: url, + method: "PUT", + body: file, + data_table: data_table + }); +}) {% endblock %} diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index aa27de7a8..ea74e25d1 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -67,14 +67,26 @@
{% trans "Create asset" %}
-
-
- - {% trans "Import" %} - - - {% trans "Export" %} - +
+
@@ -140,7 +152,7 @@ {#
  • {% trans 'Refresh' %}
  • #}
    - +{% include 'assets/_asset_update_modal.html' %} {% include 'assets/_asset_import_modal.html' %} {% include 'assets/_asset_list_modal.html' %} {% endblock %} @@ -464,42 +476,85 @@ $(document).ready(function(){ $.each(rows, function (index, obj) { assets.push(obj.id) }); - $.ajax({ - url: "{% url "assets:asset-export" %}", - method: 'POST', - data: JSON.stringify({assets_id: assets, node_id: current_node_id}), - dataType: "json", - success: function (data, textStatus) { - window.open(data.redirect) - }, - error: function () { - toastr.error('Export failed'); + + var data = { + 'resources': assets + }; + var search = $("input[type='search']").val(); + var props = { + method: "POST", + body: JSON.stringify(data), + success_url: "{% url 'api-assets:asset-list' %}", + format: 'csv', + params: { + search: search, + node_id: current_node_id || '' } - }) + }; + APIExportData(props); }) -.on('click', '#btn_asset_import', function () { - var $form = $('#fm_asset_import'); - var action = $form.attr("action"); +.on('click', '#btn_import_confirm', function () { + var file = document.getElementById('id_file').files[0]; + if(!file){ + toastr.error("{% trans "Please select file" %}"); + return + } + var url = "{% url 'api-assets:asset-list' %}"; if (current_node_id){ - action = setUrlParam(action, 'node_id', current_node_id); - $form.attr("action", action) + url = setUrlParam(url, 'node_id', current_node_id); } - $form.find('.help-block').remove(); - function success (data) { - if (data.valid === false) { - $('', {class: 'help-block text-danger'}).html(data.msg).insertAfter($('#id_assets')); - } else { - $('#id_created').html(data.created_info); - $('#id_created_detail').html(data.created.join(', ')); - $('#id_updated').html(data.updated_info); - $('#id_updated_detail').html(data.updated.join(', ')); - $('#id_failed').html(data.failed_info); - $('#id_failed_detail').html(data.failed.join(', ')); - var $data_table = $('#asset_list_table').DataTable(); - $data_table.ajax.reload(); + var data_table = $('#asset_list_table').DataTable(); + + APIImportData({ + url: url, + method: "POST", + body: file, + data_table: data_table + }); +}) +.on('click', '#download_update_template', function () { + var $data_table = $('#asset_list_table').DataTable(); + var rows = $data_table.rows('.selected').data(); + + var assets = []; + $.each(rows, function (index, obj) { + assets.push(obj.id) + }); + + var data = { + 'resources': assets + }; + var search = $("input[type='search']").val(); + var props = { + method: "POST", + body: JSON.stringify(data), + success_url: "{% url 'api-assets:asset-list' %}?format=csv&template=update", + format: 'csv', + params: { + search: search, + node_id: current_node_id || '' } + }; + APIExportData(props); +}) +.on('click', '#btn_update_confirm', function () { + var file = document.getElementById('update_file').files[0]; + if(!file){ + toastr.error("{% trans "Please select file" %}"); + return } - $form.ajaxSubmit({success: success}); + var url = "{% url 'api-assets:asset-list' %}"; + if (current_node_id){ + url = setUrlParam(url, 'node_id', current_node_id); + } + var data_table = $('#asset_list_table').DataTable(); + + APIImportData({ + url: url, + method: "PUT", + body: file, + data_table: data_table + }); }) .on('click', '.btn-create-asset', function () { var url = "{% url 'assets:asset-create' %}"; @@ -593,6 +648,12 @@ $(document).ready(function(){ return false; } var the_url = "{% url 'api-assets:asset-list' %}"; + var data = { + 'resources': id_list + }; + function refreshTag() { + $('#asset_list_table').DataTable().ajax.reload(); + } function doDeactive() { var data = []; @@ -601,7 +662,8 @@ $(document).ready(function(){ data.push(obj); }); function success() { - asset_table.ajax.reload() + setTimeout( function () { + window.location.reload();}, 500); } APIUpdateAttr({ url: the_url, @@ -617,7 +679,8 @@ $(document).ready(function(){ data.push(obj); }); function success() { - asset_table.ajax.reload() + setTimeout( function () { + window.location.reload();}, 300); } APIUpdateAttr({ url: the_url, @@ -636,68 +699,72 @@ $(document).ready(function(){ confirmButtonColor: "#DD6B55", confirmButtonText: "{% trans 'Confirm' %}", closeOnConfirm: false - }, function() { - var success = function() { + },function () { + function success(data) { + url = setUrlParam(the_url, 'spm', data.spm); + APIUpdateAttr({ + url:url, + method:'DELETE', + success:refreshTag, + flash_message:false, + }); var msg = "{% trans 'Asset Deleted.' %}"; swal("{% trans 'Asset Delete' %}", msg, "success"); - $('#asset_list_table').DataTable().ajax.reload(); - }; - var fail = function() { + } + function fail() { var msg = "{% trans 'Asset Deleting failed.' %}"; swal("{% trans 'Asset Delete' %}", msg, "error"); - }; - var url_delete = the_url + '?id__in=' + JSON.stringify(id_list); + } APIUpdateAttr({ - url: url_delete, - method: 'DELETE', - success: success, - error: fail - }); - $data_table.ajax.reload(); - jumpserver.checked = false; - }); + url: "{% url 'api-common:resources-cache' %}", + method:'POST', + body:JSON.stringify(data), + success:success, + error:fail + }) + }) } + function doUpdate() { - var data = { - 'assets_id':id_list - }; - function error(data) { - toastr.error(JSON.parse(data).error) + function fail(data) { + toastr.error(JSON.parse(data)) } function success(data) { - location.href = data.url; + var url = "{% url 'assets:asset-bulk-update' %}"; + location.href= setUrlParam(url, 'spm', data.spm); } APIUpdateAttr({ - 'url': "{% url 'api-assets:asset-bulk-update-select' %}", - 'method': 'POST', - 'body': JSON.stringify(data), - 'flash_message': false, - 'success': success, - 'error': error, + url: "{% url 'api-common:resources-cache' %}", + method:'POST', + body:JSON.stringify(data), + flash_message:false, + success:success, + error:fail }) - } + } function doRemove() { - var nodes = zTree.getSelectedNodes(); - if (!current_node_id) { - return - } + var nodes = zTree.getSelectedNodes(); + if (!current_node_id) { + return + } - var data = { - 'assets': id_list - }; + var data = { + 'assets': id_list + }; - var success = function () { - asset_table.ajax.reload() - }; + var success = function () { + asset_table.ajax.reload() + }; - APIUpdateAttr({ - 'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/', - 'method': 'PUT', - 'body': JSON.stringify(data), - 'success': success - }) + APIUpdateAttr({ + 'url': '/api/assets/v1/nodes/' + current_node_id + '/assets/remove/', + 'method': 'PUT', + 'body': JSON.stringify(data), + 'success': success + }) } + switch(action) { case 'deactive': doDeactive(); diff --git a/apps/assets/templates/assets/system_user_list.html b/apps/assets/templates/assets/system_user_list.html index b31039a46..16a0fd11c 100644 --- a/apps/assets/templates/assets/system_user_list.html +++ b/apps/assets/templates/assets/system_user_list.html @@ -14,6 +14,28 @@ {% endblock %} {% block table_search %} +
    + +
    {% endblock %} {% block table_container %} @@ -41,6 +63,8 @@ + {% include 'assets/_system_user_import_modal.html' %} + {% include 'assets/_system_user_update_modal.html' %} {% endblock %} {% block custom_foot_js %} {% endblock %} diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index 96bc451f0..653fa12c3 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -27,7 +27,9 @@ from django.contrib.messages.views import SuccessMessageMixin from common.mixins import JSONResponseMixin from common.utils import get_object_or_none, get_logger from common.permissions import AdminUserRequiredMixin -from common.const import create_success_msg, update_success_msg +from common.const import ( + create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID +) from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX from orgs.utils import current_org from .. import forms @@ -122,7 +124,7 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView): def get(self, request, *args, **kwargs): spm = request.GET.get('spm', '') - assets_id = cache.get(CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX.format(spm)) + assets_id = cache.get(KEY_CACHE_RESOURCES_ID.format(spm)) if kwargs.get('form'): self.form = kwargs['form'] elif assets_id: diff --git a/apps/common/api.py b/apps/common/api.py index 269d493d0..4f5f8da30 100644 --- a/apps/common/api.py +++ b/apps/common/api.py @@ -3,10 +3,18 @@ import os import uuid -from rest_framework.views import Response -from rest_framework import generics, serializers from django.core.cache import cache +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import generics, serializers + +from .const import KEY_CACHE_RESOURCES_ID + +__all__ = [ + 'LogTailApi', 'ResourcesIDCacheApi', +] + class OutputSerializer(serializers.Serializer): output = serializers.CharField() @@ -68,3 +76,14 @@ class LogTailApi(generics.RetrieveAPIView): data, end, new_mark = self.read_from_file() return Response({"data": data, 'end': end, 'mark': new_mark}) + + +class ResourcesIDCacheApi(APIView): + + def post(self, request, *args, **kwargs): + spm = str(uuid.uuid4()) + resources_id = request.data.get('resources') + if resources_id: + cache_key = KEY_CACHE_RESOURCES_ID.format(spm) + cache.set(cache_key, resources_id, 300) + return Response({'spm': spm}) diff --git a/apps/common/const.py b/apps/common/const.py index 018177d89..72d92da81 100644 --- a/apps/common/const.py +++ b/apps/common/const.py @@ -7,3 +7,4 @@ create_success_msg = _("%(name)s was created successfully") update_success_msg = _("%(name)s was updated successfully") FILE_END_GUARD = ">>> Content End <<<" celery_task_pre_key = "CELERY_" +KEY_CACHE_RESOURCES_ID = "RESOURCES_ID_{}" diff --git a/apps/common/mixins.py b/apps/common/mixins.py index a5e9a58d3..8e4af26dd 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -3,12 +3,15 @@ from django.db import models from django.http import JsonResponse from django.utils import timezone +from django.core.cache import cache from django.utils.translation import ugettext_lazy as _ from rest_framework.utils import html from rest_framework.settings import api_settings from rest_framework.exceptions import ValidationError from rest_framework.fields import SkipField +from .const import KEY_CACHE_RESOURCES_ID + class NoDeleteQuerySet(models.query.QuerySet): @@ -65,6 +68,27 @@ class IDInFilterMixin(object): return queryset +class IDInCacheFilterMixin(object): + + def filter_queryset(self, queryset): + queryset = super(IDInCacheFilterMixin, self).filter_queryset(queryset) + spm = self.request.query_params.get('spm') + cache_key = KEY_CACHE_RESOURCES_ID.format(spm) + resources_id = cache.get(cache_key) + if resources_id and isinstance(resources_id, list): + queryset = queryset.filter(id__in=resources_id) + return queryset + + +class IDExportFilterMixin(object): + def filter_queryset(self, queryset): + # 下载导入模版 + if self.request.query_params.get('template') == 'import': + return [] + else: + return super(IDExportFilterMixin, self).filter_queryset(queryset) + + class BulkSerializerMixin(object): """ Become rest_framework_bulk not support uuid as a primary key @@ -131,7 +155,11 @@ class BulkListSerializerMixin(object): for item in data: try: # prepare child serializer to only handle one instance - self.child.instance = self.instance.get(id=item['id']) if self.instance else None + if 'id' in item.keys(): + self.child.instance = self.instance.get(id=item['id']) if self.instance else None + if 'pk' in item.keys(): + self.child.instance = self.instance.get(id=item['pk']) if self.instance else None + self.child.initial_data = item # raw validated = self.child.run_validation(item) diff --git a/apps/common/parsers/__init__.py b/apps/common/parsers/__init__.py new file mode 100644 index 000000000..671c86586 --- /dev/null +++ b/apps/common/parsers/__init__.py @@ -0,0 +1 @@ +from .csv import * \ No newline at end of file diff --git a/apps/common/parsers/csv.py b/apps/common/parsers/csv.py new file mode 100644 index 000000000..b536a0f73 --- /dev/null +++ b/apps/common/parsers/csv.py @@ -0,0 +1,101 @@ +# ~*~ coding: utf-8 ~*~ +# + +import json +import unicodecsv + +from rest_framework.parsers import BaseParser +from rest_framework.exceptions import ParseError + +from ..utils import get_logger + +logger = get_logger(__file__) + + +class JMSCSVParser(BaseParser): + """ + Parses CSV file to serializer data + """ + + media_type = 'text/csv' + + @staticmethod + def _universal_newlines(stream): + """ + 保证在`通用换行模式`下打开文件 + """ + for line in stream.splitlines(): + yield line + + @staticmethod + def _gen_rows(csv_data, charset='utf-8', **kwargs): + csv_reader = unicodecsv.reader(csv_data, encoding=charset, **kwargs) + for row in csv_reader: + if not any(row): # 空行 + continue + yield row + + @staticmethod + def _get_fields_map(serializer): + fields_map = {} + fields = serializer.get_fields() + fields_map.update({v.label: k for k, v in fields.items()}) + fields_map.update({k: k for k, _ in fields.items()}) + return fields_map + + @staticmethod + def _process_row(row): + """ + 构建json数据前的行处理 + """ + _row = [] + for col in row: + # 列表转换 + if isinstance(col, str) and col.find("[") != -1 and col.find("]") != -1: + # 替换中文格式引号 + col = col.replace("“", '"').replace("”", '"').\ + replace("‘", '"').replace('’', '"').replace("'", '"') + col = json.loads(col) + _row.append(col) + return _row + + @staticmethod + def _process_row_data(row_data): + """ + 构建json数据后的行数据处理 + """ + _row_data = {} + for k, v in row_data.items(): + if isinstance(v, list) \ + or isinstance(v, str) and k.strip() and v.strip(): + _row_data[k] = v + return _row_data + + def parse(self, stream, media_type=None, parser_context=None): + parser_context = parser_context or {} + encoding = parser_context.get('encoding', 'utf-8') + try: + serializer = parser_context["view"].get_serializer() + except Exception as e: + logger.debug(e, exc_info=True) + raise ParseError('The resource does not support imports!') + + try: + stream_data = stream.read() + binary = self._universal_newlines(stream_data) + rows = self._gen_rows(binary, charset=encoding) + + header = next(rows) + fields_map = self._get_fields_map(serializer) + header = [fields_map.get(name, '') for name in header] + + data = [] + for row in rows: + row = self._process_row(row) + row_data = dict(zip(header, row)) + row_data = self._process_row_data(row_data) + data.append(row_data) + return data + except Exception as e: + logger.debug(e, exc_info=True) + raise ParseError('CSV parse error!') diff --git a/apps/common/renders/__init__.py b/apps/common/renders/__init__.py new file mode 100644 index 000000000..671c86586 --- /dev/null +++ b/apps/common/renders/__init__.py @@ -0,0 +1 @@ +from .csv import * \ No newline at end of file diff --git a/apps/common/renders/csv.py b/apps/common/renders/csv.py new file mode 100644 index 000000000..cec857ddb --- /dev/null +++ b/apps/common/renders/csv.py @@ -0,0 +1,71 @@ +# ~*~ coding: utf-8 ~*~ +# + +import unicodecsv + +from six import BytesIO +from rest_framework.renderers import BaseRenderer +from rest_framework.utils import encoders, json + +from ..utils import get_logger + +logger = get_logger(__file__) + + +class JMSCSVRender(BaseRenderer): + + media_type = 'text/csv' + format = 'csv' + + @staticmethod + def _get_header(fields, template): + if template == 'import': + header = [ + k for k, v in fields.items() + if not v.read_only and k != 'org_id' + ] + elif template == 'update': + header = [k for k, v in fields.items() if not v.read_only] + else: + # template in ['export'] + header = [k for k, v in fields.items() if not v.write_only] + return header + + @staticmethod + def _gen_table(data, header, labels=None): + labels = labels or {} + yield [labels.get(k, k) for k in header] + + for item in data: + row = [item.get(key) for key in header] + yield row + + def render(self, data, media_type=None, renderer_context=None): + renderer_context = renderer_context or {} + encoding = renderer_context.get('encoding', 'utf-8') + request = renderer_context['request'] + template = request.query_params.get('template', 'export') + view = renderer_context['view'] + data = json.loads(json.dumps(data, cls=encoders.JSONEncoder)) + if template == 'import': + data = [data[0]] if data else data + + try: + serializer = view.get_serializer() + except Exception as e: + logger.debug(e, exc_info=True) + value = 'The resource not support export!'.encode('utf-8') + else: + fields = serializer.get_fields() + header = self._get_header(fields, template) + labels = {k: v.label for k, v in fields.items() if v.label} + table = self._gen_table(data, header, labels) + + csv_buffer = BytesIO() + csv_writer = unicodecsv.writer(csv_buffer, encoding=encoding) + for row in table: + csv_writer.writerow(row) + + value = csv_buffer.getvalue() + + return value diff --git a/apps/common/urls/__init__.py b/apps/common/urls/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/urls/api_urls.py b/apps/common/urls/api_urls.py new file mode 100644 index 000000000..01f164b00 --- /dev/null +++ b/apps/common/urls/api_urls.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# + +from django.urls import path + +from .. import api + +app_name = 'common' + +urlpatterns = [ + path('resources/cache/', + api.ResourcesIDCacheApi.as_view(), name='resources-cache'), +] diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index a6ec9c92a..bdc71ae3c 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -363,6 +363,16 @@ REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ( 'common.permissions.IsOrgAdmin', ), + 'DEFAULT_RENDERER_CLASSES': ( + 'rest_framework.renderers.JSONRenderer', + 'rest_framework.renderers.BrowsableAPIRenderer', + 'common.renders.JMSCSVRender', + ), + 'DEFAULT_PARSER_CLASSES': ( + 'rest_framework.parsers.JSONParser', + 'rest_framework.parsers.FormParser', + 'common.parsers.JMSCSVParser' + ), 'DEFAULT_AUTHENTICATION_CLASSES': ( # 'rest_framework.authentication.BasicAuthentication', 'authentication.backends.api.AccessKeyAuthentication', diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index a2538b6c0..96c702acc 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -20,6 +20,7 @@ api_v1 = [ path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')), path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')), + path('common/v1/', include('common.urls.api_urls', namespace='api-common')), path('applications/v1/', include('applications.urls.api_urls', namespace='api-applications')), ] diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index b8c64ffce52e56ef3698928f7ac974fed7ffb69f..820dc398eb1f58f52cc32addee4b8e37cd6a0cf8 100644 GIT binary patch delta 22687 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index b1f1774ef..97df573d9 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -621,7 +621,7 @@ msgstr "管理用户" #: assets/forms/asset.py:33 assets/forms/asset.py:69 assets/forms/asset.py:109 #: assets/templates/assets/asset_create.html:36 #: assets/templates/assets/asset_create.html:38 -#: assets/templates/assets/asset_list.html:81 +#: assets/templates/assets/asset_list.html:93 #: assets/templates/assets/asset_update.html:41 #: assets/templates/assets/asset_update.html:43 #: assets/templates/assets/user_asset_list.html:33 @@ -680,6 +680,37 @@ msgstr "如果有多个的互相隔离的网络,设置资产属于的网域, msgid "Select assets" msgstr "选择资产" +#: assets/forms/domain.py:15 assets/forms/label.py:13 +#: assets/models/asset.py:279 assets/models/authbook.py:27 +#: assets/serializers/admin_user.py:31 assets/serializers/system_user.py:32 +#: assets/templates/assets/admin_user_list.html:49 +#: assets/templates/assets/domain_detail.html:60 +#: assets/templates/assets/domain_list.html:26 +#: assets/templates/assets/label_list.html:16 +#: assets/templates/assets/system_user_list.html:55 audits/models.py:19 +#: audits/templates/audits/ftp_log_list.html:41 +#: audits/templates/audits/ftp_log_list.html:71 perms/forms.py:42 +#: perms/models.py:50 +#: perms/templates/perms/asset_permission_create_update.html:45 +#: perms/templates/perms/asset_permission_list.html:56 +#: perms/templates/perms/asset_permission_list.html:125 +#: terminal/backends/command/models.py:13 terminal/models.py:155 +#: terminal/templates/terminal/command_list.html:40 +#: terminal/templates/terminal/command_list.html:73 +#: terminal/templates/terminal/session_list.html:41 +#: terminal/templates/terminal/session_list.html:72 +#: xpack/plugins/change_auth_plan/forms.py:114 +#: xpack/plugins/change_auth_plan/models.py:409 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:46 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:54 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:13 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:14 +#: xpack/plugins/cloud/models.py:187 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:63 +#: xpack/plugins/orgs/templates/orgs/org_list.html:16 +msgid "Asset" +msgstr "资产" + #: assets/forms/domain.py:51 msgid "Password should not contain special characters" msgstr "不能包含特殊字符" @@ -688,16 +719,65 @@ msgstr "不能包含特殊字符" msgid "SSH gateway support proxy SSH,RDP,VNC" msgstr "SSH网关,支持代理SSH,RDP和VNC" +#: assets/forms/domain.py:73 assets/forms/user.py:84 assets/forms/user.py:146 +#: assets/models/base.py:26 assets/models/cluster.py:18 +#: assets/models/cmd_filter.py:20 assets/models/domain.py:20 +#: assets/models/group.py:20 assets/models/label.py:18 +#: assets/templates/assets/admin_user_detail.html:56 +#: assets/templates/assets/admin_user_list.html:47 +#: assets/templates/assets/cmd_filter_detail.html:61 +#: assets/templates/assets/cmd_filter_list.html:24 +#: assets/templates/assets/domain_detail.html:56 +#: assets/templates/assets/domain_gateway_list.html:67 +#: assets/templates/assets/domain_list.html:25 +#: assets/templates/assets/label_list.html:14 +#: assets/templates/assets/system_user_detail.html:58 +#: assets/templates/assets/system_user_list.html:51 ops/models/adhoc.py:37 +#: ops/templates/ops/task_detail.html:60 ops/templates/ops/task_list.html:27 +#: orgs/models.py:12 perms/models.py:17 perms/models.py:47 +#: perms/templates/perms/asset_permission_detail.html:62 +#: perms/templates/perms/asset_permission_list.html:53 +#: perms/templates/perms/asset_permission_list.html:72 +#: perms/templates/perms/asset_permission_user.html:54 settings/models.py:29 +#: settings/templates/settings/_ldap_list_users_modal.html:38 +#: settings/templates/settings/command_storage_create.html:41 +#: settings/templates/settings/replay_storage_create.html:44 +#: settings/templates/settings/terminal_setting.html:80 +#: settings/templates/settings/terminal_setting.html:102 terminal/models.py:22 +#: terminal/models.py:241 terminal/templates/terminal/terminal_detail.html:43 +#: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 +#: users/models/user.py:61 users/templates/users/_select_user_modal.html:13 +#: users/templates/users/user_detail.html:63 +#: users/templates/users/user_group_detail.html:55 +#: users/templates/users/user_group_list.html:35 +#: users/templates/users/user_list.html:35 +#: users/templates/users/user_profile.html:51 +#: users/templates/users/user_pubkey_update.html:53 +#: xpack/plugins/change_auth_plan/forms.py:97 +#: xpack/plugins/change_auth_plan/models.py:58 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:61 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:12 +#: xpack/plugins/cloud/models.py:49 xpack/plugins/cloud/models.py:119 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:50 +#: xpack/plugins/cloud/templates/cloud/account_list.html:12 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:53 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:12 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:52 +#: xpack/plugins/orgs/templates/orgs/org_list.html:12 +msgid "Name" +msgstr "名称" + #: assets/forms/domain.py:74 assets/forms/user.py:85 assets/forms/user.py:147 #: assets/models/base.py:27 #: assets/templates/assets/_asset_user_auth_modal.html:15 #: assets/templates/assets/_asset_user_view_auth_modal.html:31 #: assets/templates/assets/admin_user_detail.html:60 -#: assets/templates/assets/admin_user_list.html:27 + +#: assets/templates/assets/admin_user_list.html:48 #: assets/templates/assets/asset_asset_user_list.html:44 #: assets/templates/assets/domain_gateway_list.html:71 #: assets/templates/assets/system_user_detail.html:62 -#: assets/templates/assets/system_user_list.html:30 audits/models.py:94 +#: assets/templates/assets/system_user_list.html:52 audits/models.py:94 #: audits/templates/audits/login_log_list.html:51 authentication/forms.py:11 #: authentication/templates/authentication/login.html:64 #: authentication/templates/authentication/new_login.html:90 @@ -707,7 +787,7 @@ msgstr "SSH网关,支持代理SSH,RDP和VNC" #: settings/templates/settings/_ldap_list_users_modal.html:37 users/forms.py:13 #: users/models/user.py:59 users/templates/users/_select_user_modal.html:14 #: users/templates/users/user_detail.html:67 -#: users/templates/users/user_list.html:24 +#: users/templates/users/user_list.html:36 #: users/templates/users/user_profile.html:47 #: xpack/plugins/change_auth_plan/forms.py:99 #: xpack/plugins/change_auth_plan/models.py:60 @@ -724,7 +804,8 @@ msgid "Password or private key passphrase" msgstr "密码或密钥密码" #: assets/forms/user.py:26 assets/models/base.py:28 -#: assets/serializers/asset_user.py:19 +#: assets/serializers/admin_user.py:20 assets/serializers/asset_user.py:19 +#: assets/serializers/system_user.py:16 #: assets/templates/assets/_asset_user_auth_modal.html:21 #: assets/templates/assets/_asset_user_view_auth_modal.html:37 #: authentication/forms.py:13 @@ -792,7 +873,7 @@ msgstr "使用逗号分隔多个命令,如: /bin/whoami,/sbin/ifconfig" #: assets/templates/assets/_asset_list_modal.html:46 #: assets/templates/assets/admin_user_assets.html:49 #: assets/templates/assets/asset_detail.html:64 -#: assets/templates/assets/asset_list.html:93 +#: assets/templates/assets/asset_list.html:105 #: assets/templates/assets/domain_gateway_list.html:68 #: assets/templates/assets/system_user_asset.html:51 #: assets/templates/assets/user_asset_list.html:45 @@ -810,7 +891,7 @@ msgstr "IP" #: assets/templates/assets/_asset_user_view_auth_modal.html:25 #: assets/templates/assets/admin_user_assets.html:48 #: assets/templates/assets/asset_detail.html:60 -#: assets/templates/assets/asset_list.html:92 +#: assets/templates/assets/asset_list.html:104 #: assets/templates/assets/system_user_asset.html:50 #: assets/templates/assets/user_asset_list.html:44 #: assets/templates/assets/user_asset_list.html:162 @@ -826,7 +907,7 @@ msgstr "主机名" #: assets/models/user.py:136 assets/templates/assets/asset_detail.html:76 #: assets/templates/assets/domain_gateway_list.html:70 #: assets/templates/assets/system_user_detail.html:70 -#: assets/templates/assets/system_user_list.html:31 +#: assets/templates/assets/system_user_list.html:53 #: assets/templates/assets/user_asset_list.html:165 #: terminal/templates/terminal/session_list.html:75 msgid "Protocol" @@ -926,19 +1007,96 @@ msgstr "主机名原始" msgid "Labels" msgstr "标签管理" +#: assets/models/asset.py:109 assets/models/base.py:34 +#: assets/models/cluster.py:28 assets/models/cmd_filter.py:25 +#: assets/models/cmd_filter.py:58 assets/models/group.py:21 +#: assets/templates/assets/admin_user_detail.html:68 +#: assets/templates/assets/asset_detail.html:128 +#: assets/templates/assets/cmd_filter_detail.html:77 +#: assets/templates/assets/domain_detail.html:72 +#: assets/templates/assets/system_user_detail.html:100 +#: ops/templates/ops/adhoc_detail.html:86 orgs/models.py:15 perms/models.py:57 +#: perms/models.py:110 perms/templates/perms/asset_permission_detail.html:98 +#: users/models/user.py:102 users/serializers/v1.py:69 +#: users/templates/users/user_detail.html:111 +#: xpack/plugins/change_auth_plan/models.py:103 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:113 +#: xpack/plugins/cloud/models.py:55 xpack/plugins/cloud/models.py:127 +msgid "Created by" +msgstr "创建者" + +#: assets/models/asset.py:110 assets/models/cluster.py:26 +#: assets/models/domain.py:23 assets/models/group.py:22 +#: assets/models/label.py:25 assets/serializers/admin_user.py:23 +#: assets/templates/assets/admin_user_detail.html:64 +#: assets/templates/assets/cmd_filter_detail.html:69 +#: assets/templates/assets/domain_detail.html:68 +#: assets/templates/assets/system_user_detail.html:96 +#: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:64 +#: orgs/models.py:16 perms/models.py:58 perms/models.py:111 +#: perms/templates/perms/asset_permission_detail.html:94 +#: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17 +#: users/templates/users/user_group_detail.html:63 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:105 +#: xpack/plugins/cloud/models.py:56 xpack/plugins/cloud/models.py:128 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:66 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:77 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:60 +msgid "Date created" +msgstr "创建日期" + +#: assets/models/asset.py:111 assets/models/base.py:31 +#: assets/models/cluster.py:29 assets/models/cmd_filter.py:22 +#: assets/models/cmd_filter.py:55 assets/models/domain.py:21 +#: assets/models/domain.py:53 assets/models/group.py:23 +#: assets/models/label.py:23 assets/templates/assets/admin_user_detail.html:72 +#: assets/templates/assets/admin_user_list.html:53 +#: assets/templates/assets/asset_detail.html:136 +#: assets/templates/assets/cmd_filter_detail.html:65 +#: assets/templates/assets/cmd_filter_list.html:27 +#: assets/templates/assets/cmd_filter_rule_list.html:62 +#: assets/templates/assets/domain_detail.html:76 +#: assets/templates/assets/domain_gateway_list.html:72 +#: assets/templates/assets/domain_list.html:28 +#: assets/templates/assets/system_user_detail.html:104 +#: assets/templates/assets/system_user_list.html:59 +#: assets/templates/assets/user_asset_list.html:171 ops/models/adhoc.py:43 +#: orgs/models.py:17 perms/models.py:59 perms/models.py:112 +#: perms/templates/perms/asset_permission_detail.html:102 settings/models.py:34 +#: terminal/models.py:32 terminal/templates/terminal/terminal_detail.html:63 +#: users/models/group.py:15 users/models/user.py:94 +#: users/templates/users/user_detail.html:127 +#: users/templates/users/user_group_detail.html:67 +#: users/templates/users/user_group_list.html:37 +#: users/templates/users/user_profile.html:134 +#: xpack/plugins/change_auth_plan/models.py:99 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:117 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:19 +#: xpack/plugins/cloud/models.py:54 xpack/plugins/cloud/models.py:125 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:70 +#: xpack/plugins/cloud/templates/cloud/account_list.html:15 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:69 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:16 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:64 +#: xpack/plugins/orgs/templates/orgs/org_list.html:22 +msgid "Comment" +msgstr "备注" + #: assets/models/asset.py:117 assets/models/base.py:38 -#: assets/templates/assets/admin_user_list.html:30 -#: assets/templates/assets/system_user_list.html:35 +#: assets/serializers/admin_user.py:29 assets/serializers/system_user.py:22 +#: assets/templates/assets/admin_user_list.html:51 +#: assets/templates/assets/system_user_list.html:57 msgid "Unreachable" msgstr "不可达" #: assets/models/asset.py:118 assets/models/base.py:39 +#: assets/serializers/admin_user.py:32 assets/serializers/system_user.py:31 #: assets/templates/assets/admin_user_assets.html:51 -#: assets/templates/assets/admin_user_list.html:29 +#: assets/templates/assets/admin_user_list.html:50 +#: assets/templates/assets/asset_list.html:107 #: assets/templates/assets/asset_asset_user_list.html:46 -#: assets/templates/assets/asset_list.html:95 #: assets/templates/assets/system_user_asset.html:53 -#: assets/templates/assets/system_user_list.html:34 +#: assets/templates/assets/system_user_list.html:56 #: users/templates/users/user_group_granted_asset.html:47 msgid "Reachable" msgstr "可连接" @@ -1085,6 +1243,42 @@ msgstr "内容" msgid "One line one command" msgstr "每行一个命令" +#: assets/models/cmd_filter.py:54 +#: assets/templates/assets/admin_user_assets.html:52 +#: assets/templates/assets/admin_user_list.html:54 +#: assets/templates/assets/asset_list.html:108 +#: assets/templates/assets/asset_asset_user_list.html:48 +#: assets/templates/assets/cmd_filter_list.html:28 +#: assets/templates/assets/cmd_filter_rule_list.html:63 +#: assets/templates/assets/domain_gateway_list.html:73 +#: assets/templates/assets/domain_list.html:29 +#: assets/templates/assets/label_list.html:17 +#: assets/templates/assets/system_user_asset.html:54 +#: assets/templates/assets/system_user_list.html:60 +#: assets/templates/assets/user_asset_list.html:48 audits/models.py:38 +#: audits/templates/audits/operate_log_list.html:41 +#: audits/templates/audits/operate_log_list.html:67 +#: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:64 +#: ops/templates/ops/task_history.html:65 ops/templates/ops/task_list.html:34 +#: perms/forms.py:51 perms/models.py:21 perms/models.py:53 +#: perms/templates/perms/asset_permission_create_update.html:50 +#: perms/templates/perms/asset_permission_list.html:60 +#: perms/templates/perms/asset_permission_list.html:134 +#: settings/templates/settings/terminal_setting.html:82 +#: settings/templates/settings/terminal_setting.html:104 +#: terminal/templates/terminal/session_list.html:81 +#: terminal/templates/terminal/terminal_list.html:36 +#: users/templates/users/user_group_list.html:38 +#: users/templates/users/user_list.html:41 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_list.html:60 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_execution_subtask_list.html:18 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:20 +#: xpack/plugins/cloud/templates/cloud/account_list.html:16 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:18 +#: xpack/plugins/orgs/templates/orgs/org_list.html:23 +msgid "Action" +msgstr "动作" + #: assets/models/cmd_filter.py:64 msgid "Command filter rule" msgstr "命令过滤规则" @@ -1125,9 +1319,9 @@ msgstr "默认资产组" #: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/session_list.html:33 #: terminal/templates/terminal/session_list.html:71 users/forms.py:283 -#: users/models/user.py:36 users/models/user.py:467 +#: users/models/user.py:36 users/models/user.py:467 users/serializers/v1.py:61 #: users/templates/users/user_group_detail.html:78 -#: users/templates/users/user_group_list.html:13 users/views/user.py:395 +#: users/templates/users/user_group_list.html:36 users/views/user.py:395 #: xpack/plugins/orgs/forms.py:26 #: xpack/plugins/orgs/templates/orgs/org_detail.html:113 #: xpack/plugins/orgs/templates/orgs/org_list.html:14 @@ -1174,7 +1368,7 @@ msgid "Shell" msgstr "Shell" #: assets/models/user.py:140 assets/templates/assets/system_user_detail.html:66 -#: assets/templates/assets/system_user_list.html:32 +#: assets/templates/assets/system_user_list.html:54 msgid "Login mode" msgstr "登录模式" @@ -1183,6 +1377,25 @@ msgstr "登录模式" msgid "%(value)s is not an even number" msgstr "%(value)s is not an even number" +#: assets/serializers/admin_user.py:26 +#: assets/templates/assets/asset_asset_user_list.html:51 +#: assets/templates/assets/cmd_filter_detail.html:73 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:109 +msgid "Date updated" +msgstr "更新日期" + +#: assets/serializers/asset.py:20 +msgid "Org name" +msgstr "组织名" + +#: assets/serializers/asset.py:22 +msgid "Hardware info" +msgstr "硬件信息" + +#: assets/serializers/asset.py:25 +msgid "Connectivity" +msgstr "连接" + #: assets/serializers/asset_user.py:23 users/forms.py:230 #: users/models/user.py:91 users/templates/users/first_login.html:42 #: users/templates/users/user_password_update.html:46 @@ -1192,6 +1405,18 @@ msgstr "%(value)s is not an even number" msgid "Public key" msgstr "ssh公钥" +#: assets/serializers/system_user.py:19 +msgid "Login mode display" +msgstr "登录模式显示" + +#: assets/serializers/system_user.py:25 +msgid "Unreachable assets" +msgstr "不可达资产" + +#: assets/serializers/system_user.py:29 +msgid "Reachable assets" +msgstr "可连接资产" + #: assets/tasks.py:31 msgid "Asset has been disabled, skipped: {}" msgstr "资产或许不支持ansible, 跳过: {}" @@ -1261,6 +1486,15 @@ msgstr "推送系统用户到入资产: {} => {}" msgid "Test asset user connectivity: {}" msgstr "测试资产用户可连接性: {}" +#: assets/templates/assets/_admin_user_import_modal.html:4 +msgid "Import admin user" +msgstr "导入管理用户" + +#: assets/templates/assets/_admin_user_update_modal.html:4 +#: assets/views/admin_user.py:64 +msgid "Update admin user" +msgstr "更新管理用户" + #: assets/templates/assets/_asset_group_bulk_update_modal.html:5 msgid "Update asset group" msgstr "更新用户组" @@ -1290,32 +1524,18 @@ msgid "Enable-MFA" msgstr "启用MFA" #: assets/templates/assets/_asset_import_modal.html:4 -msgid "Import asset" +msgid "Import assets" msgstr "导入资产" -#: assets/templates/assets/_asset_import_modal.html:9 -#: users/templates/users/_user_import_modal.html:10 -msgid "Template" -msgstr "模板" - -#: assets/templates/assets/_asset_import_modal.html:10 -#: users/templates/users/_user_import_modal.html:11 -msgid "Download" -msgstr "下载" - -#: assets/templates/assets/_asset_import_modal.html:13 -msgid "Asset csv file" -msgstr "资产csv文件" - -#: assets/templates/assets/_asset_import_modal.html:16 -msgid "If set id, will use this id update asset existed" -msgstr "如果设置了id,则会使用该行信息更新该id的资产" - #: assets/templates/assets/_asset_list_modal.html:7 assets/views/asset.py:52 #: templates/_nav.html:22 xpack/plugins/change_auth_plan/views.py:110 msgid "Asset list" msgstr "资产列表" +#: assets/templates/assets/_asset_update_modal.html:4 +msgid "Update assets" +msgstr "更新资产" + #: assets/templates/assets/_asset_user_auth_modal.html:4 msgid "Update asset user auth" msgstr "更新资产用户认证信息" @@ -1425,6 +1645,84 @@ msgstr "自动生成密钥" msgid "Other" msgstr "其它" +#: assets/templates/assets/_system_user.html:75 +#: assets/templates/assets/admin_user_create_update.html:45 +#: assets/templates/assets/asset_bulk_update.html:23 +#: assets/templates/assets/asset_create.html:67 +#: assets/templates/assets/asset_update.html:71 +#: assets/templates/assets/cmd_filter_create_update.html:15 +#: assets/templates/assets/cmd_filter_rule_create_update.html:40 +#: assets/templates/assets/domain_create_update.html:16 +#: assets/templates/assets/gateway_create_update.html:58 +#: assets/templates/assets/label_create_update.html:18 +#: perms/templates/perms/asset_permission_create_update.html:83 +#: settings/templates/settings/basic_setting.html:61 +#: settings/templates/settings/command_storage_create.html:79 +#: settings/templates/settings/email_setting.html:62 +#: settings/templates/settings/ldap_setting.html:61 +#: settings/templates/settings/replay_storage_create.html:152 +#: settings/templates/settings/security_setting.html:70 +#: settings/templates/settings/terminal_setting.html:68 +#: terminal/templates/terminal/terminal_update.html:45 +#: users/templates/users/_user.html:50 +#: users/templates/users/user_bulk_update.html:23 +#: users/templates/users/user_detail.html:176 +#: users/templates/users/user_password_update.html:71 +#: users/templates/users/user_profile.html:204 +#: users/templates/users/user_profile_update.html:63 +#: users/templates/users/user_pubkey_update.html:70 +#: users/templates/users/user_pubkey_update.html:76 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:71 +#: xpack/plugins/cloud/templates/cloud/account_create_update.html:33 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:35 +#: xpack/plugins/interface/templates/interface/interface.html:72 +msgid "Reset" +msgstr "重置" + +#: assets/templates/assets/_system_user.html:76 +#: assets/templates/assets/admin_user_create_update.html:46 +#: assets/templates/assets/asset_bulk_update.html:24 +#: assets/templates/assets/asset_create.html:68 +#: assets/templates/assets/asset_list.html:125 +#: assets/templates/assets/asset_update.html:72 +#: assets/templates/assets/cmd_filter_create_update.html:16 +#: assets/templates/assets/cmd_filter_rule_create_update.html:41 +#: assets/templates/assets/domain_create_update.html:17 +#: assets/templates/assets/gateway_create_update.html:59 +#: assets/templates/assets/label_create_update.html:19 +#: audits/templates/audits/login_log_list.html:89 +#: perms/templates/perms/asset_permission_create_update.html:84 +#: settings/templates/settings/basic_setting.html:62 +#: settings/templates/settings/command_storage_create.html:80 +#: settings/templates/settings/email_setting.html:63 +#: settings/templates/settings/ldap_setting.html:64 +#: settings/templates/settings/replay_storage_create.html:153 +#: settings/templates/settings/security_setting.html:71 +#: settings/templates/settings/terminal_setting.html:70 +#: terminal/templates/terminal/command_list.html:103 +#: terminal/templates/terminal/session_list.html:126 +#: terminal/templates/terminal/terminal_update.html:46 +#: users/templates/users/_user.html:51 +#: users/templates/users/forgot_password.html:42 +#: users/templates/users/user_bulk_update.html:24 +#: users/templates/users/user_list.html:57 +#: users/templates/users/user_password_update.html:72 +#: users/templates/users/user_profile_update.html:64 +#: users/templates/users/user_pubkey_update.html:77 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_create_update.html:72 +#: xpack/plugins/interface/templates/interface/interface.html:74 +msgid "Submit" +msgstr "提交" + +#: assets/templates/assets/_system_user_import_modal.html:4 +msgid "Import system user" +msgstr "导入系统用户" + +#: assets/templates/assets/_system_user_update_modal.html:4 +#: assets/views/system_user.py:61 +msgid "Update system user" +msgstr "更新系统用户" + #: assets/templates/assets/_user_asset_detail_modal.html:11 #: assets/templates/assets/asset_asset_user_list.html:13 #: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:187 @@ -1495,6 +1793,82 @@ msgstr "更新成功" msgid "Update failed!" msgstr "更新失败" +#: assets/templates/assets/admin_user_detail.html:24 +#: assets/templates/assets/admin_user_list.html:29 +#: assets/templates/assets/admin_user_list.html:111 +#: assets/templates/assets/asset_detail.html:27 +#: assets/templates/assets/asset_list.html:86 +#: assets/templates/assets/asset_list.html:190 +#: assets/templates/assets/cmd_filter_detail.html:29 +#: assets/templates/assets/cmd_filter_list.html:58 +#: assets/templates/assets/cmd_filter_rule_list.html:86 +#: assets/templates/assets/domain_detail.html:24 +#: assets/templates/assets/domain_detail.html:103 +#: assets/templates/assets/domain_gateway_list.html:97 +#: assets/templates/assets/domain_list.html:54 +#: assets/templates/assets/label_list.html:39 +#: assets/templates/assets/system_user_detail.html:26 +#: assets/templates/assets/system_user_list.html:33 +#: assets/templates/assets/system_user_list.html:117 audits/models.py:33 +#: perms/templates/perms/asset_permission_detail.html:30 +#: perms/templates/perms/asset_permission_list.html:181 +#: terminal/templates/terminal/terminal_detail.html:16 +#: terminal/templates/terminal/terminal_list.html:72 +#: users/templates/users/user_detail.html:25 +#: users/templates/users/user_group_detail.html:28 +#: users/templates/users/user_group_list.html:20 +#: users/templates/users/user_group_list.html:69 +#: users/templates/users/user_list.html:20 +#: users/templates/users/user_list.html:96 +#: users/templates/users/user_list.html:99 +#: users/templates/users/user_profile.html:177 +#: users/templates/users/user_profile.html:187 +#: users/templates/users/user_profile.html:196 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:29 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:55 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:23 +#: xpack/plugins/cloud/templates/cloud/account_list.html:39 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:25 +#: xpack/plugins/orgs/templates/orgs/org_list.html:87 +msgid "Update" +msgstr "更新" + +#: assets/templates/assets/admin_user_detail.html:28 +#: assets/templates/assets/admin_user_list.html:112 +#: assets/templates/assets/asset_detail.html:31 +#: assets/templates/assets/asset_list.html:191 +#: assets/templates/assets/cmd_filter_detail.html:33 +#: assets/templates/assets/cmd_filter_list.html:59 +#: assets/templates/assets/cmd_filter_rule_list.html:87 +#: assets/templates/assets/domain_detail.html:28 +#: assets/templates/assets/domain_detail.html:104 +#: assets/templates/assets/domain_gateway_list.html:98 +#: assets/templates/assets/domain_list.html:55 +#: assets/templates/assets/label_list.html:40 +#: assets/templates/assets/system_user_detail.html:30 +#: assets/templates/assets/system_user_list.html:118 audits/models.py:34 +#: ops/templates/ops/task_list.html:64 +#: perms/templates/perms/asset_permission_detail.html:34 +#: perms/templates/perms/asset_permission_list.html:182 +#: settings/templates/settings/terminal_setting.html:90 +#: settings/templates/settings/terminal_setting.html:112 +#: terminal/templates/terminal/terminal_list.html:74 +#: users/templates/users/user_detail.html:30 +#: users/templates/users/user_group_detail.html:32 +#: users/templates/users/user_group_list.html:71 +#: users/templates/users/user_list.html:104 +#: users/templates/users/user_list.html:108 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:33 +#: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_list.html:57 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:27 +#: xpack/plugins/cloud/templates/cloud/account_list.html:41 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_detail.html:30 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_list.html:55 +#: xpack/plugins/orgs/templates/orgs/org_detail.html:29 +#: xpack/plugins/orgs/templates/orgs/org_list.html:89 +msgid "Delete" +msgstr "删除" + #: assets/templates/assets/admin_user_detail.html:83 msgid "Replace node assets admin user with this" msgstr "替换资产的管理员" @@ -1506,35 +1880,94 @@ msgstr "替换资产的管理员" msgid "Select nodes" msgstr "选择节点" -#: assets/templates/assets/admin_user_list.html:10 +#: assets/templates/assets/admin_user_detail.html:100 +#: assets/templates/assets/asset_detail.html:211 +#: assets/templates/assets/asset_list.html:692 +#: assets/templates/assets/cmd_filter_detail.html:106 +#: assets/templates/assets/system_user_asset.html:112 +#: assets/templates/assets/system_user_detail.html:182 +#: assets/templates/assets/system_user_list.html:168 +#: settings/templates/settings/terminal_setting.html:165 +#: templates/_modal.html:23 terminal/templates/terminal/session_detail.html:108 +#: users/templates/users/user_detail.html:388 +#: users/templates/users/user_detail.html:414 +#: users/templates/users/user_detail.html:437 +#: users/templates/users/user_detail.html:482 +#: users/templates/users/user_group_create_update.html:32 +#: users/templates/users/user_group_list.html:114 +#: users/templates/users/user_list.html:260 +#: users/templates/users/user_profile.html:238 +#: xpack/plugins/cloud/templates/cloud/account_create_update.html:34 +#: xpack/plugins/cloud/templates/cloud/sync_instance_task_create.html:36 +#: xpack/plugins/interface/templates/interface/interface.html:103 +#: xpack/plugins/orgs/templates/orgs/org_create_update.html:33 +msgid "Confirm" +msgstr "确认" + +#: assets/templates/assets/admin_user_list.html:7 msgid "" "Admin users are asset (charged server) on the root, or have NOPASSWD: ALL " "sudo permissions users, " msgstr "" "管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户," -#: assets/templates/assets/admin_user_list.html:11 +#: assets/templates/assets/admin_user_list.html:8 msgid "" "Jumpserver users of the system using the user to `push system user`, `get " "assets hardware information`, etc. " msgstr "Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。" -#: assets/templates/assets/admin_user_list.html:12 +#: assets/templates/assets/admin_user_list.html:9 msgid "You can set any one for Windows or other hardware." msgstr "Windows或其它硬件可以随意设置一个" -#: assets/templates/assets/admin_user_list.html:18 +#: assets/templates/assets/admin_user_list.html:19 +#: assets/templates/assets/asset_list.html:76 +#: assets/templates/assets/system_user_list.html:23 +#: audits/templates/audits/login_log_list.html:85 +#: users/templates/users/user_group_list.html:10 +#: users/templates/users/user_list.html:10 +msgid "Export" +msgstr "导出" + +#: assets/templates/assets/admin_user_list.html:24 +#: assets/templates/assets/asset_list.html:81 +#: assets/templates/assets/system_user_list.html:28 +#: settings/templates/settings/_ldap_list_users_modal.html:97 +#: users/templates/users/user_group_list.html:15 +#: users/templates/users/user_list.html:15 +#: xpack/plugins/license/templates/license/license_detail.html:110 +msgid "Import" +msgstr "导入" + +#: assets/templates/assets/admin_user_list.html:39 #: assets/views/admin_user.py:48 msgid "Create admin user" msgstr "创建管理用户" -#: assets/templates/assets/admin_user_list.html:31 -#: assets/templates/assets/system_user_list.html:36 +#: assets/templates/assets/admin_user_list.html:52 +#: assets/templates/assets/system_user_list.html:58 #: ops/templates/ops/adhoc_history.html:54 #: ops/templates/ops/task_history.html:60 msgid "Ratio" msgstr "比例" +#: assets/templates/assets/admin_user_list.html:159 +#: assets/templates/assets/admin_user_list.html:197 +#: assets/templates/assets/asset_list.html:499 +#: assets/templates/assets/asset_list.html:543 +#: assets/templates/assets/system_user_list.html:226 +#: assets/templates/assets/system_user_list.html:262 +#: users/templates/users/user_group_list.html:163 +#: users/templates/users/user_group_list.html:199 +#: users/templates/users/user_list.html:162 +#: users/templates/users/user_list.html:198 +#, fuzzy +#| msgid "Please Select User" +msgid "Please select file" +msgstr "选择用户" + + #: assets/templates/assets/asset_asset_user_list.html:16 #: assets/templates/assets/asset_detail.html:23 assets/views/asset.py:68 msgid "Asset user list" @@ -1548,6 +1981,7 @@ msgstr "资产用户" msgid "Password version" msgstr "密码版本" + #: assets/templates/assets/asset_asset_user_list.html:47 #: assets/templates/assets/cmd_filter_detail.html:73 #: xpack/plugins/change_auth_plan/templates/change_auth_plan/plan_detail.html:109 @@ -1626,146 +2060,133 @@ msgstr "" msgid "Create asset" msgstr "创建资产" -#: assets/templates/assets/asset_list.html:73 -#: settings/templates/settings/_ldap_list_users_modal.html:97 -#: users/templates/users/user_list.html:7 -#: xpack/plugins/license/templates/license/license_detail.html:110 -msgid "Import" -msgstr "导入" - -#: assets/templates/assets/asset_list.html:76 -#: audits/templates/audits/login_log_list.html:85 -#: users/templates/users/user_list.html:10 -msgid "Export" -msgstr "导出" - -#: assets/templates/assets/asset_list.html:94 +#: assets/templates/assets/asset_list.html:106 msgid "Hardware" msgstr "硬件" -#: assets/templates/assets/asset_list.html:105 -#: users/templates/users/user_list.html:38 +#: assets/templates/assets/asset_list.html:117 +#: users/templates/users/user_list.html:50 msgid "Delete selected" msgstr "批量删除" -#: assets/templates/assets/asset_list.html:106 -#: users/templates/users/user_list.html:39 +#: assets/templates/assets/asset_list.html:118 +#: users/templates/users/user_list.html:51 msgid "Update selected" msgstr "批量更新" -#: assets/templates/assets/asset_list.html:107 +#: assets/templates/assets/asset_list.html:119 msgid "Remove from this node" msgstr "从节点移除" -#: assets/templates/assets/asset_list.html:108 -#: users/templates/users/user_list.html:40 +#: assets/templates/assets/asset_list.html:120 +#: users/templates/users/user_list.html:52 msgid "Deactive selected" msgstr "禁用所选" -#: assets/templates/assets/asset_list.html:109 -#: users/templates/users/user_list.html:41 +#: assets/templates/assets/asset_list.html:121 +#: users/templates/users/user_list.html:53 msgid "Active selected" msgstr "激活所选" -#: assets/templates/assets/asset_list.html:126 +#: assets/templates/assets/asset_list.html:138 msgid "Add node" msgstr "新建节点" -#: assets/templates/assets/asset_list.html:127 +#: assets/templates/assets/asset_list.html:139 msgid "Rename node" msgstr "重命名节点" -#: assets/templates/assets/asset_list.html:128 +#: assets/templates/assets/asset_list.html:140 msgid "Delete node" msgstr "删除节点" -#: assets/templates/assets/asset_list.html:130 +#: assets/templates/assets/asset_list.html:142 msgid "Add assets to node" msgstr "添加资产到节点" -#: assets/templates/assets/asset_list.html:131 +#: assets/templates/assets/asset_list.html:143 msgid "Move assets to node" msgstr "移动资产到节点" -#: assets/templates/assets/asset_list.html:133 +#: assets/templates/assets/asset_list.html:145 msgid "Refresh node hardware info" msgstr "更新节点资产硬件信息" -#: assets/templates/assets/asset_list.html:134 +#: assets/templates/assets/asset_list.html:146 msgid "Test node connective" msgstr "测试节点资产可连接性" -#: assets/templates/assets/asset_list.html:136 +#: assets/templates/assets/asset_list.html:148 msgid "Refresh all node assets amount" msgstr "刷新所有节点资产数量" -#: assets/templates/assets/asset_list.html:138 +#: assets/templates/assets/asset_list.html:150 msgid "Display only current node assets" msgstr "仅显示当前节点资产" -#: assets/templates/assets/asset_list.html:139 +#: assets/templates/assets/asset_list.html:151 msgid "Displays all child node assets" msgstr "显示所有子节点资产" -#: assets/templates/assets/asset_list.html:217 +#: assets/templates/assets/asset_list.html:229 msgid "Create node failed" msgstr "创建节点失败" -#: assets/templates/assets/asset_list.html:229 +#: assets/templates/assets/asset_list.html:241 msgid "Have child node, cancel" msgstr "存在子节点,不能删除" -#: assets/templates/assets/asset_list.html:231 +#: assets/templates/assets/asset_list.html:243 msgid "Have assets, cancel" msgstr "存在资产,不能删除" -#: assets/templates/assets/asset_list.html:302 +#: assets/templates/assets/asset_list.html:314 msgid "Rename success" msgstr "重命名成功" -#: assets/templates/assets/asset_list.html:303 +#: assets/templates/assets/asset_list.html:315 msgid "Rename failed, do not change the root node name" msgstr "重命名失败,不能更改root节点的名称" -#: assets/templates/assets/asset_list.html:631 -#: assets/templates/assets/system_user_list.html:138 +#: assets/templates/assets/asset_list.html:686 +#: assets/templates/assets/system_user_list.html:162 #: users/templates/users/user_detail.html:382 #: users/templates/users/user_detail.html:408 #: users/templates/users/user_detail.html:476 -#: users/templates/users/user_group_list.html:84 -#: users/templates/users/user_list.html:209 +#: users/templates/users/user_group_list.html:108 +#: users/templates/users/user_list.html:254 #: xpack/plugins/interface/templates/interface/interface.html:97 msgid "Are you sure?" msgstr "你确认吗?" -#: assets/templates/assets/asset_list.html:632 +#: assets/templates/assets/asset_list.html:687 msgid "This will delete the selected assets !!!" msgstr "删除选择资产" -#: assets/templates/assets/asset_list.html:635 -#: assets/templates/assets/system_user_list.html:142 +#: assets/templates/assets/asset_list.html:690 +#: assets/templates/assets/system_user_list.html:166 #: settings/templates/settings/terminal_setting.html:163 #: users/templates/users/user_detail.html:386 #: users/templates/users/user_detail.html:412 #: users/templates/users/user_detail.html:480 #: users/templates/users/user_group_create_update.html:31 -#: users/templates/users/user_group_list.html:88 -#: users/templates/users/user_list.html:213 +#: users/templates/users/user_group_list.html:112 +#: users/templates/users/user_list.html:258 #: xpack/plugins/interface/templates/interface/interface.html:101 #: xpack/plugins/orgs/templates/orgs/org_create_update.html:32 msgid "Cancel" msgstr "取消" -#: assets/templates/assets/asset_list.html:641 +#: assets/templates/assets/asset_list.html:696 msgid "Asset Deleted." msgstr "已被删除" -#: assets/templates/assets/asset_list.html:642 -#: assets/templates/assets/asset_list.html:647 +#: assets/templates/assets/asset_list.html:697 +#: assets/templates/assets/asset_list.html:702 msgid "Asset Delete" msgstr "删除" -#: assets/templates/assets/asset_list.html:646 +#: assets/templates/assets/asset_list.html:701 msgid "Asset Deleting failed." msgstr "删除失败" @@ -1948,25 +2369,25 @@ msgstr "" "资产中,如果资产(交换机、windows)不支持ansible, 请手动填写账号密码。目前还不" "支持Windows的自动推送" -#: assets/templates/assets/system_user_list.html:21 +#: assets/templates/assets/system_user_list.html:43 #: assets/views/system_user.py:45 msgid "Create system user" msgstr "创建系统用户" -#: assets/templates/assets/system_user_list.html:139 +#: assets/templates/assets/system_user_list.html:163 msgid "This will delete the selected System Users !!!" msgstr "删除选择系统用户" -#: assets/templates/assets/system_user_list.html:148 +#: assets/templates/assets/system_user_list.html:172 msgid "System Users Deleted." msgstr "已被删除" -#: assets/templates/assets/system_user_list.html:149 -#: assets/templates/assets/system_user_list.html:154 +#: assets/templates/assets/system_user_list.html:173 +#: assets/templates/assets/system_user_list.html:178 msgid "System Users Delete" msgstr "删除系统用户" -#: assets/templates/assets/system_user_list.html:153 +#: assets/templates/assets/system_user_list.html:177 msgid "System Users Deleting failed." msgstr "系统用户删除失败" @@ -1974,10 +2395,6 @@ msgstr "系统用户删除失败" msgid "Admin user list" msgstr "管理用户列表" -#: assets/views/admin_user.py:64 -msgid "Update admin user" -msgstr "更新管理用户" - #: assets/views/admin_user.py:79 assets/views/admin_user.py:103 msgid "Admin user detail" msgstr "管理用户详情" @@ -2058,10 +2475,6 @@ msgstr "更新标签" msgid "System user list" msgstr "系统用户列表" -#: assets/views/system_user.py:61 -msgid "Update system user" -msgstr "更新系统用户" - #: assets/views/system_user.py:75 msgid "System user detail" msgstr "系统用户详情" @@ -2551,11 +2964,11 @@ msgstr "" msgid "Encrypt field using Secret Key" msgstr "" -#: common/mixins.py:32 +#: common/mixins.py:35 msgid "is discard" msgstr "" -#: common/mixins.py:33 +#: common/mixins.py:36 msgid "discard time" msgstr "" @@ -2914,7 +3327,7 @@ msgstr "命令执行列表" msgid "Command execution" msgstr "命令执行" -#: orgs/mixins.py:77 orgs/models.py:24 +#: orgs/mixins.py:81 orgs/models.py:24 msgid "Organization" msgstr "组织管理" @@ -2940,7 +3353,7 @@ msgstr "下载文件" #: templates/_nav.html:14 users/forms.py:253 users/models/group.py:26 #: users/models/user.py:67 users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:213 -#: users/templates/users/user_list.html:26 +#: users/templates/users/user_list.html:38 #: xpack/plugins/orgs/templates/orgs/org_list.html:15 msgid "User group" msgstr "用户组" @@ -3065,9 +3478,9 @@ msgstr "创建授权规则" #: perms/templates/perms/asset_permission_list.html:59 #: perms/templates/perms/asset_permission_list.html:73 +#: users/templates/users/user_list.html:40 xpack/plugins/cloud/models.py:53 +#: xpack/plugins/cloud/templates/cloud/account_detail.html:60 #: perms/templates/perms/remote_app_permission_list.html:18 -#: users/templates/users/user_list.html:28 xpack/plugins/cloud/models.py:53 -#: xpack/plugins/cloud/templates/cloud/account_detail.html:58 #: xpack/plugins/cloud/templates/cloud/account_list.html:14 msgid "Validity" msgstr "有效" @@ -3700,6 +4113,18 @@ msgstr "注销登录" msgid "Dashboard" msgstr "仪表盘" +#: templates/_import_modal.html:12 +msgid "Download the imported template or use the exported CSV file format" +msgstr "下载导入的模板或使用导出的csv格式" + +#: templates/_import_modal.html:13 +msgid "Download the import template" +msgstr "下载导入模版" + +#: templates/_import_modal.html:17 templates/_update_modal.html:17 +msgid "Select the CSV file to import" +msgstr "请选择csv文件导入" + #: templates/_message.html:7 #, python-format msgid "" @@ -3840,6 +4265,14 @@ msgid "" "Displays the results of items _START_ to _END_; A total of _TOTAL_ entries" msgstr "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项" +#: templates/_update_modal.html:12 +msgid "Download the update template or use the exported CSV file format" +msgstr "下载更新的模板或使用导出的csv格式" + +#: templates/_update_modal.html:13 +msgid "Download the update template" +msgstr "下载更新模版" + #: templates/captcha/image.html:3 msgid "Play CAPTCHA as audio file" msgstr "语言播放验证码" @@ -4206,18 +4639,18 @@ msgid "" "You should use your ssh client tools connect terminal: {}

    {}" msgstr "你可以使用ssh客户端工具连接终端" -#: users/api/user.py:69 users/api/user.py:80 users/api/user.py:106 +#: users/api/user.py:77 users/api/user.py:88 users/api/user.py:114 msgid "You do not have permission." msgstr "你没有权限" -#: users/api/user.py:210 +#: users/api/user.py:218 msgid "Could not reset self otp, use profile reset instead" msgstr "不能再该页面重置MFA, 请去个人信息页面重置" #: users/forms.py:32 users/models/user.py:71 #: users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:87 -#: users/templates/users/user_list.html:25 +#: users/templates/users/user_list.html:37 #: users/templates/users/user_profile.html:55 msgid "Role" msgstr "角色" @@ -4242,7 +4675,7 @@ msgstr "添加到用户组" msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:89 users/forms.py:219 users/serializers/v1.py:38 +#: users/forms.py:89 users/forms.py:219 users/serializers/v1.py:53 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" @@ -4339,7 +4772,7 @@ msgid "Wechat" msgstr "微信" #: users/models/user.py:106 users/templates/users/user_detail.html:103 -#: users/templates/users/user_list.html:27 +#: users/templates/users/user_list.html:39 #: users/templates/users/user_profile.html:100 msgid "Source" msgstr "用户来源" @@ -4357,6 +4790,34 @@ msgstr "用户认证源来自 {}, 请去相应系统修改密码" msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" +#: users/serializers/v1.py:17 +msgid "Groups name" +msgstr "用户组名" + +#: users/serializers/v1.py:20 +msgid "Source name" +msgstr "用户来源名" + +#: users/serializers/v1.py:23 +msgid "Is first login" +msgstr "首次登录" + +#: users/serializers/v1.py:25 +msgid "Role name" +msgstr "角色名" + +#: users/serializers/v1.py:26 +msgid "Is valid" +msgstr "账户是否有效" + +#: users/serializers/v1.py:27 +msgid "Is expired" +msgstr " 是否过期" + +#: users/serializers/v1.py:28 +msgid "Avatar url" +msgstr "头像路径" + #: users/serializers_v2/user.py:36 msgid "name not unique" msgstr "名称重复" @@ -4393,21 +4854,22 @@ msgstr "资产数量" msgid "Security and Role" msgstr "角色安全" +#: users/templates/users/_user_groups_import_modal.html:4 +msgid "Import user groups" +msgstr "导入用户组" + +#: users/templates/users/_user_groups_update_modal.html:4 +msgid "Update user groups" +msgstr "更新用户组" + #: users/templates/users/_user_import_modal.html:4 -msgid "Import user" -msgstr "导入" +msgid "Import users" +msgstr "导入用户" -#: users/templates/users/_user_import_modal.html:6 -msgid "Download template or use export csv format" -msgstr "下载模板或使用导出的csv格式" - -#: users/templates/users/_user_import_modal.html:14 -msgid "Users csv file" -msgstr "用户csv文件" - -#: users/templates/users/_user_import_modal.html:16 -msgid "If set id, will use this id update user existed" -msgstr "如果设置了id,则会使用该行信息更新该id的用户" +#: users/templates/users/_user_update_modal.html:4 +#: users/templates/users/user_update.html:4 users/views/user.py:123 +msgid "Update user" +msgstr "更新用户" #: users/templates/users/_user_update_pk_modal.html:4 msgid "Update User SSH Public Key" @@ -4535,7 +4997,7 @@ msgid "Very strong" msgstr "很强" #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:16 users/views/user.py:83 +#: users/templates/users/user_list.html:28 users/views/user.py:83 msgid "Create user" msgstr "创建用户" @@ -4656,49 +5118,49 @@ msgstr "用户组详情" msgid "Add user" msgstr "添加用户" -#: users/templates/users/user_group_list.html:5 users/views/group.py:44 +#: users/templates/users/user_group_list.html:28 users/views/group.py:44 msgid "Create user group" msgstr "创建用户组" -#: users/templates/users/user_group_list.html:85 +#: users/templates/users/user_group_list.html:109 msgid "This will delete the selected groups !!!" msgstr "删除选择组" -#: users/templates/users/user_group_list.html:94 +#: users/templates/users/user_group_list.html:118 msgid "UserGroups Deleted." msgstr "用户组删除" -#: users/templates/users/user_group_list.html:95 -#: users/templates/users/user_group_list.html:100 +#: users/templates/users/user_group_list.html:119 +#: users/templates/users/user_group_list.html:124 msgid "UserGroups Delete" msgstr "用户组删除" -#: users/templates/users/user_group_list.html:99 +#: users/templates/users/user_group_list.html:123 msgid "UserGroup Deleting failed." msgstr "用户组删除失败" -#: users/templates/users/user_list.html:210 +#: users/templates/users/user_list.html:255 msgid "This will delete the selected users !!!" msgstr "删除选中用户 !!!" -#: users/templates/users/user_list.html:219 +#: users/templates/users/user_list.html:264 msgid "User Deleted." msgstr "已被删除" -#: users/templates/users/user_list.html:220 -#: users/templates/users/user_list.html:225 +#: users/templates/users/user_list.html:265 +#: users/templates/users/user_list.html:270 msgid "User Delete" msgstr "删除" -#: users/templates/users/user_list.html:224 +#: users/templates/users/user_list.html:269 msgid "User Deleting failed." msgstr "用户删除失败" -#: users/templates/users/user_list.html:260 +#: users/templates/users/user_list.html:305 msgid "User is expired" msgstr "用户已失效" -#: users/templates/users/user_list.html:263 +#: users/templates/users/user_list.html:308 msgid "User is inactive" msgstr "用户已禁用" @@ -4798,10 +5260,6 @@ msgid "" "corresponding private key." msgstr "新的公钥已设置成功,请下载对应的私钥" -#: users/templates/users/user_update.html:4 users/views/user.py:123 -msgid "Update user" -msgstr "更新用户" - #: users/utils.py:38 msgid "Create account successfully" msgstr "创建账户成功" @@ -5728,6 +6186,25 @@ msgstr "创建组织" msgid "Update org" msgstr "更新组织" + +#~ msgid "Template" +#~ msgstr "模板" + +#~ msgid "Download" +#~ msgstr "下载" + +#~ msgid "Asset csv file" +#~ msgstr "资产csv文件" + +#~ msgid "If set id, will use this id update asset existed" +#~ msgstr "如果设置了id,则会使用该行信息更新该id的资产" + +#~ msgid "Users csv file" +#~ msgstr "用户csv文件" + +#~ msgid "If set id, will use this id update user existed" +#~ msgstr "如果设置了id,则会使用该行信息更新该id的用户" + #~ msgid "MFA Confirm" #~ msgstr "确认" @@ -5747,6 +6224,7 @@ msgstr "更新组织" #~ msgid "Restore default successfully!" #~ msgstr "恢复默认成功!" + #~ msgid "Beijing Duizhan Tech, Inc." #~ msgstr "北京堆栈科技有限公司" @@ -5780,6 +6258,24 @@ msgstr "更新组织" #~ msgid "Invalid private key" #~ msgstr "ssh密钥不合法" +#, fuzzy +#~| msgid "CPU count" +#~ msgid "Cpu count" +#~ msgstr "CPU数量" + +#~ msgid "Login Jumpserver" +#~ msgstr "登录 Jumpserver" + +#, fuzzy +#~| msgid "Delete succeed" +#~ msgid "Delete success!" +#~ msgstr "删除成功" + +#, fuzzy +#~| msgid "Username does not exist" +#~ msgid "This license does not exist!" +#~ msgstr "用户名不存在" + #~ msgid "Valid" #~ msgstr "账户状态" diff --git a/apps/orgs/mixins.py b/apps/orgs/mixins.py index 5c4879204..431443ebe 100644 --- a/apps/orgs/mixins.py +++ b/apps/orgs/mixins.py @@ -8,9 +8,12 @@ from django.shortcuts import redirect, get_object_or_404 from django.forms import ModelForm from django.http.response import HttpResponseForbidden from django.core.exceptions import ValidationError +from rest_framework import serializers from common.utils import get_logger -from .utils import current_org, set_current_org, set_to_root_org +from .utils import ( + current_org, set_current_org, set_to_root_org, get_current_org_id +) from .models import Organization logger = get_logger(__file__) @@ -18,7 +21,8 @@ tl = Local() __all__ = [ 'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm', - 'RootOrgViewMixin', 'OrgMembershipSerializerMixin', 'OrgMembershipModelViewSetMixin' + 'RootOrgViewMixin', 'OrgMembershipSerializerMixin', + 'OrgMembershipModelViewSetMixin', 'OrgResourceSerializerMixin', ] @@ -202,3 +206,11 @@ class OrgMembershipModelViewSetMixin: def get_queryset(self): queryset = self.membership_class.objects.filter(organization=self.org) return queryset + + +class OrgResourceSerializerMixin(serializers.Serializer): + """ + 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id + (同时为serializer.is_valid()对Model的unique_together校验做准备) + """ + org_id = serializers.HiddenField(default=get_current_org_id) diff --git a/apps/orgs/utils.py b/apps/orgs/utils.py index 7898e0343..808536984 100644 --- a/apps/orgs/utils.py +++ b/apps/orgs/utils.py @@ -38,4 +38,10 @@ def get_current_org(): return _find('current_org') +def get_current_org_id(): + org = get_current_org() + org_id = str(org.id) if org.is_real() else '' + return org_id + + current_org = LocalProxy(partial(_find, 'current_org')) diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index b3a2bd35e..b452c353a 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -954,9 +954,92 @@ function rootNodeAddDom(ztree, callback) { }) } +function APIExportData(props) { + props = props || {}; + $.ajax({ + url: '/api/common/v1/resources/cache/', + type: props.method || "POST", + data: props.body, + contentType: props.content_type || "application/json; charset=utf-8", + dataType: props.data_type || "json", + success: function (data) { + var export_url = props.success_url; + var params = props.params || {}; + params['format'] = props.format; + params['spm'] = data.spm; + for (var k in params){ + export_url = setUrlParam(export_url, k, params[k]) + } + window.open(export_url); + }, + error: function () { + toastr.error('Export failed'); + } + }) +} + +function APIImportData(props){ + props = props || {}; + $.ajax({ + url: props.url, + type: props.method || "POST", + processData: false, + data: props.body, + contentType: props.content_type || 'text/csv', + success: function (data) { + if(props.method === 'POST'){ + $('#created_failed').html(''); + $('#created_failed_detail').html(''); + $('#success_created').html("Import Success"); + $('#success_created_detail').html("Count" + ": " + data.length); + }else{ + $('#updated_failed').html(''); + $('#updated_failed_detail').html(''); + $('#success_updated').html("Update Success"); + $('#success_updated_detail').html("Count" + ": " + data.length); + } + + props.data_table.ajax.reload() + }, + error: function (error) { + var data = error.responseJSON; + if (data instanceof Array){ + var html = ''; + var li = ''; + var err = ''; + $.each(data, function (index, item){ + err = ''; + for (var prop in item) { + err += prop + ": " + item[prop][0] + " " + } + if (err) { + li = "
  • " + "Line " + (++index) + ". " + err + "
  • "; + html += li + } + }); + html = "
      " + html + "
    " + } + else { + html = error.responseText + } + if(props.method === 'POST'){ + $('#success_created').html(''); + $('#success_created_detail').html(''); + $('#created_failed').html("Import failed"); + $('#created_failed_detail').html(html); + }else{ + $('#success_updated').html(''); + $('#success_updated_detail').html(''); + $('#updated_failed').html("Update failed"); + $('#updated_failed_detail').html(html); + } + } + }) +} + function htmlEscape ( d ) { return typeof d === 'string' ? d.replace(//g, '>').replace(/"/g, '"') : d; -} \ No newline at end of file +} diff --git a/apps/templates/_import_modal.html b/apps/templates/_import_modal.html new file mode 100644 index 000000000..01a1cdf72 --- /dev/null +++ b/apps/templates/_import_modal.html @@ -0,0 +1,28 @@ +{% extends '_modal.html' %} +{% load i18n %} + +{% block modal_id %}import_modal{% endblock %} + +{% block modal_confirm_id %}btn_import_confirm{% endblock %} + +{% block modal_body %} +
    + {% csrf_token %} +
    + + {% trans 'Download the import template' %} +
    + +
    + + +
    +
    + +
    +

    +

    +

    +

    +
    +{% endblock %} diff --git a/apps/templates/_modal.html b/apps/templates/_modal.html index 237e5618b..7b5f55de4 100644 --- a/apps/templates/_modal.html +++ b/apps/templates/_modal.html @@ -8,7 +8,7 @@
    + diff --git a/apps/templates/_update_modal.html b/apps/templates/_update_modal.html new file mode 100644 index 000000000..db2b14110 --- /dev/null +++ b/apps/templates/_update_modal.html @@ -0,0 +1,28 @@ +{% extends '_modal.html' %} +{% load i18n %} + +{% block modal_id %}update_modal{% endblock %} + +{% block modal_confirm_id %}btn_update_confirm{% endblock %} + +{% block modal_body %} +
    + {% csrf_token %} +
    + + {% trans 'Download the update template' %} +
    + +
    + + +
    +
    + +
    +

    +

    +

    +

    +
    +{% endblock %} diff --git a/apps/users/api/group.py b/apps/users/api/group.py index 8cf9fcb0e..fc9a84928 100644 --- a/apps/users/api/group.py +++ b/apps/users/api/group.py @@ -9,13 +9,13 @@ from ..serializers import UserGroupSerializer, \ UserGroupUpdateMemberSerializer from ..models import UserGroup from common.permissions import IsOrgAdmin -from common.mixins import IDInFilterMixin +from common.mixins import IDInCacheFilterMixin __all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi'] -class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet): +class UserGroupViewSet(IDInCacheFilterMixin, BulkModelViewSet): filter_fields = ("name",) search_fields = filter_fields queryset = UserGroup.objects.all() diff --git a/apps/users/api/user.py b/apps/users/api/user.py index 63c3dab12..c7668cb86 100644 --- a/apps/users/api/user.py +++ b/apps/users/api/user.py @@ -15,7 +15,7 @@ from rest_framework.pagination import LimitOffsetPagination from common.permissions import ( IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser ) -from common.mixins import IDInFilterMixin +from common.mixins import IDInCacheFilterMixin from common.utils import get_logger from orgs.utils import current_org from ..serializers import UserSerializer, UserPKUpdateSerializer, \ @@ -32,7 +32,7 @@ __all__ = [ ] -class UserViewSet(IDInFilterMixin, BulkModelViewSet): +class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet): filter_fields = ('username', 'email', 'name', 'id') search_fields = filter_fields queryset = User.objects.exclude(role=User.ROLE_APP) @@ -40,9 +40,15 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet): permission_classes = (IsOrgAdmin,) pagination_class = LimitOffsetPagination + def send_created_signal(self, users): + if not isinstance(users, list): + users = [users] + for user in users: + post_user_create.send(self.__class__, user=user) + def perform_create(self, serializer): - user = serializer.save() - post_user_create.send(self.__class__, user=user) + users = serializer.save() + self.send_created_signal(users) def get_queryset(self): queryset = current_org.get_org_users() @@ -213,4 +219,4 @@ class UserResetOTPApi(generics.RetrieveAPIView): user.otp_secret_key = '' user.save() logout(request) - return Response({"msg": "success"}) + return Response({"msg": "success"}) \ No newline at end of file diff --git a/apps/users/serializers/v1.py b/apps/users/serializers/v1.py index b8c91417d..a0a13d4fa 100644 --- a/apps/users/serializers/v1.py +++ b/apps/users/serializers/v1.py @@ -19,12 +19,21 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): list_serializer_class = AdaptedBulkListSerializer fields = [ 'id', 'name', 'username', 'email', 'groups', 'groups_display', - 'role', 'role_display', 'avatar_url', 'wechat', 'phone', - 'otp_level', 'comment', 'source', 'source_display', - 'is_valid', 'is_expired', 'is_active', - 'created_by', 'is_first_login', - 'date_password_last_updated', 'date_expired', + 'role', 'role_display', 'wechat', 'phone', 'otp_level', + 'comment', 'source', 'source_display', 'is_valid', 'is_expired', + 'is_active', 'created_by', 'is_first_login', + 'date_password_last_updated', 'date_expired', 'avatar_url', ] + extra_kwargs = { + 'groups_display': {'label': _('Groups name')}, + 'source_display': {'label': _('Source name')}, + 'is_first_login': {'label': _('Is first login'), 'read_only': True}, + 'role_display': {'label': _('Role name')}, + 'is_valid': {'label': _('Is valid')}, + 'is_expired': {'label': _('Is expired')}, + 'avatar_url': {'label': _('Avatar url')}, + 'created_by': {'read_only': True}, 'source': {'read_only': True} + } class UserPKUpdateSerializer(serializers.ModelSerializer): @@ -48,17 +57,20 @@ class UserUpdateGroupSerializer(serializers.ModelSerializer): class UserGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): - users = serializers.SerializerMethodField() + users = serializers.PrimaryKeyRelatedField( + required=False, many=True, queryset=User.objects.all(), label=_('User') + ) class Meta: model = UserGroup list_serializer_class = AdaptedBulkListSerializer - fields = '__all__' - read_only_fields = ['created_by'] - - @staticmethod - def get_users(obj): - return [user.name for user in obj.users.all()] + fields = [ + 'id', 'org_id', 'name', 'users', 'comment', 'date_created', + 'created_by', + ] + extra_kwargs = { + 'created_by': {'label': _('Created by'), 'read_only': True} + } class UserGroupUpdateMemberSerializer(serializers.ModelSerializer): diff --git a/apps/users/signals_handler.py b/apps/users/signals_handler.py index 1bc3ef430..4c6afc663 100644 --- a/apps/users/signals_handler.py +++ b/apps/users/signals_handler.py @@ -28,4 +28,3 @@ def on_user_create(sender, user=None, **kwargs): logger.info(" - Sending welcome mail ...".format(user.name)) if user.email: send_user_created_mail(user) - diff --git a/apps/users/templates/users/_user_groups_import_modal.html b/apps/users/templates/users/_user_groups_import_modal.html new file mode 100644 index 000000000..63d057215 --- /dev/null +++ b/apps/users/templates/users/_user_groups_import_modal.html @@ -0,0 +1,6 @@ +{% extends '_import_modal.html' %} +{% load i18n %} + +{% block modal_title%}{% trans "Import user groups" %}{% endblock %} + +{% block import_modal_download_template_url %}{% url "api-users:user-group-list" %}{% endblock %} \ No newline at end of file diff --git a/apps/users/templates/users/_user_groups_update_modal.html b/apps/users/templates/users/_user_groups_update_modal.html new file mode 100644 index 000000000..a07c0f82c --- /dev/null +++ b/apps/users/templates/users/_user_groups_update_modal.html @@ -0,0 +1,4 @@ +{% extends '_update_modal.html' %} +{% load i18n %} + +{% block modal_title%}{% trans "Update user group" %}{% endblock %} \ No newline at end of file diff --git a/apps/users/templates/users/_user_import_modal.html b/apps/users/templates/users/_user_import_modal.html index 678023cfc..e53d67fa7 100644 --- a/apps/users/templates/users/_user_import_modal.html +++ b/apps/users/templates/users/_user_import_modal.html @@ -1,28 +1,6 @@ -{% extends '_modal.html' %} +{% extends '_import_modal.html' %} {% load i18n %} -{% block modal_id %}user_import_modal{% endblock %} -{% block modal_title%}{% trans "Import user" %}{% endblock %} -{% block modal_body %} -

    {% trans "Download template or use export csv format" %}

    -
    - {% csrf_token %} -
    - - {% trans 'Download' %} -
    -
    - - - {% trans 'If set id, will use this id update user existed' %} -
    -
    -

    -

    -

    -

    -

    -

    -

    -

    -{% endblock %} -{% block modal_confirm_id %}btn_user_import{% endblock %} + +{% block modal_title%}{% trans "Import users" %}{% endblock %} + +{% block import_modal_download_template_url %}{% url "api-users:user-list" %}{% endblock %} diff --git a/apps/users/templates/users/_user_update_modal.html b/apps/users/templates/users/_user_update_modal.html new file mode 100644 index 000000000..9dfe60c96 --- /dev/null +++ b/apps/users/templates/users/_user_update_modal.html @@ -0,0 +1,4 @@ +{% extends '_update_modal.html' %} +{% load i18n %} + +{% block modal_title%}{% trans "Update user" %}{% endblock %} \ No newline at end of file diff --git a/apps/users/templates/users/user_group_list.html b/apps/users/templates/users/user_group_list.html index 6f6c6fc72..d8fc92e87 100644 --- a/apps/users/templates/users/user_group_list.html +++ b/apps/users/templates/users/user_group_list.html @@ -1,6 +1,29 @@ {% extends '_base_list.html' %} {% load i18n static %} -{% block table_search %}{% endblock %} +{% block table_search %} +
    + +
    +{% endblock %} {% block table_container %}
    {% trans "Create user group" %}
    @@ -16,7 +39,8 @@
    - +{% include "users/_user_groups_import_modal.html" %} +{% include "users/_user_groups_update_modal.html" %} {% endblock %} {% block content_bottom_left %}{% endblock %} @@ -111,6 +135,78 @@ $(document).ready(function() { default: break; } +}).on('click', '.btn_export', function(){ + var data_table = $('#group_list_table').DataTable(); + var rows = data_table.rows('.selected').data(); + var groups = []; + $.each(rows, function (index, obj) { + groups.push(obj.id) + }); + var data = { + 'resources': groups + }; + var search = $("input[type='search']").val(); + var props = { + method: "POST", + body: JSON.stringify(data), + success_url: "{% url 'api-users:user-group-list' %}", + format: "csv", + params: { + search: search + } + }; + APIExportData(props); +}).on('click', '#btn_import_confirm',function () { + var url = "{% url 'api-users:user-group-list' %}"; + var file = document.getElementById('id_file').files[0]; + if(!file){ + toastr.error("{% trans "Please select file" %}"); + return + } + var data_table = $('#group_list_table').DataTable(); + APIImportData({ + url: url, + method: "POST", + body: file, + data_table: data_table + }); }) +.on('click', '#download_update_template', function(){ + var data_table = $('#group_list_table').DataTable(); + var rows = data_table.rows('.selected').data(); + var groups = []; + $.each(rows, function (index, obj) { + groups.push(obj.id) + }); + var data = { + 'resources': groups + }; + var search = $("input[type='search']").val(); + var props = { + method: "POST", + body: JSON.stringify(data), + success_url: "{% url 'api-users:user-group-list' %}?format=csv&template=update", + format: "csv", + params: { + search: search + } + }; + APIExportData(props); +}).on('click', '#btn_update_confirm',function () { + var url = "{% url 'api-users:user-group-list' %}"; + var file = document.getElementById('update_file').files[0]; + if(!file){ + toastr.error("{% trans "Please select file" %}"); + return + } + var data_table = $('#group_list_table').DataTable(); + APIImportData({ + url: url, + method: "PUT", + body: file, + data_table: data_table + }); +}) + {% endblock %} diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index 15b5fb2f9..5d33a96aa 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -1,16 +1,28 @@ {% extends '_base_list.html' %} {% load i18n static %} {% block table_search %} -
    - -
    +
    + +
    {% endblock %} {% block table_container %}
    {% trans "Create user" %}
    @@ -48,6 +60,7 @@
    {% include "users/_user_import_modal.html" %} +{% include "users/_user_update_modal.html" %} {% endblock %} {% block content_bottom_left %}{% endblock %} {% block custom_foot_js %} @@ -113,6 +126,7 @@ function initTable() { return table } + $(document).ready(function(){ var table = initTable(); var fields = $('#fm_user_bulk_update .form-group'); @@ -120,87 +134,127 @@ $(document).ready(function(){ console.log(value) }); $('.btn_export').click(function () { - var users = []; var rows = table.rows('.selected').data(); - if(rows.length===0){ - rows = table.rows().data(); - } + var users = []; $.each(rows, function (index, obj) { users.push(obj.id) }); - $.ajax({ - url: "{% url 'users:user-export' %}", - method: 'POST', - data: JSON.stringify({users_id: users}), - dataType: "json", - success: function (data, textStatus) { - window.open(data.redirect) - }, - error: function () { - toastr.error('Export failed'); + var data = { + 'resources': users + }; + var search = $("input[type='search']").val(); + var props = { + method: "POST", + body: JSON.stringify(data), + success_url: "{% url 'api-users:user-list' %}", + format: 'csv', + params: { + search: search } - }) + }; + APIExportData(props); }); - $('#btn_user_import').click(function() { - var $form = $('#fm_user_import'); - $form.find('.help-block').remove(); - function success (data) { - if (data.valid === false) { - $('', {class: 'help-block text-danger'}).html(data.msg).insertAfter($('#id_users')); - } else { - $('#id_created').html(data.created_info); - $('#id_created_detail').html(data.created.join(', ')); - $('#id_updated').html(data.updated_info); - $('#id_updated_detail').html(data.updated.join(', ')); - $('#id_failed').html(data.failed_info); - $('#id_failed_detail').html(data.failed.join(', ')); - var $data_table = $('#user_list_table').DataTable(); - $data_table.ajax.reload(); - } + $('#btn_import_confirm').click(function() { + var url = "{% url 'api-users:user-list' %}"; + var file = document.getElementById('id_file').files[0]; + if(!file){ + toastr.error("{% trans "Please select file" %}"); + return } - $form.ajaxSubmit({success: success}); - }) + var data_table = $('#user_list_table').DataTable(); + APIImportData({ + url: url, + method: "POST", + body: file, + data_table: data_table + }); + }); + $('#download_update_template').click(function () { + var rows = table.rows('.selected').data(); + var users = []; + $.each(rows, function (index, obj) { + users.push(obj.id) + }); + var data = { + 'resources': users + }; + var search = $("input[type='search']").val(); + var props = { + method: "POST", + body: JSON.stringify(data), + success_url: "{% url 'api-users:user-list' %}?format=csv&template=update", + format: 'csv', + params: { + search: search + } + }; + APIExportData(props); + }); + $('#btn_update_confirm').click(function() { + var url = "{% url 'api-users:user-list' %}"; + var file = document.getElementById('update_file').files[0]; + if(!file){ + toastr.error("{% trans "Please select file" %}"); + return + } + var data_table = $('#user_list_table').DataTable(); + APIImportData({ + url: url, + method: "PUT", + body: file, + data_table: data_table + }); + }); }).on('click', '#btn_bulk_update', function(){ var action = $('#slct_bulk_update').val(); var $data_table = $('#user_list_table').DataTable(); var id_list = []; - var plain_id_list = []; $data_table.rows({selected: true}).every(function(){ - id_list.push({pk: this.data().id}); - plain_id_list.push(this.data().id); + id_list.push(this.data().id); }); - if (id_list === []) { + if (id_list.length === 0) { return false; } var the_url = "{% url 'api-users:user-list' %}"; + var data = { + 'resources': id_list + }; + function refreshTag() { + $('#user_list_table').DataTable().ajax.reload() + } function doDeactive() { - var body = $.each(id_list, function(index, user_object) { - user_object['is_active'] = false; + var data = []; + $.each(id_list, function(index, object_id) { + var obj = {"pk": object_id, "is_active": false}; + data.push(obj); }); function success() { - location.reload(); + setTimeout( function () { + window.location.reload();}, 300); } APIUpdateAttr({ url: the_url, method: 'PATCH', - body: JSON.stringify(body), + body: JSON.stringify(data), success: success }); - location.reload(); } - function doActive() { - var body = $.each(id_list, function(index, user_object) { - user_object['is_active'] = true; + function doActive() { + var data = []; + $.each(id_list, function(index, object_id) { + var obj = {"pk": object_id, "is_active": true}; + data.push(obj); }); function success() { - location.reload(); + setTimeout( function () { + window.location.reload();}, 300); } APIUpdateAttr({ url: the_url, method: 'PATCH', - body: JSON.stringify(body), + body: JSON.stringify(data), success: success }); } @@ -214,26 +268,49 @@ $(document).ready(function(){ confirmButtonColor: "#DD6B55", confirmButtonText: "{% trans 'Confirm' %}", closeOnConfirm: false - }, function() { - var success = function() { + },function () { + function success(data) { + url = setUrlParam(the_url, 'spm', data.spm); + APIUpdateAttr({ + url:url, + method:'DELETE', + success:refreshTag, + flash_message:false, + }); var msg = "{% trans 'User Deleted.' %}"; swal("{% trans 'User Delete' %}", msg, "success"); - $('#user_list_table').DataTable().ajax.reload(); - }; - var fail = function() { + } + function fail() { var msg = "{% trans 'User Deleting failed.' %}"; swal("{% trans 'User Delete' %}", msg, "error"); - }; - var url_delete = the_url + '?id__in=' + JSON.stringify(plain_id_list); - APIUpdateAttr({url: url_delete, method: 'DELETE', success: success, error: fail}); - jumpserver.checked = false; - }); + } + APIUpdateAttr({ + url: "{% url 'api-common:resources-cache' %}", + method:'POST', + body:JSON.stringify(data), + success:success, + error:fail + }) + }) } + function doUpdate() { - var users_id = plain_id_list.join(','); - var url = "{% url 'users:user-bulk-update' %}?users_id=" + users_id; - location.href = url - } + function fail(data) { + toastr.error(JSON.parse(data)) + } + function success(data) { + var url = "{% url 'users:user-bulk-update' %}"; + location.href= setUrlParam(url, 'spm', data.spm); + } + APIUpdateAttr({ + url: "{% url 'api-common:resources-cache' %}", + method:'POST', + body:JSON.stringify(data), + flash_message:false, + success:success, + error:fail + }) + } switch(action) { case 'deactive': doDeactive(); diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 270a9a5c9..a7515d030 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -31,7 +31,9 @@ from django.views.generic.detail import DetailView from django.views.decorators.csrf import csrf_exempt from django.contrib.auth import logout as auth_logout -from common.const import create_success_msg, update_success_msg +from common.const import ( + create_success_msg, update_success_msg, KEY_CACHE_RESOURCES_ID +) from common.mixins import JSONResponseMixin from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from common.permissions import AdminUserRequiredMixin @@ -156,15 +158,12 @@ class UserBulkUpdateView(AdminUserRequiredMixin, TemplateView): id_list = None def get(self, request, *args, **kwargs): - users_id = self.request.GET.get('users_id', '') - self.id_list = [i for i in users_id.split(',')] - + spm = request.GET.get('spm', '') + users_id = cache.get(KEY_CACHE_RESOURCES_ID.format(spm)) if kwargs.get('form'): self.form = kwargs['form'] elif users_id: - self.form = self.form_class( - initial={'users': self.id_list} - ) + self.form = self.form_class(initial={'users': users_id}) else: self.form = self.form_class() return super().get(request, *args, **kwargs)