Commit 22f362aa authored by 八千流's avatar 八千流 Committed by 老广

Dev csv (#2640)

* [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] 翻译
parent 49429008
...@@ -20,7 +20,7 @@ from rest_framework.response import Response ...@@ -20,7 +20,7 @@ from rest_framework.response import Response
from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import BulkModelViewSet
from rest_framework.pagination import LimitOffsetPagination from rest_framework.pagination import LimitOffsetPagination
from common.mixins import IDInFilterMixin from common.mixins import IDInCacheFilterMixin
from common.utils import get_logger from common.utils import get_logger
from ..hands import IsOrgAdmin from ..hands import IsOrgAdmin
from ..models import AdminUser, Asset from ..models import AdminUser, Asset
...@@ -36,7 +36,7 @@ __all__ = [ ...@@ -36,7 +36,7 @@ __all__ = [
] ]
class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet): class AdminUserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
""" """
Admin user api set, for add,delete,update,list,retrieve resource Admin user api set, for add,delete,update,list,retrieve resource
""" """
......
...@@ -16,8 +16,9 @@ from django.urls import reverse_lazy ...@@ -16,8 +16,9 @@ from django.urls import reverse_lazy
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Q from django.db.models import Q
from common.mixins import IDInFilterMixin from common.mixins import IDInCacheFilterMixin
from common.utils import get_logger
from common.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX from ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from ..models import Asset, AdminUser, Node from ..models import Asset, AdminUser, Node
...@@ -35,7 +36,7 @@ __all__ = [ ...@@ -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. API endpoint that allows Asset to be viewed or edited.
""" """
...@@ -47,6 +48,19 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): ...@@ -47,6 +48,19 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
pagination_class = LimitOffsetPagination pagination_class = LimitOffsetPagination
permission_classes = (IsOrgAdminOrAppUser,) 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): def filter_node(self, queryset):
node_id = self.request.query_params.get("node_id") node_id = self.request.query_params.get("node_id")
if not node_id: if not node_id:
...@@ -89,7 +103,7 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet): ...@@ -89,7 +103,7 @@ class AssetViewSet(IDInFilterMixin, LabelFilter, BulkModelViewSet):
return queryset return queryset
class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): class AssetListUpdateApi(IDInCacheFilterMixin, ListBulkCreateUpdateDestroyAPIView):
""" """
Asset bulk update api Asset bulk update api
""" """
......
...@@ -21,6 +21,7 @@ from rest_framework.pagination import LimitOffsetPagination ...@@ -21,6 +21,7 @@ from rest_framework.pagination import LimitOffsetPagination
from common.utils import get_logger from common.utils import get_logger
from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser from common.permissions import IsOrgAdmin, IsOrgAdminOrAppUser
from common.mixins import IDInCacheFilterMixin
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
from .. import serializers from .. import serializers
from ..tasks import push_system_user_to_assets_manual, \ from ..tasks import push_system_user_to_assets_manual, \
...@@ -38,7 +39,7 @@ __all__ = [ ...@@ -38,7 +39,7 @@ __all__ = [
] ]
class SystemUserViewSet(BulkModelViewSet): class SystemUserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
""" """
System user api set, for add,delete,update,list,retrieve resource System user api set, for add,delete,update,list,retrieve resource
""" """
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.core.cache import cache from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
...@@ -15,14 +16,29 @@ class AdminUserSerializer(serializers.ModelSerializer): ...@@ -15,14 +16,29 @@ class AdminUserSerializer(serializers.ModelSerializer):
""" """
管理用户 管理用户
""" """
assets_amount = serializers.SerializerMethodField() password = serializers.CharField(
unreachable_amount = serializers.SerializerMethodField() required=False, write_only=True, label=_('Password')
reachable_amount = serializers.SerializerMethodField() )
unreachable_amount = serializers.SerializerMethodField(label=_('Unreachable'))
assets_amount = serializers.SerializerMethodField(label=_('Asset'))
reachable_amount = serializers.SerializerMethodField(label=_('Reachable'))
class Meta: class Meta:
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
model = AdminUser 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): def get_field_names(self, declared_fields, info):
fields = super().get_field_names(declared_fields, info) fields = super().get_field_names(declared_fields, info)
......
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
# #
from rest_framework import serializers 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.mixins import BulkSerializerMixin
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from ..models import Asset from ..models import Asset
...@@ -13,15 +16,35 @@ __all__ = [ ...@@ -13,15 +16,35 @@ __all__ = [
] ]
class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer, OrgResourceSerializerMixin):
""" """
资产的数据结构 资产的数据结构
""" """
class Meta: class Meta:
model = Asset model = Asset
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = '__all__' # validators = [] # 解决批量导入时unique_together字段校验失败
validators = [] 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 @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
......
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from ..models import SystemUser, Asset from ..models import SystemUser, Asset
...@@ -10,16 +12,36 @@ class SystemUserSerializer(serializers.ModelSerializer): ...@@ -10,16 +12,36 @@ class SystemUserSerializer(serializers.ModelSerializer):
""" """
系统用户 系统用户
""" """
unreachable_amount = serializers.SerializerMethodField() password = serializers.CharField(
reachable_amount = serializers.SerializerMethodField() required=False, write_only=True, label=_('Password')
unreachable_assets = serializers.SerializerMethodField() )
reachable_assets = serializers.SerializerMethodField() unreachable_amount = serializers.SerializerMethodField(
assets_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: class Meta:
model = SystemUser model = SystemUser
exclude = ('_password', '_private_key', '_public_key')
list_serializer_class = AdaptedBulkListSerializer 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): def get_field_names(self, declared_fields, info):
fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info) fields = super(SystemUserSerializer, self).get_field_names(declared_fields, info)
......
{% 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 %}
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update admin user" %}{% endblock %}
\ No newline at end of file
{% extends '_modal.html' %} {% extends '_import_modal.html' %}
{% load i18n %} {% load i18n %}
{% block modal_id %}asset_import_modal{% endblock %}
{% block modal_title%}{% trans "Import asset" %}{% endblock %} {% block modal_title%}{% trans "Import assets" %}{% endblock %}
{% block modal_body %}
<form method="post" action="{% url 'assets:asset-import' %}" id="fm_asset_import" enctype="multipart/form-data"> {% block import_modal_download_template_url %}{% url "api-assets:asset-list" %}{% endblock %}
{% csrf_token %}
<div class="form-group">
<label class="control-label" for="id_assets">{% trans "Template" %}</label>
<a href="{% url 'assets:asset-export' %}" style="display: block">{% trans 'Download' %}</a>
</div>
<div class="form-group">
<label class="control-label" for="id_users">{% trans "Asset csv file" %}</label>
<input id="id_assets" type="file" name="file" />
<span class="help-block red-fonts">
{% trans 'If set id, will use this id update asset existed' %}
</span>
</div>
</form>
<p>
<p class="text-success" id="id_created"></p>
<p id="id_created_detail"></p>
<p class="text-warning" id="id_updated"></p>
<p id="id_updated_detail"></p>
<p class="text-danger" id="id_failed"></p>
<p id="id_failed_detail"></p>
</p>
{% endblock %}
{% block modal_confirm_id %}btn_asset_import{% endblock %}
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update assets" %}{% endblock %}
{% 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 %}
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update system user" %}{% endblock %}
\ No newline at end of file
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n static %} {% load i18n static %}
{% block table_search %}
{% endblock %}
{% block help_message %} {% block help_message %}
<div class="alert alert-info help-message"> <div class="alert alert-info help-message">
{# 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#} {# 管理用户是资产(被控服务器)上的root,或拥有 NOPASSWD: ALL sudo权限的用户,Jumpserver使用该用户来 `推送系统用户`、`获取资产硬件信息`等。#}
...@@ -12,6 +9,30 @@ ...@@ -12,6 +9,30 @@
{% trans 'You can set any one for Windows or other hardware.' %} {% trans 'You can set any one for Windows or other hardware.' %}
</div> </div>
{% endblock %} {% endblock %}
{% block table_search %}
<div class="" style="float: right">
<div class=" btn-group">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">CSV <span class="caret"></span></button>
<ul class="dropdown-menu">
<li>
<a class=" btn_export" tabindex="0">
<span>{% trans "Export" %}</span>
</a>
</li>
<li>
<a class=" btn_import" data-toggle="modal" data-target="#import_modal" tabindex="0">
<span>{% trans "Import" %}</span>
</a>
</li>
<li>
<a class=" btn_update" data-toggle="modal" data-target="#update_modal" tabindex="0">
<span>{% trans "Update" %}</span>
</a>
</li>
</ul>
</div>
</div>
{% endblock %}
{% block table_container %} {% block table_container %}
<div class="uc pull-left m-r-5"> <div class="uc pull-left m-r-5">
...@@ -36,6 +57,8 @@ ...@@ -36,6 +57,8 @@
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
{% include 'assets/_admin_user_import_modal.html' %}
{% include 'assets/_admin_user_update_modal.html' %}
{% endblock %} {% endblock %}
{% block content_bottom_left %}{% endblock %} {% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
...@@ -107,6 +130,82 @@ $(document).ready(function(){ ...@@ -107,6 +130,82 @@ $(document).ready(function(){
$data_table.ajax.reload(); $data_table.ajax.reload();
}, 3000); }, 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
});
})
</script> </script>
{% endblock %} {% endblock %}
...@@ -14,6 +14,28 @@ ...@@ -14,6 +14,28 @@
{% endblock %} {% endblock %}
{% block table_search %} {% block table_search %}
<div class="" style="float: right">
<div class=" btn-group">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">CSV <span class="caret"></span></button>
<ul class="dropdown-menu">
<li>
<a class=" btn_export" tabindex="0">
<span>{% trans "Export" %}</span>
</a>
</li>
<li>
<a class=" btn_import" data-toggle="modal" data-target="#import_modal" tabindex="0">
<span>{% trans "Import" %}</span>
</a>
</li>
<li>
<a class=" btn_update" data-toggle="modal" data-target="#update_modal" tabindex="0">
<span>{% trans "Update" %}</span>
</a>
</li>
</ul>
</div>
</div>
{% endblock %} {% endblock %}
{% block table_container %} {% block table_container %}
...@@ -41,6 +63,8 @@ ...@@ -41,6 +63,8 @@
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
{% include 'assets/_system_user_import_modal.html' %}
{% include 'assets/_system_user_update_modal.html' %}
{% endblock %} {% endblock %}
{% block custom_foot_js %} {% block custom_foot_js %}
<script> <script>
...@@ -173,6 +197,81 @@ $(document).ready(function(){ ...@@ -173,6 +197,81 @@ $(document).ready(function(){
break; break;
} }
}) })
.on('click', '.btn_export', function () {
var data_table = $('#system_user_list_table').DataTable();
var rows = data_table.rows('.selected').data();
var system_users = [];
$.each(rows, function (index, obj) {
system_users.push(obj.id)
});
var data = {
'resources': system_users
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-assets:system-user-list' %}",
format: "csv",
params:{
search:search
}
};
APIExportData(props);
})
.on('click', '#btn_import_confirm', function () {
var url = "{% url 'api-assets:system-user-list' %}";
var file = document.getElementById('id_file').files[0];
if(!file){
toastr.error("{% trans 'Please select file' %}");
return
}
var data_table = $('#system_user_list_table').DataTable();
APIImportData({
url: url,
method: "POST",
body: file,
data_table: data_table
});
})
.on('click', '#download_update_template', function () {
var data_table = $('#system_user_list_table').DataTable();
var rows = data_table.rows('.selected').data();
var system_users = [];
$.each(rows, function (index, obj) {
system_users.push(obj.id)
});
var data = {
'resources': system_users
};
var search = $("input[type='search']").val();
var props = {
method: "POST",
body: JSON.stringify(data),
success_url: "{% url 'api-assets:system-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:system-user-list' %}";
var data_table = $('#system_user_list_table').DataTable();
APIImportData({
url: url,
method: "PUT",
body: file,
data_table: data_table
});
})
</script> </script>
{% endblock %} {% endblock %}
......
...@@ -27,7 +27,9 @@ from django.contrib.messages.views import SuccessMessageMixin ...@@ -27,7 +27,9 @@ from django.contrib.messages.views import SuccessMessageMixin
from common.mixins import JSONResponseMixin from common.mixins import JSONResponseMixin
from common.utils import get_object_or_none, get_logger from common.utils import get_object_or_none, get_logger
from common.permissions import AdminUserRequiredMixin 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 ..const import CACHE_KEY_ASSET_BULK_UPDATE_ID_PREFIX
from orgs.utils import current_org from orgs.utils import current_org
from .. import forms from .. import forms
...@@ -122,7 +124,7 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView): ...@@ -122,7 +124,7 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
spm = request.GET.get('spm', '') 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'): if kwargs.get('form'):
self.form = kwargs['form'] self.form = kwargs['form']
elif assets_id: elif assets_id:
......
...@@ -3,10 +3,18 @@ ...@@ -3,10 +3,18 @@
import os import os
import uuid import uuid
from rest_framework.views import Response
from rest_framework import generics, serializers
from django.core.cache import cache 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): class OutputSerializer(serializers.Serializer):
output = serializers.CharField() output = serializers.CharField()
...@@ -68,3 +76,14 @@ class LogTailApi(generics.RetrieveAPIView): ...@@ -68,3 +76,14 @@ class LogTailApi(generics.RetrieveAPIView):
data, end, new_mark = self.read_from_file() data, end, new_mark = self.read_from_file()
return Response({"data": data, 'end': end, 'mark': new_mark}) 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})
...@@ -7,3 +7,4 @@ create_success_msg = _("%(name)s was created successfully") ...@@ -7,3 +7,4 @@ create_success_msg = _("%(name)s was created successfully")
update_success_msg = _("%(name)s was updated successfully") update_success_msg = _("%(name)s was updated successfully")
FILE_END_GUARD = ">>> Content End <<<" FILE_END_GUARD = ">>> Content End <<<"
celery_task_pre_key = "CELERY_" celery_task_pre_key = "CELERY_"
KEY_CACHE_RESOURCES_ID = "RESOURCES_ID_{}"
...@@ -3,12 +3,15 @@ ...@@ -3,12 +3,15 @@
from django.db import models from django.db import models
from django.http import JsonResponse from django.http import JsonResponse
from django.utils import timezone from django.utils import timezone
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework.utils import html from rest_framework.utils import html
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import SkipField from rest_framework.fields import SkipField
from .const import KEY_CACHE_RESOURCES_ID
class NoDeleteQuerySet(models.query.QuerySet): class NoDeleteQuerySet(models.query.QuerySet):
...@@ -65,6 +68,27 @@ class IDInFilterMixin(object): ...@@ -65,6 +68,27 @@ class IDInFilterMixin(object):
return queryset 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): class BulkSerializerMixin(object):
""" """
Become rest_framework_bulk not support uuid as a primary key Become rest_framework_bulk not support uuid as a primary key
...@@ -131,7 +155,11 @@ class BulkListSerializerMixin(object): ...@@ -131,7 +155,11 @@ class BulkListSerializerMixin(object):
for item in data: for item in data:
try: try:
# prepare child serializer to only handle one instance # 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 self.child.initial_data = item
# raw # raw
validated = self.child.run_validation(item) validated = self.child.run_validation(item)
......
from .csv import *
\ No newline at end of file
# ~*~ 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!')
from .csv import *
\ No newline at end of file
# ~*~ 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
# -*- 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'),
]
...@@ -363,6 +363,16 @@ REST_FRAMEWORK = { ...@@ -363,6 +363,16 @@ REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': ( 'DEFAULT_PERMISSION_CLASSES': (
'common.permissions.IsOrgAdmin', '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': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
# 'rest_framework.authentication.BasicAuthentication', # 'rest_framework.authentication.BasicAuthentication',
'authentication.backends.api.AccessKeyAuthentication', 'authentication.backends.api.AccessKeyAuthentication',
......
...@@ -20,6 +20,7 @@ api_v1 = [ ...@@ -20,6 +20,7 @@ api_v1 = [
path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')), path('orgs/v1/', include('orgs.urls.api_urls', namespace='api-orgs')),
path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')), path('settings/v1/', include('settings.urls.api_urls', namespace='api-settings')),
path('authentication/v1/', include('authentication.urls.api_urls', namespace='api-auth')), 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')), path('applications/v1/', include('applications.urls.api_urls', namespace='api-applications')),
] ]
......
This diff is collapsed.
...@@ -8,9 +8,12 @@ from django.shortcuts import redirect, get_object_or_404 ...@@ -8,9 +8,12 @@ from django.shortcuts import redirect, get_object_or_404
from django.forms import ModelForm from django.forms import ModelForm
from django.http.response import HttpResponseForbidden from django.http.response import HttpResponseForbidden
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from rest_framework import serializers
from common.utils import get_logger 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 from .models import Organization
logger = get_logger(__file__) logger = get_logger(__file__)
...@@ -18,7 +21,8 @@ tl = Local() ...@@ -18,7 +21,8 @@ tl = Local()
__all__ = [ __all__ = [
'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm', 'OrgManager', 'OrgViewGenericMixin', 'OrgModelMixin', 'OrgModelForm',
'RootOrgViewMixin', 'OrgMembershipSerializerMixin', 'OrgMembershipModelViewSetMixin' 'RootOrgViewMixin', 'OrgMembershipSerializerMixin',
'OrgMembershipModelViewSetMixin', 'OrgResourceSerializerMixin',
] ]
...@@ -202,3 +206,11 @@ class OrgMembershipModelViewSetMixin: ...@@ -202,3 +206,11 @@ class OrgMembershipModelViewSetMixin:
def get_queryset(self): def get_queryset(self):
queryset = self.membership_class.objects.filter(organization=self.org) queryset = self.membership_class.objects.filter(organization=self.org)
return queryset 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)
...@@ -38,4 +38,10 @@ def get_current_org(): ...@@ -38,4 +38,10 @@ def get_current_org():
return _find('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')) current_org = LocalProxy(partial(_find, 'current_org'))
...@@ -954,9 +954,92 @@ function rootNodeAddDom(ztree, callback) { ...@@ -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 = "<li>" + "Line " + (++index) + ". " + err + "</li>";
html += li
}
});
html = "<ul>" + html + "</ul>"
}
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 ) { function htmlEscape ( d ) {
return typeof d === 'string' ? return typeof d === 'string' ?
d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') : d.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;') :
d; d;
} }
\ No newline at end of file
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}import_modal{% endblock %}
{% block modal_confirm_id %}btn_import_confirm{% endblock %}
{% block modal_body %}
<form method="post" id="fm_import">
{% csrf_token %}
<div class="form-group">
<label class="control-label">{% trans "Download the imported template or use the exported CSV file format" %}</label>
<a href="{% block import_modal_download_template_url %}{% endblock %}?format=csv&template=import" style="display: block">{% trans 'Download the import template' %}</a>
</div>
<div class="form-group">
<label class="control-label" for="id_file">{% trans "Select the CSV file to import" %}</label>
<input id="id_file" type="file" name="file" />
</div>
</form>
<div>
<p class="text-success" id="success_created"></p>
<p id="success_created_detail"></p>
<p class="text-danger" id="created_failed"></p>
<p id="created_failed_detail"></p>
</div>
{% endblock %}
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<div class="modal-dialog {% block modal_class %}{% endblock %}"> <div class="modal-dialog {% block modal_class %}{% endblock %}">
<div class="modal-content animated fadeIn"> <div class="modal-content animated fadeIn">
<div class="modal-header"> <div class="modal-header">
<button data-dismiss="modal" class="close close_btn1" type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button> <button data-dismiss="modal" id="close_button1" class="close close_btn1 " type="button"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
<h4 class="modal-title">{% block modal_title %}{% endblock %}</h4> <h4 class="modal-title">{% block modal_title %}{% endblock %}</h4>
<small>{% block modal_comment %}{% endblock %}</small> <small>{% block modal_comment %}{% endblock %}</small>
</div> </div>
...@@ -19,10 +19,32 @@ ...@@ -19,10 +19,32 @@
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
{% block modal_button %} {% block modal_button %}
<button data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button> <button id="close_button2" data-dismiss="modal" class="btn btn-white close_btn2" type="button">{% trans "Close" %}</button>
<button class="btn btn-primary" type="button" id="{% block modal_confirm_id %}{% endblock %}">{% trans 'Confirm' %}</button> <button class="btn btn-primary" type="button" id="{% block modal_confirm_id %}{% endblock %}">{% trans 'Confirm' %}</button>
{% endblock %} {% endblock %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script>
$(document).ready(function(){
})
.on('click', '#close_button1', function () {
SetMessageLabelEmpty()
})
.on('click', '#close_button2', function () {
SetMessageLabelEmpty()
});
function SetMessageLabelEmpty() {
$('#success_created').html('');
$('#success_created_detail').html('');
$('#created_failed').html('');
$('#created_failed_detail').html('');
$('#success_updated').html('');
$('#success_updated_detail').html('');
$('#updated_failed').html('');
$('#updated_failed_detail').html('');
}
</script>
{% extends '_modal.html' %}
{% load i18n %}
{% block modal_id %}update_modal{% endblock %}
{% block modal_confirm_id %}btn_update_confirm{% endblock %}
{% block modal_body %}
<form method="post" id="fm_import">
{% csrf_token %}
<div class="form-group">
<label class="control-label">{% trans "Download the update template or use the exported CSV file format" %}</label>
<a id="download_update_template" style="display: block">{% trans 'Download the update template' %}</a>
</div>
<div class="form-group">
<label class="control-label" for="update_file">{% trans "Select the CSV file to import" %}</label>
<input id="update_file" type="file" name="file" />
</div>
</form>
<div>
<p class="text-warning" id="success_updated"></p>
<p id="success_updated_detail"></p>
<p class="text-danger" id="updated_failed"></p>
<p id="updated_failed_detail"></p>
</div>
{% endblock %}
...@@ -9,13 +9,13 @@ from ..serializers import UserGroupSerializer, \ ...@@ -9,13 +9,13 @@ from ..serializers import UserGroupSerializer, \
UserGroupUpdateMemberSerializer UserGroupUpdateMemberSerializer
from ..models import UserGroup from ..models import UserGroup
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
from common.mixins import IDInFilterMixin from common.mixins import IDInCacheFilterMixin
__all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi'] __all__ = ['UserGroupViewSet', 'UserGroupUpdateUserApi']
class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet): class UserGroupViewSet(IDInCacheFilterMixin, BulkModelViewSet):
filter_fields = ("name",) filter_fields = ("name",)
search_fields = filter_fields search_fields = filter_fields
queryset = UserGroup.objects.all() queryset = UserGroup.objects.all()
......
...@@ -15,7 +15,7 @@ from rest_framework.pagination import LimitOffsetPagination ...@@ -15,7 +15,7 @@ from rest_framework.pagination import LimitOffsetPagination
from common.permissions import ( from common.permissions import (
IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
) )
from common.mixins import IDInFilterMixin from common.mixins import IDInCacheFilterMixin
from common.utils import get_logger from common.utils import get_logger
from orgs.utils import current_org from orgs.utils import current_org
from ..serializers import UserSerializer, UserPKUpdateSerializer, \ from ..serializers import UserSerializer, UserPKUpdateSerializer, \
...@@ -32,7 +32,7 @@ __all__ = [ ...@@ -32,7 +32,7 @@ __all__ = [
] ]
class UserViewSet(IDInFilterMixin, BulkModelViewSet): class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
filter_fields = ('username', 'email', 'name', 'id') filter_fields = ('username', 'email', 'name', 'id')
search_fields = filter_fields search_fields = filter_fields
queryset = User.objects.exclude(role=User.ROLE_APP) queryset = User.objects.exclude(role=User.ROLE_APP)
...@@ -40,9 +40,15 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet): ...@@ -40,9 +40,15 @@ class UserViewSet(IDInFilterMixin, BulkModelViewSet):
permission_classes = (IsOrgAdmin,) permission_classes = (IsOrgAdmin,)
pagination_class = LimitOffsetPagination 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): def perform_create(self, serializer):
user = serializer.save() users = serializer.save()
post_user_create.send(self.__class__, user=user) self.send_created_signal(users)
def get_queryset(self): def get_queryset(self):
queryset = current_org.get_org_users() queryset = current_org.get_org_users()
...@@ -213,4 +219,4 @@ class UserResetOTPApi(generics.RetrieveAPIView): ...@@ -213,4 +219,4 @@ class UserResetOTPApi(generics.RetrieveAPIView):
user.otp_secret_key = '' user.otp_secret_key = ''
user.save() user.save()
logout(request) logout(request)
return Response({"msg": "success"}) return Response({"msg": "success"})
\ No newline at end of file
...@@ -19,12 +19,21 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): ...@@ -19,12 +19,21 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = [ fields = [
'id', 'name', 'username', 'email', 'groups', 'groups_display', 'id', 'name', 'username', 'email', 'groups', 'groups_display',
'role', 'role_display', 'avatar_url', 'wechat', 'phone', 'role', 'role_display', 'wechat', 'phone', 'otp_level',
'otp_level', 'comment', 'source', 'source_display', 'comment', 'source', 'source_display', 'is_valid', 'is_expired',
'is_valid', 'is_expired', 'is_active', 'is_active', 'created_by', 'is_first_login',
'created_by', 'is_first_login', 'date_password_last_updated', 'date_expired', 'avatar_url',
'date_password_last_updated', 'date_expired',
] ]
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): class UserPKUpdateSerializer(serializers.ModelSerializer):
...@@ -48,17 +57,20 @@ class UserUpdateGroupSerializer(serializers.ModelSerializer): ...@@ -48,17 +57,20 @@ class UserUpdateGroupSerializer(serializers.ModelSerializer):
class UserGroupSerializer(BulkSerializerMixin, 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: class Meta:
model = UserGroup model = UserGroup
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = '__all__' fields = [
read_only_fields = ['created_by'] 'id', 'org_id', 'name', 'users', 'comment', 'date_created',
'created_by',
@staticmethod ]
def get_users(obj): extra_kwargs = {
return [user.name for user in obj.users.all()] 'created_by': {'label': _('Created by'), 'read_only': True}
}
class UserGroupUpdateMemberSerializer(serializers.ModelSerializer): class UserGroupUpdateMemberSerializer(serializers.ModelSerializer):
......
...@@ -28,4 +28,3 @@ def on_user_create(sender, user=None, **kwargs): ...@@ -28,4 +28,3 @@ def on_user_create(sender, user=None, **kwargs):
logger.info(" - Sending welcome mail ...".format(user.name)) logger.info(" - Sending welcome mail ...".format(user.name))
if user.email: if user.email:
send_user_created_mail(user) send_user_created_mail(user)
{% 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
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update user group" %}{% endblock %}
\ No newline at end of file
{% extends '_modal.html' %} {% extends '_import_modal.html' %}
{% load i18n %} {% load i18n %}
{% block modal_id %}user_import_modal{% endblock %}
{% block modal_title%}{% trans "Import user" %}{% endblock %} {% block modal_title%}{% trans "Import users" %}{% endblock %}
{% block modal_body %}
<p class="text-success">{% trans "Download template or use export csv format" %}</p> {% block import_modal_download_template_url %}{% url "api-users:user-list" %}{% endblock %}
<form method="post" action="{% url 'users:user-import' %}" id="fm_user_import" enctype="multipart/form-data">
{% csrf_token %}
<div class="form-group">
<label class="control-label" for="id_users">{% trans "Template" %}</label>
<a href="{% url 'users:user-export' %}" style="display: block">{% trans 'Download' %}</a>
</div>
<div class="form-group">
<label class="control-label" for="id_users">{% trans "Users csv file" %}</label>
<input id="id_users" type="file" name="file" />
<span class="help-block red-fonts">{% trans 'If set id, will use this id update user existed' %}</span>
</div>
</form>
<p>
<p class="text-success" id="id_created"></p>
<p id="id_created_detail"></p>
<p class="text-warning" id="id_updated"></p>
<p id="id_updated_detail"></p>
<p class="text-danger" id="id_failed"></p>
<p id="id_failed_detail"></p>
</p>
{% endblock %}
{% block modal_confirm_id %}btn_user_import{% endblock %}
{% extends '_update_modal.html' %}
{% load i18n %}
{% block modal_title%}{% trans "Update user" %}{% endblock %}
\ No newline at end of file
{% extends '_base_list.html' %} {% extends '_base_list.html' %}
{% load i18n static %} {% load i18n static %}
{% block table_search %}{% endblock %} {% block table_search %}
<div class="" style="float: right">
<div class=" btn-group">
<button data-toggle="dropdown" class="btn btn-default btn-sm dropdown-toggle">CSV <span class="caret"></span></button>
<ul class="dropdown-menu">
<li>
<a class=" btn_export" tabindex="0">
<span>{% trans "Export" %}</span>
</a>
</li>
<li>
<a class=" btn_import" data-toggle="modal" data-target="#import_modal" tabindex="0">
<span>{% trans "Import" %}</span>
</a>
</li>
<li>
<a class=" btn_update" data-toggle="modal" data-target="#update_modal" tabindex="0">
<span>{% trans "Update" %}</span>
</a>
</li>
</ul>
</div>
</div>
{% endblock %}
{% block table_container %} {% block table_container %}
<div class="pull-left m-r-5"><a href="{% url 'users:user-group-create' %}" class="btn btn-sm btn-primary ">{% trans "Create user group" %}</a></div> <div class="pull-left m-r-5"><a href="{% url 'users:user-group-create' %}" class="btn btn-sm btn-primary ">{% trans "Create user group" %}</a></div>
<table class="table table-striped table-bordered table-hover " id="group_list_table" > <table class="table table-striped table-bordered table-hover " id="group_list_table" >
...@@ -16,7 +39,8 @@ ...@@ -16,7 +39,8 @@
</tr> </tr>
</thead> </thead>
</table> </table>
{% include "users/_user_groups_import_modal.html" %}
{% include "users/_user_groups_update_modal.html" %}
{% endblock %} {% endblock %}
{% block content_bottom_left %}{% endblock %} {% block content_bottom_left %}{% endblock %}
...@@ -111,6 +135,78 @@ $(document).ready(function() { ...@@ -111,6 +135,78 @@ $(document).ready(function() {
default: default:
break; 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
});
})
</script> </script>
{% endblock %} {% endblock %}
This diff is collapsed.
...@@ -31,7 +31,9 @@ from django.views.generic.detail import DetailView ...@@ -31,7 +31,9 @@ from django.views.generic.detail import DetailView
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth import logout as auth_logout 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.mixins import JSONResponseMixin
from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen from common.utils import get_logger, get_object_or_none, is_uuid, ssh_key_gen
from common.permissions import AdminUserRequiredMixin from common.permissions import AdminUserRequiredMixin
...@@ -156,15 +158,12 @@ class UserBulkUpdateView(AdminUserRequiredMixin, TemplateView): ...@@ -156,15 +158,12 @@ class UserBulkUpdateView(AdminUserRequiredMixin, TemplateView):
id_list = None id_list = None
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
users_id = self.request.GET.get('users_id', '') spm = request.GET.get('spm', '')
self.id_list = [i for i in users_id.split(',')] users_id = cache.get(KEY_CACHE_RESOURCES_ID.format(spm))
if kwargs.get('form'): if kwargs.get('form'):
self.form = kwargs['form'] self.form = kwargs['form']
elif users_id: elif users_id:
self.form = self.form_class( self.form = self.form_class(initial={'users': users_id})
initial={'users': self.id_list}
)
else: else:
self.form = self.form_class() self.form = self.form_class()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment