i;i++)r.push({name:a,value:n[i]});else null!==n&&"undefined"!=typeof n&&r.push({name:this.name,value:n})}}),e.param(r)},e.fn.fieldValue=function(t){for(var r=[],a=0,n=this.length;n>a;a++){var i=this[a],o=e.fieldValue(i,t);null===o||"undefined"==typeof o||o.constructor==Array&&!o.length||(o.constructor==Array?e.merge(r,o):r.push(o))}return r},e.fieldValue=function(t,r){var a=t.name,n=t.type,i=t.tagName.toLowerCase();if(void 0===r&&(r=!0),r&&(!a||t.disabled||"reset"==n||"button"==n||("checkbox"==n||"radio"==n)&&!t.checked||("submit"==n||"image"==n)&&t.form&&t.form.clk!=t||"select"==i&&-1==t.selectedIndex))return null;if("select"==i){var o=t.selectedIndex;if(0>o)return null;for(var s=[],u=t.options,c="select-one"==n,l=c?o+1:u.length,f=c?o:0;l>f;f++){var m=u[f];if(m.selected){var d=m.value;if(d||(d=m.attributes&&m.attributes.value&&!m.attributes.value.specified?m.text:m.value),c)return d;s.push(d)}}return s}return e(t).val()},e.fn.clearForm=function(t){return this.each(function(){e("input,select,textarea",this).clearFields(t)})},e.fn.clearFields=e.fn.clearInputs=function(t){var r=/^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i;return this.each(function(){var a=this.type,n=this.tagName.toLowerCase();r.test(a)||"textarea"==n?this.value="":"checkbox"==a||"radio"==a?this.checked=!1:"select"==n?this.selectedIndex=-1:"file"==a?/MSIE/.test(navigator.userAgent)?e(this).replaceWith(e(this).clone(!0)):e(this).val(""):t&&(t===!0&&/hidden/.test(a)||"string"==typeof t&&e(this).is(t))&&(this.value="")})},e.fn.resetForm=function(){return this.each(function(){("function"==typeof this.reset||"object"==typeof this.reset&&!this.reset.nodeType)&&this.reset()})},e.fn.enable=function(e){return void 0===e&&(e=!0),this.each(function(){this.disabled=!e})},e.fn.selected=function(t){return void 0===t&&(t=!0),this.each(function(){var r=this.type;if("checkbox"==r||"radio"==r)this.checked=t;else if("option"==this.tagName.toLowerCase()){var a=e(this).parent("select");t&&a[0]&&"select-one"==a[0].type&&a.find("option").selected(!1),this.selected=t}})},e.fn.ajaxSubmit.debug=!1});
\ No newline at end of file
diff --git a/apps/users/forms.py b/apps/users/forms.py
index 8c21011ce..efe98e67b 100644
--- a/apps/users/forms.py
+++ b/apps/users/forms.py
@@ -34,6 +34,13 @@ class UserCreateForm(forms.ModelForm):
}
+class UserBulkImportForm(forms.ModelForm):
+
+ class Meta:
+ model = User
+ fields = ['username', 'email', 'enable_otp', 'role']
+
+
class UserUpdateForm(forms.ModelForm):
class Meta:
diff --git a/apps/users/templates/users/_user_import_modal.html b/apps/users/templates/users/_user_import_modal.html
new file mode 100644
index 000000000..99a61d126
--- /dev/null
+++ b/apps/users/templates/users/_user_import_modal.html
@@ -0,0 +1,19 @@
+{% extends '_modal.html' %}
+{% load i18n %}
+{% block modal_id %}user_import_modal{% endblock %}
+{% block modal_title%}{% trans "Import User" %}{% endblock %}
+{% block modal_body %}
+{% trans "Hint: your excel should organized in the following format." %}
+{% trans "* You should have a very worksheet named `users`." %}
+{% trans "* Rows in this worksheet: username, email, enable_opt(0, 1), role(one of ['Admin', 'User'])" %}
+
+{% 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 36029c59b..2e34b560b 100644
--- a/apps/users/templates/users/user_list.html
+++ b/apps/users/templates/users/user_list.html
@@ -17,7 +17,8 @@ div.dataTables_wrapper div.dataTables_filter {
{% endblock %}
{% block table_search %}{% endblock %}
{% block table_container %}
-
+
+
@@ -51,10 +52,12 @@ div.dataTables_wrapper div.dataTables_filter {
{% include "users/_user_bulk_update_modal.html" %}
+{% include "users/_user_import_modal.html" %}
{% endblock %}
{% block content_bottom_left %}
{% endblock %}
{% block custom_foot_js %}
+
{% endblock %}
diff --git a/apps/users/urls.py b/apps/users/urls.py
index 5847e30b5..eb7797d61 100644
--- a/apps/users/urls.py
+++ b/apps/users/urls.py
@@ -23,6 +23,7 @@ urlpatterns = [
url(r'^user/(?P[0-9]+)/granted-asset', views.UserGrantedAssetView.as_view(), name='user-granted-asset'),
url(r'^user/(?P[0-9]+)/login-history', views.UserDetailView.as_view(), name='user-login-history'),
url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'),
+ url(r'^import/$', views.BulkImportUserView.as_view(), name='user-import'),
url(r'^user/(?P[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'),
url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'),
url(r'^user/(?P[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'),
diff --git a/apps/users/views.py b/apps/users/views.py
index 8dec28a22..930d1cea9 100644
--- a/apps/users/views.py
+++ b/apps/users/views.py
@@ -2,6 +2,7 @@
from __future__ import unicode_literals
+from django import forms
from django.conf import settings
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.contrib.auth.mixins import LoginRequiredMixin
@@ -23,10 +24,11 @@ from django.views.generic.detail import DetailView
from formtools.wizard.views import SessionWizardView
+from common.mixins import JSONResponseMixin
from common.utils import get_object_or_none, get_logger
from .models import User, UserGroup
from .forms import UserCreateForm, UserUpdateForm, UserGroupForm, UserLoginForm, UserInfoForm, UserKeyForm, \
- UserPrivateAssetPermissionForm
+ UserPrivateAssetPermissionForm, UserBulkImportForm
from .utils import AdminUserRequiredMixin, user_add_success_next, send_reset_password_mail
from .hands import AssetPermission, get_user_granted_asset_groups, get_user_granted_assets
@@ -443,3 +445,66 @@ class UserGrantedAssetView(AdminUserRequiredMixin, SingleObjectMixin, ListView):
}
kwargs.update(context)
return super(UserGrantedAssetView, self).get_context_data(**kwargs)
+
+
+class FileForm(forms.Form):
+ excel = forms.FileField()
+
+
+class BulkImportUserView(AdminUserRequiredMixin, JSONResponseMixin, FormView):
+ form_class = FileForm
+
+ def form_invalid(self, form):
+ try:
+ error = form.errors.values()[-1][-1]
+ except Exception as e:
+ print e
+ error = _('Invalid file.')
+ data = {
+ 'success': False,
+ 'msg': error
+ }
+ return self.render_json_response(data)
+
+ def form_valid(self, form):
+ from openpyxl import load_workbook
+ try:
+ wb = load_workbook(form.cleaned_data['excel'])
+ ws = wb['users']
+ except Exception as e:
+ print e
+ error = _('Not a valid Excel file.')
+ data = {
+ 'success': False,
+ 'msg': error
+ }
+ return self.render_json_response(data)
+
+ errors = []
+ for index, row in enumerate(ws.rows):
+ user_data = [cell.value for cell in row]
+ if len(user_data) != 4:
+ errors.append("Row {}: invalid user data format.".format(index))
+ continue
+ username, email, enable_otp, role = user_data
+ data = {
+ 'username': username,
+ 'email': email,
+ 'enable_otp': True if enable_otp in ['T', '1', 1, True] else False,
+ 'role': role
+ }
+ form = UserBulkImportForm(data, auto_id=False)
+ if form.is_valid():
+ form.save()
+ else:
+ form_errors = form.errors.as_data()
+ for key, err_list in form_errors.iteritems():
+ error_line = "{} :".format(key)
+ for errs in err_list:
+ error_line = "{}{}".format(error_line, ";".join([err for err in errs.messages]))
+ errors.append("Row {}: {}".format(index, error_line))
+ data = {
+ 'success': True if not errors else False,
+ 'msg': 'ok' if not errors else '
'.join(errors)
+ }
+ return self.render_json_response(data)
diff --git a/requirements.txt b/requirements.txt
index 311786d8d..ac60097ab 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,6 +13,7 @@ wcwidth==0.1.7
websocket-client==0.37.0
djangorestframework==3.4.5
ForgeryPy==0.1
+openpyxl==2.4.0
paramiko==2.0.2
celery==3.1.23
ansible==2.1.1.0