mirror of https://github.com/jumpserver/jumpserver
[Update] 去掉原来批量的view
parent
e4880a247f
commit
31d2f2a799
|
@ -9,8 +9,6 @@ urlpatterns = [
|
|||
path('', views.AssetListView.as_view(), name='asset-index'),
|
||||
path('asset/', views.AssetListView.as_view(), name='asset-list'),
|
||||
path('asset/create/', views.AssetCreateView.as_view(), name='asset-create'),
|
||||
path('asset/export/', views.AssetExportView.as_view(), name='asset-export'),
|
||||
path('asset/import/', views.BulkImportAssetView.as_view(), name='asset-import'),
|
||||
path('asset/<uuid:pk>/', views.AssetDetailView.as_view(), name='asset-detail'),
|
||||
path('asset/<uuid:pk>/update/', views.AssetUpdateView.as_view(), name='asset-update'),
|
||||
path('asset/<uuid:pk>/delete/', views.AssetDeleteView.as_view(), name='asset-delete'),
|
||||
|
|
|
@ -37,7 +37,7 @@ from ..models import Asset, AdminUser, SystemUser, Label, Node, Domain
|
|||
__all__ = [
|
||||
'AssetListView', 'AssetCreateView', 'AssetUpdateView', 'AssetUserListView',
|
||||
'UserAssetListView', 'AssetBulkUpdateView', 'AssetDetailView',
|
||||
'AssetDeleteView', 'AssetExportView', 'BulkImportAssetView',
|
||||
'AssetDeleteView',
|
||||
]
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
@ -229,150 +229,3 @@ class AssetDetailView(PermissionsMixin, DetailView):
|
|||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class AssetExportView(PermissionsMixin, View):
|
||||
permission_classes = [IsValidUser]
|
||||
|
||||
def get(self, request):
|
||||
spm = request.GET.get('spm', '')
|
||||
assets_id_default = [Asset.objects.first().id] if Asset.objects.first() else []
|
||||
assets_id = cache.get(spm, assets_id_default)
|
||||
fields = [
|
||||
field for field in Asset._meta.fields
|
||||
if field.name not in [
|
||||
'date_created', 'org_id'
|
||||
]
|
||||
]
|
||||
filename = 'assets-{}.csv'.format(
|
||||
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
|
||||
)
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
|
||||
response.write(codecs.BOM_UTF8)
|
||||
assets = Asset.objects.filter(id__in=assets_id)
|
||||
writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
|
||||
|
||||
header = [field.verbose_name for field in fields]
|
||||
writer.writerow(header)
|
||||
|
||||
for asset in assets:
|
||||
data = [getattr(asset, field.name) for field in fields]
|
||||
writer.writerow(data)
|
||||
return response
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
try:
|
||||
assets_id = json.loads(request.body).get('assets_id', [])
|
||||
node_id = json.loads(request.body).get('node_id', None)
|
||||
except ValueError:
|
||||
return HttpResponse('Json object not valid', status=400)
|
||||
|
||||
if not assets_id:
|
||||
node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
|
||||
assets = node.get_all_assets()
|
||||
for asset in assets:
|
||||
assets_id.append(asset.id)
|
||||
|
||||
spm = uuid.uuid4().hex
|
||||
cache.set(spm, assets_id, 300)
|
||||
url = reverse_lazy('assets:asset-export') + '?spm=%s' % spm
|
||||
return JsonResponse({'redirect': url})
|
||||
|
||||
|
||||
class BulkImportAssetView(PermissionsMixin, JSONResponseMixin, FormView):
|
||||
form_class = forms.FileForm
|
||||
permission_classes = [IsOrgAdmin]
|
||||
|
||||
def form_valid(self, form):
|
||||
node_id = self.request.GET.get("node_id")
|
||||
node = get_object_or_none(Node, id=node_id) if node_id else Node.root()
|
||||
f = form.cleaned_data['file']
|
||||
det_result = chardet.detect(f.read())
|
||||
f.seek(0) # reset file seek index
|
||||
|
||||
file_data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode())
|
||||
csv_file = StringIO(file_data)
|
||||
reader = csv.reader(csv_file)
|
||||
csv_data = [row for row in reader]
|
||||
fields = [
|
||||
field for field in Asset._meta.fields
|
||||
if field.name not in [
|
||||
'date_created'
|
||||
]
|
||||
]
|
||||
header_ = csv_data[0]
|
||||
mapping_reverse = {field.verbose_name: field.name for field in fields}
|
||||
attr = [mapping_reverse.get(n, None) for n in header_]
|
||||
if None in attr:
|
||||
data = {'valid': False,
|
||||
'msg': 'Must be same format as '
|
||||
'template or export file'}
|
||||
return self.render_json_response(data)
|
||||
|
||||
created, updated, failed = [], [], []
|
||||
assets = []
|
||||
for row in csv_data[1:]:
|
||||
if set(row) == {''}:
|
||||
continue
|
||||
|
||||
asset_dict_raw = dict(zip(attr, row))
|
||||
asset_dict = dict()
|
||||
for k, v in asset_dict_raw.items():
|
||||
v = v.strip()
|
||||
if k == 'is_active':
|
||||
v = False if v in ['False', 0, 'false'] else True
|
||||
elif k == 'admin_user':
|
||||
v = get_object_or_none(AdminUser, name=v)
|
||||
elif k in ['port', 'cpu_count', 'cpu_cores']:
|
||||
try:
|
||||
v = int(v)
|
||||
except ValueError:
|
||||
v = ''
|
||||
elif k == 'domain':
|
||||
v = get_object_or_none(Domain, name=v)
|
||||
elif k == 'platform':
|
||||
v = v.lower().capitalize()
|
||||
if v != '':
|
||||
asset_dict[k] = v
|
||||
|
||||
asset = None
|
||||
asset_id = asset_dict.pop('id', None)
|
||||
if asset_id:
|
||||
asset = get_object_or_none(Asset, id=asset_id)
|
||||
if not asset:
|
||||
try:
|
||||
if len(Asset.objects.filter(hostname=asset_dict.get('hostname'))):
|
||||
raise Exception(_('already exists'))
|
||||
with transaction.atomic():
|
||||
asset = Asset.objects.create(**asset_dict)
|
||||
if node:
|
||||
asset.nodes.set([node])
|
||||
created.append(asset_dict['hostname'])
|
||||
assets.append(asset)
|
||||
except Exception as e:
|
||||
failed.append('%s: %s' % (asset_dict['hostname'], str(e)))
|
||||
else:
|
||||
for k, v in asset_dict.items():
|
||||
if v != '':
|
||||
setattr(asset, k, v)
|
||||
try:
|
||||
asset.save()
|
||||
updated.append(asset_dict['hostname'])
|
||||
except Exception as e:
|
||||
failed.append('%s: %s' % (asset_dict['hostname'], str(e)))
|
||||
|
||||
data = {
|
||||
'created': created,
|
||||
'created_info': 'Created {}'.format(len(created)),
|
||||
'updated': updated,
|
||||
'updated_info': 'Updated {}'.format(len(updated)),
|
||||
'failed': failed,
|
||||
'failed_info': 'Failed {}'.format(len(failed)),
|
||||
'valid': True,
|
||||
'msg': 'Created: {}. Updated: {}, Error: {}'.format(
|
||||
len(created), len(updated), len(failed))
|
||||
}
|
||||
return self.render_json_response(data)
|
||||
|
||||
|
|
|
@ -140,3 +140,14 @@ class NeedMFAVerify(permissions.BasePermission):
|
|||
if time.time() - mfa_verify_time < settings.SECURITY_MFA_VERIFY_TTL:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class CanUpdateSuperUser(permissions.BasePermission):
|
||||
def has_object_permission(self, request, view, obj):
|
||||
if request.method in ['GET', 'OPTIONS']:
|
||||
return True
|
||||
if request.user.is_superuser:
|
||||
return True
|
||||
if hasattr(obj, 'is_superuser') and obj.is_superuser:
|
||||
return False
|
||||
return True
|
||||
|
|
|
@ -373,7 +373,7 @@ defaults = {
|
|||
'HTTP_BIND_HOST': '0.0.0.0',
|
||||
'HTTP_LISTEN_PORT': 8080,
|
||||
'LOGIN_LOG_KEEP_DAYS': 90,
|
||||
'ASSETS_PERM_CACHE_TIME': 3600,
|
||||
'ASSETS_PERM_CACHE_TIME': 3600*24,
|
||||
'SECURITY_MFA_VERIFY_TTL': 3600,
|
||||
}
|
||||
|
||||
|
|
|
@ -21,9 +21,7 @@ from ..utils import (
|
|||
)
|
||||
from ..hands import User, Asset, Node, SystemUser, NodeSerializer
|
||||
from .. import serializers, const
|
||||
from ..mixins import (
|
||||
AssetsFilterMixin,
|
||||
)
|
||||
from ..mixins import AssetsFilterMixin
|
||||
from ..models import Action
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
@ -155,13 +153,13 @@ class UserGrantedAssetsApi(UserPermissionCacheMixin, AssetsFilterMixin, ListAPIV
|
|||
user = self.get_object()
|
||||
util = AssetPermissionUtil(user, cache_policy=self.cache_policy)
|
||||
assets = util.get_assets()
|
||||
for k, v in assets.items():
|
||||
for asset, system_users in assets.items():
|
||||
system_users_granted = []
|
||||
for system_user, actions in v.items():
|
||||
for system_user, actions in system_users.items():
|
||||
system_user.actions = actions
|
||||
system_users_granted.append(system_user)
|
||||
k.system_users_granted = system_users_granted
|
||||
queryset.append(k)
|
||||
asset.system_users_granted = system_users_granted
|
||||
queryset.append(system_users_granted)
|
||||
return queryset
|
||||
|
||||
def get_permissions(self):
|
||||
|
|
|
@ -224,6 +224,8 @@ class AssetPermissionCacheMixin:
|
|||
CACHE_TIME = settings.ASSETS_PERM_CACHE_TIME
|
||||
CACHE_POLICY_MAP = (('0', 'never'), ('1', 'using'), ('2', 'refresh'))
|
||||
cache_policy = '1'
|
||||
obj_id = ''
|
||||
_filter_id = None
|
||||
|
||||
@classmethod
|
||||
def is_not_using_cache(cls, cache_policy):
|
||||
|
@ -270,7 +272,6 @@ class AssetPermissionCacheMixin:
|
|||
def get_assets_from_cache(self):
|
||||
cached = cache.get(self.asset_key)
|
||||
if not cached:
|
||||
print("Refresh cache")
|
||||
self.update_cache()
|
||||
cached = cache.get(self.asset_key)
|
||||
return cached
|
||||
|
@ -320,7 +321,7 @@ class AssetPermissionCacheMixin:
|
|||
def get_meta_cache_key(self):
|
||||
cache_key = self.CACHE_META_KEY_PREFIX + '{obj_id}_{filter_id}'
|
||||
key = cache_key.format(
|
||||
obj_id=str(self.object.id), filter_id=self._filter_id
|
||||
obj_id=self.obj_id, filter_id=self._filter_id
|
||||
)
|
||||
return key
|
||||
|
||||
|
@ -345,7 +346,7 @@ class AssetPermissionCacheMixin:
|
|||
|
||||
def expire_cache_meta(self):
|
||||
cache_key = self.CACHE_META_KEY_PREFIX + '{obj_id}_*'
|
||||
key = cache_key.format(obj_id=str(self.object.id))
|
||||
key = cache_key.format(obj_id=self.obj_id)
|
||||
cache.delete_pattern(key)
|
||||
|
||||
def update_cache(self):
|
||||
|
@ -378,6 +379,15 @@ class AssetPermissionCacheMixin:
|
|||
key = cls.CACHE_KEY_PREFIX + '*'
|
||||
cache.delete_pattern(key)
|
||||
|
||||
def get_assets_without_cache(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_nodes_with_assets_without_cache(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_system_user_without_cache(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class AssetPermissionUtil(AssetPermissionCacheMixin):
|
||||
get_permissions_map = {
|
||||
|
@ -472,8 +482,6 @@ class AssetPermissionUtil(AssetPermissionCacheMixin):
|
|||
for node in nodes:
|
||||
pattern.add(r'^{0}$|^{0}:'.format(node.key))
|
||||
pattern = '|'.join(list(pattern))
|
||||
now = time.time()
|
||||
print("Get node assets start")
|
||||
if pattern:
|
||||
assets = Asset.objects.filter(nodes__key__regex=pattern)\
|
||||
.prefetch_related('nodes', "protocols")\
|
||||
|
@ -481,7 +489,6 @@ class AssetPermissionUtil(AssetPermissionCacheMixin):
|
|||
.distinct()
|
||||
else:
|
||||
assets = []
|
||||
print("Get node assets end, using: {}".format(time.time() - now))
|
||||
self.tree.add_assets_without_system_users(assets)
|
||||
assets = self.tree.get_assets()
|
||||
self._assets = assets
|
||||
|
|
|
@ -13,7 +13,8 @@ from rest_framework_bulk import BulkModelViewSet
|
|||
from rest_framework.pagination import LimitOffsetPagination
|
||||
|
||||
from common.permissions import (
|
||||
IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser
|
||||
IsOrgAdmin, IsCurrentUserOrReadOnly, IsOrgAdminOrAppUser,
|
||||
CanUpdateSuperUser,
|
||||
)
|
||||
from common.mixins import IDInCacheFilterMixin
|
||||
from common.utils import get_logger
|
||||
|
@ -37,7 +38,7 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
|
|||
search_fields = filter_fields
|
||||
queryset = User.objects.exclude(role=User.ROLE_APP)
|
||||
serializer_class = UserSerializer
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
permission_classes = (IsOrgAdmin, CanUpdateSuperUser)
|
||||
pagination_class = LimitOffsetPagination
|
||||
|
||||
def send_created_signal(self, users):
|
||||
|
@ -70,28 +71,6 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
|
|||
"""
|
||||
return not self.request.user.is_superuser and instance.is_superuser
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
"""
|
||||
rewrite because limit org_admin destroy superuser
|
||||
"""
|
||||
instance = self.get_object()
|
||||
if self._deny_permission(instance):
|
||||
data = {'msg': _("You do not have permission.")}
|
||||
return Response(data=data, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return super().destroy(request, *args, **kwargs)
|
||||
|
||||
def update(self, request, *args, **kwargs):
|
||||
"""
|
||||
rewrite because limit org_admin update superuser
|
||||
"""
|
||||
instance = self.get_object()
|
||||
if self._deny_permission(instance):
|
||||
data = {'msg': _("You do not have permission.")}
|
||||
return Response(data=data, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return super().update(request, *args, **kwargs)
|
||||
|
||||
def _bulk_deny_permission(self, instances):
|
||||
deny_instances = [i for i in instances if self._deny_permission(i)]
|
||||
if len(deny_instances) > 0:
|
||||
|
@ -108,26 +87,12 @@ class UserViewSet(IDInCacheFilterMixin, BulkModelViewSet):
|
|||
"""
|
||||
rewrite because limit org_admin update superuser
|
||||
"""
|
||||
partial = kwargs.pop('partial', False)
|
||||
|
||||
# restrict the update to the filtered queryset
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
if self._bulk_deny_permission(queryset):
|
||||
data = {'msg': _("You do not have permission.")}
|
||||
return Response(data=data, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
serializer = self.get_serializer(
|
||||
queryset, data=request.data, many=True, partial=partial,
|
||||
)
|
||||
|
||||
try:
|
||||
serializer.is_valid(raise_exception=True)
|
||||
except Exception as e:
|
||||
data = {'error': str(e)}
|
||||
return Response(data=data, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
self.perform_bulk_update(serializer)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return super().bulk_update(request, *args, **kwargs)
|
||||
|
||||
|
||||
class UserChangePasswordApi(generics.RetrieveUpdateAPIView):
|
||||
|
|
|
@ -39,6 +39,14 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
|||
'created_by': {'read_only': True}, 'source': {'read_only': True}
|
||||
}
|
||||
|
||||
def validate_role(self, value):
|
||||
request = self.context.get('request')
|
||||
if not request.user.is_superuser and value != User.ROLE_USER:
|
||||
role_display = dict(User.ROLE_CHOICES)[User.ROLE_USER]
|
||||
msg = _("Role limit to {}".format(role_display))
|
||||
raise serializers.ValidationError(msg)
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def validate_password(value):
|
||||
from ..utils import check_password_rules
|
||||
|
|
|
@ -2,27 +2,27 @@
|
|||
{% load i18n static %}
|
||||
{% 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>
|
||||
<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 %}
|
||||
<div class="uc pull-left m-r-5"><a href="{% url "users:user-create" %}" class="btn btn-sm btn-primary"> {% trans "Create user" %} </a></div>
|
||||
|
@ -92,23 +92,23 @@ function initTable() {
|
|||
}},
|
||||
{targets: 7, createdCell: function (td, cellData, rowData) {
|
||||
var update_btn = "";
|
||||
if (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin')) {
|
||||
update_btn = '<a class="btn btn-xs disabled btn-info">{% trans "Update" %}</a>';
|
||||
}
|
||||
else{
|
||||
{#if (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin')) {#}
|
||||
{# update_btn = '<a class="btn btn-xs disabled btn-info">{% trans "Update" %}</a>';#}
|
||||
{#}#}
|
||||
{#else{#}
|
||||
update_btn = '<a href="{% url "users:user-update" pk=DEFAULT_PK %}" class="btn btn-xs btn-info">{% trans "Update" %}</a>'.replace('00000000-0000-0000-0000-000000000000', cellData);
|
||||
}
|
||||
{#}#}
|
||||
|
||||
var del_btn = "";
|
||||
if (rowData.id === 1 || rowData.username === "admin" || rowData.username === "{{ request.user.username }}" || (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin'))) {
|
||||
del_btn = '<a class="btn btn-xs btn-danger m-l-xs" disabled>{% trans "Delete" %}</a>'
|
||||
.replace('{{ DEFAULT_PK }}', cellData)
|
||||
.replace('99991938', rowData.name);
|
||||
} else {
|
||||
{#if (rowData.id === 1 || rowData.username === "admin" || rowData.username === "{{ request.user.username }}" || (rowData.role === 'Admin' && ('{{ request.user.role }}' !== 'Admin'))) {#}
|
||||
{# del_btn = '<a class="btn btn-xs btn-danger m-l-xs" disabled>{% trans "Delete" %}</a>'#}
|
||||
{# .replace('{{ DEFAULT_PK }}', cellData)#}
|
||||
{# .replace('99991938', rowData.name);#}
|
||||
{#} else {#}
|
||||
del_btn = '<a class="btn btn-xs btn-danger m-l-xs btn_user_delete" data-uid="{{ DEFAULT_PK }}" data-name="99991938">{% trans "Delete" %}</a>'
|
||||
.replace('{{ DEFAULT_PK }}', cellData)
|
||||
.replace('99991938', rowData.name);
|
||||
}
|
||||
{#}#}
|
||||
$(td).html(update_btn + del_btn)
|
||||
}}],
|
||||
ajax_url: '{% url "api-users:user-list" %}',
|
||||
|
|
|
@ -29,9 +29,7 @@ urlpatterns = [
|
|||
|
||||
# User view
|
||||
path('user/', views.UserListView.as_view(), name='user-list'),
|
||||
path('user/export/', views.UserExportView.as_view(), name='user-export'),
|
||||
path('first-login/', views.UserFirstLoginView.as_view(), name='user-first-login'),
|
||||
path('user/import/', views.UserBulkImportView.as_view(), name='user-import'),
|
||||
path('user/create/', views.UserCreateView.as_view(), name='user-create'),
|
||||
path('user/<uuid:pk>/update/', views.UserUpdateView.as_view(), name='user-update'),
|
||||
path('user/update/', views.UserBulkUpdateView.as_view(), name='user-bulk-update'),
|
||||
|
|
|
@ -46,9 +46,7 @@ from ..signals import post_user_create
|
|||
|
||||
__all__ = [
|
||||
'UserListView', 'UserCreateView', 'UserDetailView',
|
||||
'UserUpdateView',
|
||||
'UserGrantedAssetView',
|
||||
'UserExportView', 'UserBulkImportView', 'UserProfileView',
|
||||
'UserUpdateView', 'UserGrantedAssetView', 'UserProfileView',
|
||||
'UserProfileUpdateView', 'UserPasswordUpdateView',
|
||||
'UserPublicKeyUpdateView', 'UserBulkUpdateView',
|
||||
'UserPublicKeyGenerateView',
|
||||
|
@ -223,147 +221,6 @@ class UserDetailView(PermissionsMixin, DetailView):
|
|||
return queryset
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
class UserExportView(View):
|
||||
def get(self, request):
|
||||
fields = [
|
||||
User._meta.get_field(name)
|
||||
for name in [
|
||||
'id', 'name', 'username', 'email', 'role',
|
||||
'wechat', 'phone', 'is_active', 'comment',
|
||||
]
|
||||
]
|
||||
spm = request.GET.get('spm', '')
|
||||
users_id = cache.get(spm, [])
|
||||
filename = 'users-{}.csv'.format(
|
||||
timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
|
||||
)
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename="%s"' % filename
|
||||
response.write(codecs.BOM_UTF8)
|
||||
users = User.objects.filter(id__in=users_id)
|
||||
writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL)
|
||||
|
||||
header = [field.verbose_name for field in fields]
|
||||
header.append(_('User groups'))
|
||||
writer.writerow(header)
|
||||
|
||||
for user in users:
|
||||
groups = ','.join([group.name for group in user.groups.all()])
|
||||
data = [getattr(user, field.name) for field in fields]
|
||||
data.append(groups)
|
||||
writer.writerow(data)
|
||||
|
||||
return response
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
users_id = json.loads(request.body).get('users_id', [])
|
||||
except ValueError:
|
||||
return HttpResponse('Json object not valid', status=400)
|
||||
spm = uuid.uuid4().hex
|
||||
cache.set(spm, users_id, 300)
|
||||
url = reverse('users:user-export') + '?spm=%s' % spm
|
||||
return JsonResponse({'redirect': url})
|
||||
|
||||
|
||||
class UserBulkImportView(PermissionsMixin, JSONResponseMixin, FormView):
|
||||
form_class = forms.FileForm
|
||||
permission_classes = [IsOrgAdmin]
|
||||
|
||||
def form_invalid(self, form):
|
||||
try:
|
||||
error = form.errors.values()[-1][-1]
|
||||
except Exception as e:
|
||||
error = _('Invalid file.')
|
||||
data = {
|
||||
'success': False,
|
||||
'msg': error
|
||||
}
|
||||
return self.render_json_response(data)
|
||||
|
||||
# todo: need be patch, method to long
|
||||
def form_valid(self, form):
|
||||
f = form.cleaned_data['file']
|
||||
det_result = chardet.detect(f.read())
|
||||
f.seek(0) # reset file seek index
|
||||
data = f.read().decode(det_result['encoding']).strip(codecs.BOM_UTF8.decode())
|
||||
csv_file = StringIO(data)
|
||||
reader = csv.reader(csv_file)
|
||||
csv_data = [row for row in reader]
|
||||
header_ = csv_data[0]
|
||||
fields = [
|
||||
User._meta.get_field(name)
|
||||
for name in [
|
||||
'id', 'name', 'username', 'email', 'role',
|
||||
'wechat', 'phone', 'is_active', 'comment',
|
||||
]
|
||||
]
|
||||
mapping_reverse = {field.verbose_name: field.name for field in fields}
|
||||
mapping_reverse[_('User groups')] = 'groups'
|
||||
attr = [mapping_reverse.get(n, None) for n in header_]
|
||||
if None in attr:
|
||||
data = {'valid': False,
|
||||
'msg': 'Must be same format as '
|
||||
'template or export file'}
|
||||
return self.render_json_response(data)
|
||||
|
||||
created, updated, failed = [], [], []
|
||||
for row in csv_data[1:]:
|
||||
if set(row) == {''}:
|
||||
continue
|
||||
user_dict = dict(zip(attr, row))
|
||||
id_ = user_dict.pop('id')
|
||||
for k, v in user_dict.items():
|
||||
if k in ['is_active']:
|
||||
if v.lower() == 'false':
|
||||
v = False
|
||||
else:
|
||||
v = bool(v)
|
||||
elif k == 'groups':
|
||||
groups_name = v.split(',')
|
||||
v = UserGroup.objects.filter(name__in=groups_name)
|
||||
else:
|
||||
continue
|
||||
user_dict[k] = v
|
||||
user = get_object_or_none(User, id=id_) if id_ and is_uuid(id_) else None
|
||||
if not user:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
groups = user_dict.pop('groups')
|
||||
user = User.objects.create(**user_dict)
|
||||
user.groups.set(groups)
|
||||
created.append(user_dict['username'])
|
||||
post_user_create.send(self.__class__, user=user)
|
||||
except Exception as e:
|
||||
failed.append('%s: %s' % (user_dict['username'], str(e)))
|
||||
else:
|
||||
for k, v in user_dict.items():
|
||||
if k == 'groups':
|
||||
user.groups.set(v)
|
||||
continue
|
||||
if v:
|
||||
setattr(user, k, v)
|
||||
try:
|
||||
user.save()
|
||||
updated.append(user_dict['username'])
|
||||
except Exception as e:
|
||||
failed.append('%s: %s' % (user_dict['username'], str(e)))
|
||||
|
||||
data = {
|
||||
'created': created,
|
||||
'created_info': 'Created {}'.format(len(created)),
|
||||
'updated': updated,
|
||||
'updated_info': 'Updated {}'.format(len(updated)),
|
||||
'failed': failed,
|
||||
'failed_info': 'Failed {}'.format(len(failed)),
|
||||
'valid': True,
|
||||
'msg': 'Created: {}. Updated: {}, Error: {}'.format(
|
||||
len(created), len(updated), len(failed))
|
||||
}
|
||||
return self.render_json_response(data)
|
||||
|
||||
|
||||
class UserGrantedAssetView(PermissionsMixin, DetailView):
|
||||
model = User
|
||||
template_name = 'users/user_granted_asset.html'
|
||||
|
|
Loading…
Reference in New Issue