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 zcmeHsg;yNe_I2Z~A-Fq18VErGf#3~|ySuwP!8N#hkOW9*+#3r7*Weo5gNEP)zRt|N z-^^s@`wQNyT3xq$Rh`qT?%n&Gea}$>BOnq0kO8Ow0DuM{CMZ0h4F>=uAOZl70I2Y~ z;*Jh(<_>O#YTi!fuKFBa_IA{Ti15t$0C?E*|2zJRX8@8SukeWzH|!F$MDvZ1!9klk zG|QuN4F8ETw$Zwb>2x*wSY2JC8v47^U8TVoX@8U~|4Skm09+27U zlge1#v~~N_l$B^q8G&+$r&2UUeug{{$oN_xFsyZ0MS!K;l#E}7tP>W7v+7lHQLWzH z=*$xGLBj^^n-32(G1U_>&ShVy+wB{mUPpJ3vhmJKvCAd#%kh}I@;fCOEmam1rTN3b zA}A`MZzx$w!1xwQO>pV}IoYF={e+XQcTrN}yqbx5>O~$?<0)?=x=6G9#`%s%-ggQdf^KK! zqErKEdjZ@(V4Qn+KmaKHjgd-z3Y}*#n90EihXG@xp^LelE0E*o^?ywKFQ(sLULBG& zCX2v@8*v5HGB+ACP;NBPsk!*7lq_pm3?&-r!`f8K+IemhEpQxBZW{Tv*PF8o>;^=7mGjIKeGH|fu1@rRXeZU}N^NAC)_c)|2WZd6*B9==k*Pu6lmfaU%%Ia)o zLI%D~ZhSM4FJIu6u7~+1wBwkm;PA+jyqkz|KSWdMF$3y%#<^urbm6So7AD;81>F~j z@#u{7bJLA{i+tofJBph(Aleo$mDP-m+7vVLxU6X0@f`)SiBf@~se#fFQBP(*^K0hG z355Npiq(zi6U)mXR>c{~50}{Xi)4Oi+pZF0M6~p$JezP5MXC{yRYjsqyv?nv2=$Fz z1*;W-twRo$v@KiC=`79_8j60JZ`Krj_yf(V`XTcWv`^PYCy3l&fevY8Zx82lAZ(@o z37PX8R}w)OT)bgH7YFtP3^IQO%UiV}heb}@R-#=I+}6buvmq?RA<=yln|$WrLYYyX z`ZVIRf+V%_{QeG426dSQJ(cgYF6k3a9u}HILmI5#5OUIIHhJ=-QUdWB5{ifSMjF3t z2IEsTu{6~nyW9Zs3@Vm8bo0suDIhQK5IKcQ)KT4gGl(8r9hEd7hcHQh<-s9_CS5PU z0sWA0>r7+_*fX{#KEu(tI(Am|6%kZb7mgcs0oGrh27dQvuoWq^J|*NI`SSifQ`bHz z4Yta_$>$K2LdgbTtovPl)d8=9wAoU7@Qe!o*F!Ejyl5Uwz!BS&7mSSC&7LN zFa3ZuXp5wBVH3`x7&5a9^yBBewfIom{rFfluPnB8=3={Zv!41-0B^(hfYB3xtu6fE zciq*8)VF;GnuJ)?hHJib6#2)w++_^H&HY!MhG$BNz3+2s$l}kdYu1X*>T55lgDo#( zE4bn*`l{^4cnFee6|lw(n1>A)FL6j+#|OHUqJxgA9(2>DYrd}NqFFvbjERyi6tcSV zl-C7DKYbz|AtKJbEfUlYMIW1-cRhO4UxUssamEwn0}nj8ZRqp8NzGV3QlQSMaG<`} zwUmn*qIXzo-}N`&kPc>bJ_I$o0ckOeW36(;U1u{6RJ1tj)@pHnL=*Ps+}|ZI*4jfe z<&)g_vi06ATHymN2F$ot820>f=|ShG)@BQh=k?pcqOAFE)?QZ^xM%ANC?E2fm3T$P zCVt1KW>O0<>+S96-6#52oT&~gR<**yO%M!c1i$0V)ymk#+)UNY#oEEr^=D*)z`!=e zN$^E;or=vdT(&Mxt_Qb(-d_E^inhC`$Vd7-XC*(1yZqb_7=)m@{a~zMc5nkET0*2G zH?3opG4DO~p&hYClw#aiTl-G;z;@z?eqCd`_cZUuecs^1HFL-kmwaE{I?|DgawkVO zu*PM{MFsQKO0!1Q$E_Mvv80YpkCD_i6GSh%>tlQb(%BPuHu0ER*wT3!zr$5fxBGmI zlex~Ilw^y8{S}+#RH^;cWCyz+U)LgPD3CRgGo1mVjWVojOxacaO51(Fw8-DSrF4Ay z#k|fsNlUkQBKQbuRgsGCgcG+en(pm?eZJqP{W{BE=AbNvSG-qRBKwR2jJ=VEM9E9b z1YVHR%RDCIo>;wz4wqYcD~0m%*eXLnYTnS)`W;(@hP6y=`K-Y-zXfDUt0`vFqd=Bl z;AQp0xE6WeP=D(QvEDEvp0wU{E#}<@#6j3UJI`S0&X4LvvoD2E`-=zRA9riBEx=hS zpa#*c@VHgW_kbqh_&(}xQ@ZXAn%>Ee#0@b$Vq@^^25bZ)MZ`7yoh$90%wmCBWIf)h zqCM1an;IwsKXOlVEX!7^^HtBet&D1P{(8mMSFM|rL$H76%KFkZq zpg@C(66fqf`Jh&@h)r1?9W^^9Py`D%(1=%B89T43#6d zn&_T4t{N}>fagLhD3>ojtOIuvb*219ND0nF>xtJm`jhQP5IY0?PO=`x;^f& z)s=I&u^L#4csLevWom0b_Q|;{+?yh5Qj76-+L+$OX+1j8$FAcD zfymqttMDm$CUJ5x0#B#sxCFMyULmU!WGw`PVGJV}14^Iu>Vt7yF-f>eF88l5EAvDo zVIP54R|i*Xihc2v6e4%y6E3_*t85&Oe%FVV0k3{=-E8df0jUDk`i8jgXQ_m*w|2&U zyzI|!mH@Wt#3fwy@@VT?NkeT~cV{;B#qAeY%!Pdj3&=R684^63)TOfC1yO zPy4`%xHm$0nIcD^;xNv&G;1>7&2B{|l5U&wQ)U;}K&03W1r^@<_z~s1IDJoR z=B7>Yup{NE-YMH8VG8~ro|QUr)svy_lBZ1z)gwpxQ`^HO=ez=Li3zwF-l4~jwlhwj zQ?lmVfG(T|2a!9X)Yl(+OS*%@ctF4^(V(G*-Ohk3w*b2yu*VgyAmD*R6Lk1QfzA&acqy>B^IS#P@iF6I=` z8wWZAj63!gmM$p#khi{onhZ0mZFu&0`8>Dp9Nz>-+X+y^WzLMWf?;x0lR7PNM74xKI_F}h`BT57M|C6oG6f^N$;SF) zm<^O5h=;dX`s`rsT1-qPf$7r#1(I@WcuVsJkFCQD^>Q`v$o2<~XN8$6JEgp;Bz{e$ zq->)QG=0?IWhBT_AfEmZb}gC({e(xO>)8~0)_E8d#K=z@7GJmgTKEGglf0=psT3vE zp1?5yRch0-XY-A#9)wJ(^V4Zn**?eitvnPCI|dWpzO)qD8VU+&1w|U4A(b--1naGj zF^wT%mDbn>>Cn?pY~d{hN@!#kC5*Lbv(0?j_f?QC`LO!i;4TG<{xG#gEk<6R;WAo8 zEsJm`x(6GIVIO7mIZyxs%ghW7@jWlg$G8gH0Vj}{x`&5#&yq6nOJ&(>Ej;H032Cd3 zy(Uy4x9+9E!g#4pg2qpRUj>%&*xR2CvX4qmqynojM=MOzn3|XypG%fx+O09hTXJVG zv6kcExSb6;qzHCAVcY_OQeB82#?;1l(DaBPk_!klOPlC1ZtV^59BN$!=v!!5zVIB) za)*IdL0`4JutbqA2evz?BW^lI1i+u(K9lpedD9D0;*VFOMV$|+F)KVKFqvLIsG-*u z?UzuK(aY4WM%hfLYOe^w@^3(44pQQqe#?x2?8$wqErgZjd@QkMAzq^+WZ)nw#QBv20a9w#YZR1Q!#xH zTG+W&(#5-*Fr34XybSY6j^Uq6?$z`S3YegSjbS*MBC2~CfLSooI!MEo& zW__UTx?SVya(ez{=Q>7TYp&Y8dGb7iH0i=DdjG=ec|UPqhGG4nh|p=LzteYKz0>&2 zqzjt|IK9)(6ypH;z|F?dXS#e3FiHDQv71=a%vS)rg9A%VSbvBRH*Y)hKh#ItPpP|% z@tp$oC~mWZhnpVBi5w12B|lI#pOrzG7w;aGK39hw>i%^uVs}OWyiyEzL;OOeW{a97 zsqoy0BEBzv#ox2<%8ivDTCl*$@JL7qovW}$i6OZRv7>DenTePLY$nr`#M6oO?25<# znwGa=&8@k3iZ(ETvD4P}t2S2HR5{Cb*n$0q2zSq>4K6<`K_KCa;wp!ySG!~D!SEm4jDlyigC%%YI zK_8xIe2!o%qDBX>#IiD!i-s+LvRAoYwvbuOs2V_Vla*8qrpsuoNjb_nNzt)4!rg@DcvrFkz)W|DAXp;Vob$ z>in|GO84;e!OaKpj*_WfBo?H9)*CB6yPL;aC77_92e?&Buw6l1`LcX}b3qbbRuu9L zYa?wyItgXiWpRiEYNNwN!#wPx+YrXzE8`IS7l0dHJ*AFD-qMx4H>5vV^M z@rrzR#(l`U* zXK9D9b?kd6L_JVv5@PB%+C00nFnboEslhKz`}ncnTDZaL^H9@7m3G~xjsW{3#)=}I zb(SZHOcJZTN*q8Lax@_N~?}XB9MI||D4TPG3KB*n~smlhd zfK9@vkVMy?N;K=T3~#cN(he~*+6LjoIO#`$RH(78%W~2_D4Sjp3WRUoD=iy$$|lTB z1n<{AcBcrp1PXG{FA!E8&vHsNjW;9*2^DFfGjJaUYS(zZFYqwqh zjz(cD^vDG(h4_xUVPxVgiy8AgSg7q}nLbDo4sUJNc#2%;gse?Nunb;gpCD9(n_O#5k3^`$eXR=sxD93t2OBW+5Km~8uMuoY*5 zd{&MaIGoLk>fSitrS)`U4rAqVs?x?pv5(ZcE(h%FvZ^8GtQIv)fn!N+VJqlg*`TEM z$(x5B99GE6pVMc~c^U7t?i72)EfK2UZ4(&@jXcNY+NaG6k8FB9hlzKo-#tr1yGNTB z5$XK;q_cMh%wIBVaR2a%hZ4TY-EqP7*oXZXRPk+6`gb zNGBNb@n97$Ge=V;7e^;opsAya`5#@G{}r}i;UFeS6xNj>koc`DA?%!#hF(aY3UHT1 z!k47>Nc%CVL*HOdHn9|l;DqY%F?N-dUxrMjGx*&@%p1%0oY6-0y|zGJy=Y8H(MTa& zHExa>5~wcwo$4kD7?JKBK|%pCX4X!hG>bY~Dyc)AT0T3ltSS@xOh<5l`bGBNs4eIoKX?_{9B^hAG`&#Yu8iSs7+rAw>V!r)lVy%3~xz0_X5nPkJyg|-M7cj@7o25kvH#sXu0fDZPDwusOJB*B{%U5mh_tM1%8UWkSCUF+zTyOh)GP|_Xb^Q)G11oe7@pb{G&QIhLhY2 zaX60YE#X1+H=lWywZ;9w{u_4D#H@o3G44UMGxL4rNR^vf#Tl+}7aRf$dwK)TWrQQB;@6H2dSc}|S@lLN#h73Cuumk+3^Bz}G$Lo{E#bM0n8>o)FR)Z5 z6h)4y0{kC-Y>3J%Vra8YIw@ImtdhS=Y+35C3Y;LH-x>Kcb@&7zwtz0027d420$N8_J)*{vXa} B$<_b> literal 0 HcmV?d00001 diff --git a/apps/users/forms.py b/apps/users/forms.py index 472a8892c..56be49f0f 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -141,4 +141,4 @@ class UserGroupPrivateAssetPermissionForm(forms.ModelForm): class FileForm(forms.Form): - users = forms.FileField() + file = forms.FileField() diff --git a/apps/users/templates/users/_user_import_modal.html b/apps/users/templates/users/_user_import_modal.html index 4ee538908..3e56c6a47 100644 --- a/apps/users/templates/users/_user_import_modal.html +++ b/apps/users/templates/users/_user_import_modal.html @@ -3,13 +3,25 @@ {% block modal_id %}user_import_modal{% endblock %} {% block modal_title%}{% trans "Import user" %}{% endblock %} {% block modal_body %} -

{% trans " * CSV format should be same as export" %}

+

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

{% csrf_token %}
- - + + {% trans 'Download' %} +
+
+ +
+

+

+

+

+

+

+

+

{% 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)