From eae580e51f11cbc06c0baa8dd9560ce1b240722a Mon Sep 17 00:00:00 2001
From: ibuler
Date: Thu, 24 Nov 2016 15:45:08 +0800
Subject: [PATCH] Update user import and export
---
apps/common/templatetags/common_tags.py | 3 +-
apps/jumpserver/settings.py | 1 +
apps/media/files/user_import_template.xlsx | Bin 0 -> 8592 bytes
apps/users/forms.py | 2 +-
.../templates/users/_user_import_modal.html | 18 +++-
apps/users/templates/users/user_list.html | 16 ++--
apps/users/views.py | 85 ++++++++++++------
7 files changed, 83 insertions(+), 42 deletions(-)
create mode 100644 apps/media/files/user_import_template.xlsx
diff --git a/apps/common/templatetags/common_tags.py b/apps/common/templatetags/common_tags.py
index 253f6d741..f096df945 100644
--- a/apps/common/templatetags/common_tags.py
+++ b/apps/common/templatetags/common_tags.py
@@ -44,6 +44,7 @@ def join_attr(seq, attr=None, sep=None):
print(seq)
return sep.join(seq)
+
@register.filter
-def IntToStr(value):
+def int_to_str(value):
return str(value)
\ No newline at end of file
diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py
index 3f31995d6..e2934c58f 100644
--- a/apps/jumpserver/settings.py
+++ b/apps/jumpserver/settings.py
@@ -98,6 +98,7 @@ TEMPLATES = [
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.static',
'django.template.context_processors.request',
+ 'django.template.context_processors.media',
],
},
},
diff --git a/apps/media/files/user_import_template.xlsx b/apps/media/files/user_import_template.xlsx
new file mode 100644
index 0000000000000000000000000000000000000000..5c06ab198067e6cd774c453539db168e4018e56f
GIT binary patch
literal 8592
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
{% endblock %}
{% block modal_confirm_id %}btn_user_import{% endblock %}
diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html
index dd2747102..7cae42497 100644
--- a/apps/users/templates/users/user_list.html
+++ b/apps/users/templates/users/user_list.html
@@ -114,16 +114,16 @@ $(document).ready(function(){
$form.find('.help-block').remove();
function success (data) {
if (data.valid === false) {
- var $help = $form.find('.help-block');
$('', {class: 'help-block text-danger'}).html(data.msg).insertAfter($('#id_users'));
} else {
-{# $('#user_import_modal').modal('hide');#}
-{# var $data_table = $('#user_list_table').DataTable();#}
-{# toastr.success("{% trans 'Import User Success.' %}");#}
- $('', {class: 'help-block text-danger'}).html(data.errors.join(',')).insertAfter($('#id_users'));
- $('', {class: 'help-block text-warning'}).html(data.updated.join(',')).insertAfter($('#id_users'));
- $('', {class: 'help-block text-primary'}).html(data.created.join(',')).insertAfter($('#id_users'));
-{# $data_table.ajax.reload();#}
+ $('#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();
}
}
$form.ajaxSubmit({success: success});
diff --git a/apps/users/views.py b/apps/users/views.py
index 37c398012..595d110aa 100644
--- a/apps/users/views.py
+++ b/apps/users/views.py
@@ -3,8 +3,11 @@
from __future__ import unicode_literals
import json
import uuid
-from io import BytesIO
+import codecs
+from openpyxl import Workbook
+from openpyxl.writer.excel import save_virtual_workbook
+from openpyxl import load_workbook
import unicodecsv as csv
from django import forms
from django.utils import timezone
@@ -37,8 +40,6 @@ from .utils import AdminUserRequiredMixin, user_add_success_next, send_reset_pas
from .hands import write_login_log_async
from . import forms
-
-
logger = get_logger(__name__)
@@ -96,7 +97,11 @@ class UserListView(AdminUserRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super(UserListView, self).get_context_data(**kwargs)
- context.update({'app': _('Users'), 'action': _('User list'), 'groups': UserGroup.objects.all()})
+ context.update({
+ 'app': _('Users'),
+ 'action': _('User list'),
+ 'groups': UserGroup.objects.all()
+ })
return context
@@ -496,41 +501,61 @@ class BulkImportUserView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
return self.render_json_response(data)
def form_valid(self, form):
- users_csv = form.cleaned_data['users']
- users_csv_f = csv.reader(users_csv, encoding='utf-8')
+ try:
+ wb = load_workbook(form.cleaned_data['file'])
+ ws = wb.get_active_sheet()
+ except Exception as e:
+ print(e)
+ data = {'valid': False, 'msg': 'Not a valid Excel file'}
+ return self.render_json_response(data)
+
+ rows = ws.rows
header_need = ["name", 'username', 'email', 'groups', "role", "phone", "wechat", "comment"]
- header = next(users_csv_f)
+ header = [col.value for col in next(rows)]
print(header)
if header != header_need:
- data = {'valid': False, 'msg': 'Must be same format as export csv: name, ...'}
+ data = {'valid': False, 'msg': 'Must be same format as template or export file'}
return self.render_json_response(data)
created = []
updated = []
- errors = []
- for row in users_csv_f:
- user_dict = dict(zip(header, row))
- groups_name = user_dict.pop('groups').split(',')
- groups = UserGroup.objects.filter(name__in=groups_name)
+ failed = []
+ for row in rows:
+ user_dict = dict(zip(header, [col.value for col in row]))
+ groups_name = user_dict.pop('groups')
+ if groups_name:
+ groups_name = groups_name.split(',')
+ groups = UserGroup.objects.filter(name__in=groups_name)
+ else:
+ groups = None
try:
user = User.objects.create(**user_dict)
- user.groups.add(*tuple(groups))
- user.save()
created.append(user_dict['username'])
except IntegrityError:
user = User.objects.filter(username=user_dict['username'])
+ if not user:
+ failed.append(user_dict['username'])
+ continue
user.update(**user_dict)
- user[0].groups.add(*tuple(groups))
+ user = user[0]
updated.append(user_dict['username'])
except TypeError:
- errors.append(user_dict['username'])
+ failed.append(user_dict['username'])
+ user = None
+
+ if user and groups:
+ user.groups.add(*tuple(groups))
+ user.save()
data = {
'created': created,
+ 'created_info': 'Created {}'.format(len(created)),
'updated': updated,
- 'errors': errors,
+ '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(errors))
+ 'msg': 'Created: {}. Updated: {}, Error: {}'.format(len(created), len(updated), len(failed))
}
return self.render_json_response(data)
@@ -544,22 +569,24 @@ class ExportUserCsvView(View):
return HttpResponse('May be expired', status=404)
users = User.objects.filter(id__in=users_id)
- filename = 'users-%s.csv' % timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')
- response = HttpResponse(content_type='application/csv')
- response['Content-Disposition'] = 'attachment; filename="%s"' % filename
- writer = csv.writer(response, delimiter=str(","), lineterminator='\n',
- quoting=csv.QUOTE_ALL, dialect='excel')
+ wb = Workbook()
+ ws = wb.active
+ ws.title = 'User'
header = ["name", 'username', 'email', 'groups', "role", "phone", "wechat", "comment"]
- writer.writerow(header)
+ ws.append(header)
+
for user in users:
- writer.writerow([user.name, user.username, user.email,
- ','.join([group.name for group in user.groups.all()]),
- user.role, user.phone, user.wechat, user.comment])
+ ws.append([user.name, user.username, user.email,
+ ','.join([group.name for group in user.groups.all()]),
+ user.role, user.phone, user.wechat, user.comment])
+
+ filename = 'users-{}.xlsx'.format(timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S'))
+ response = HttpResponse(save_virtual_workbook(wb), content_type='application/vnd.ms-excel')
+ response['Content-Disposition'] = 'attachment; filename="%s"' % filename
return response
def post(self, request, *args, **kwargs):
try:
- print(request.body)
users_id = json.loads(request.body).get('users_id', [])
except ValueError:
return HttpResponse('Json object not valid', status=400)