diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 1a66e2d38..499ad2cf0 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -86,7 +86,6 @@ class Asset(models.Model): def __unicode__(self): return '%s <%s: %s>' % (self.hostname, self.ip, self.port) - __str__ = __unicode__ @property diff --git a/apps/assets/models/group.py b/apps/assets/models/group.py index c7e72acfe..b31b80871 100644 --- a/apps/assets/models/group.py +++ b/apps/assets/models/group.py @@ -23,6 +23,7 @@ class AssetGroup(models.Model): def __unicode__(self): return self.name + __str__ = __unicode__ class Meta: ordering = ['name'] diff --git a/apps/assets/models/idc.py b/apps/assets/models/idc.py index fbbde2c98..d79d897fc 100644 --- a/apps/assets/models/idc.py +++ b/apps/assets/models/idc.py @@ -36,6 +36,7 @@ class IDC(models.Model): def __unicode__(self): return self.name + __str__ = __unicode__ @classmethod def initial(cls): diff --git a/apps/assets/models/user.py b/apps/assets/models/user.py index 04a4282d8..1504f9c2f 100644 --- a/apps/assets/models/user.py +++ b/apps/assets/models/user.py @@ -50,7 +50,6 @@ class AdminUser(models.Model): def __unicode__(self): return self.name - __str__ = __unicode__ @property diff --git a/apps/assets/templates/assets/_asset_import_modal.html b/apps/assets/templates/assets/_asset_import_modal.html index d226ace94..8ef73e531 100644 --- a/apps/assets/templates/assets/_asset_import_modal.html +++ b/apps/assets/templates/assets/_asset_import_modal.html @@ -7,11 +7,14 @@ {% csrf_token %}
- {% trans 'Download' %} + {% trans 'Download' %}
+ + {% trans 'If set id, will use this id update asset existed' %} +

diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index 3844e2fd8..893de66a6 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -1,14 +1,14 @@ # coding:utf-8 from __future__ import absolute_import, unicode_literals + +import csv import json import uuid +import codecs +from io import StringIO -from openpyxl import Workbook -from openpyxl.writer.excel import save_virtual_workbook -from openpyxl import load_workbook from django.conf import settings -from django.db import IntegrityError -from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ from django.views.generic import TemplateView, ListView, View from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from django.urls import reverse_lazy @@ -206,45 +206,42 @@ class AssetModalListView(AdminUserRequiredMixin, ListView): @method_decorator(csrf_exempt, name='dispatch') class AssetExportView(View): - @staticmethod - def get_asset_attr(asset, attr): - if attr in ['admin_user', 'idc']: - return getattr(asset, attr) - elif attr in ['status', 'type', 'env']: - return getattr(asset, 'get_{}_display'.format(attr))() - else: - return getattr(asset, attr) - def get(self, request, *args, **kwargs): spm = request.GET.get('spm', '') - assets_id = cache.get(spm) - if not assets_id and not isinstance(assets_id, list): - return HttpResponse('May be expired', status=404) - + assets_id = cache.get(spm, [Asset.objects.first().id]) + print(assets_id) + fields = [ + field for field in Asset._meta.fields + if field.name not in [ + 'date_created' + ] + ] + 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) - wb = Workbook() - ws = wb.active - ws.title = 'Asset' - header = ['hostname', 'ip', 'port', 'admin_user', 'idc', 'memory', 'disk', - 'mac_address', 'other_ip', 'remote_card_ip', 'os', 'cabinet_no', - 'cabinet_pos', 'number', 'status', 'type', 'env', 'sn', 'comment'] - ws.append(header) + writer = csv.writer(response, dialect='excel', + quoting=csv.QUOTE_MINIMAL) + + header = [field.verbose_name for field in fields] + header.append(_('Asset groups')) + writer.writerow(header) for asset in assets: - ws.append([self.get_asset_attr(asset, attr) for attr in header]) - - filename = 'assets-{}.xlsx'.format(timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')) - response = HttpResponse(save_virtual_workbook(wb), content_type='applications/vnd.ms-excel') - response['Content-Disposition'] = 'attachment; filename="%s"' % filename + groups = ','.join([group.name for group in asset.groups.all()]) + data = [getattr(asset, field.name) for field in fields] + data.append(groups) + writer.writerow(data) return response def post(self, request, *args, **kwargs): try: assets_id = json.loads(request.body).get('assets_id', []) - print(assets_id) except ValueError: return HttpResponse('Json object not valid', status=400) - spm = uuid.uuid4().get_hex() + spm = uuid.uuid4().hex cache.set(spm, assets_id, 300) url = reverse_lazy('assets:asset-export') + '?spm=%s' % spm return JsonResponse({'redirect': url}) @@ -254,67 +251,74 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): form_class = forms.FileForm def form_valid(self, form): - 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'} + file = form.cleaned_data['file'] + data = file.read().decode('utf-8').strip( + codecs.BOM_UTF8.decode('utf-8')) + csv_file = StringIO(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} + mapping_reverse[_('Asset 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) - rows = ws.rows - header_all = ['hostname', 'ip', 'port', 'admin_user', 'idc', 'cpu', 'memory', 'disk', - 'mac_address', 'other_ip', 'remote_card_ip', 'os', 'cabinet_no', - 'cabinet_pos', 'number', 'status', 'type', 'env', 'sn', 'comment'] - header_min = ['hostname', 'ip', 'port', 'admin_user', 'comment'] - header = [col.value for col in next(rows)] - if not set(header).issubset(set(header_all)) and not set(header).issuperset(set(header_min)): - 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 rows: - asset_dict = dict(zip(header, [col.value for col in row])) - if asset_dict.get('admin_user', None): - admin_user = get_object_or_none(AdminUser, name=asset_dict['admin_user']) - asset_dict['admin_user'] = admin_user - - if asset_dict.get('idc'): - idc = get_object_or_none(IDC, name=asset_dict['idc']) - asset_dict['idc'] = idc - - if asset_dict.get('type'): - asset_display_type_map = dict(zip(dict(Asset.TYPE_CHOICES).values(), dict(Asset.TYPE_CHOICES).keys())) - asset_type = asset_display_type_map.get(asset_dict['type'], 'Server') - asset_dict['type'] = asset_type - - if asset_dict.get('status'): - asset_display_status_map = dict(zip(dict(Asset.STATUS_CHOICES).values(), - dict(Asset.STATUS_CHOICES).keys())) - asset_status = asset_display_status_map.get(asset_dict['status'], 'In use') - asset_dict['status'] = asset_status - - if asset_dict.get('env'): - asset_display_env_map = dict(zip(dict(Asset.ENV_CHOICES).values(), - dict(Asset.ENV_CHOICES).keys())) - asset_env = asset_display_env_map.get(asset_dict['env'], 'Prod') - asset_dict['env'] = asset_env - - try: - Asset.objects.create(**asset_dict) - created.append(asset_dict['ip']) - except IntegrityError as e: - asset = Asset.objects.filter(ip=asset_dict['ip'], port=asset_dict['port']) - if not asset: - failed.append(asset_dict['ip']) + created, updated, failed = [], [], [] + for row in csv_data[1:]: + if set(row) == {''}: + continue + asset_dict = dict(zip(attr, row)) + id_ = asset_dict.pop('id', 0) + asset = get_object_or_none(Asset, id=id_) + for k, v in asset_dict.items(): + if k == 'idc': + v = get_object_or_none(IDC, name=v) + elif k == 'is_active': + v = bool(v) + elif k == 'admin_user': + v = get_object_or_none(AdminUser, name=v) + elif k in ['port', 'cabinet_pos', 'cpu_count', 'cpu_cores']: + try: + v = int(v) + except ValueError: + v = 0 + elif k == 'groups': + groups_name = v.split(',') + v = AssetGroup.objects.filter(name__in=groups_name) + else: continue - asset.update(**asset_dict) - updated.append(asset_dict['ip']) - except TypeError as e: - print(e) - failed.append(asset_dict['ip']) + asset_dict[k] = v + + if not asset: + try: + groups = asset_dict.pop('groups') + asset = Asset.objects.create(**asset_dict) + asset.groups.set(groups) + created.append(asset_dict['hostname']) + except IndexError as e: + failed.append('%s: %s' % (asset_dict['hostname'], str(e))) + else: + for k, v in asset_dict.items(): + if k == 'groups': + asset.groups.set(v) + continue + 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, @@ -324,7 +328,8 @@ class BulkImportAssetView(AdminUserRequiredMixin, JSONResponseMixin, FormView): 'failed': failed, 'failed_info': 'Failed {}'.format(len(failed)), 'valid': True, - 'msg': 'Created: {}. Updated: {}, Error: {}'.format(len(created), len(updated), len(failed)) + 'msg': 'Created: {}. Updated: {}, Error: {}'.format( + len(created), len(updated), len(failed)) } return self.render_json_response(data) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 83ead6169..a81ab5ab0 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -260,7 +260,7 @@ MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/') + '/' # Use django-bootstrap-form to format template, input max width arg -BOOTSTRAP_COLUMN_COUNT = 11 +# BOOTSTRAP_COLUMN_COUNT = 11 # Init data or generate fake data source for development FIXTURE_DIRS = [os.path.join(BASE_DIR, 'fixtures'), ] diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 1c72f8960..4772033de 100644 Binary files a/apps/locale/zh/LC_MESSAGES/django.mo and b/apps/locale/zh/LC_MESSAGES/django.mo differ diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 23ab70107..dc9099344 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Jumpserver 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-04-07 17:03+0800\n" +"POT-Creation-Date: 2017-04-08 21:58+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -78,7 +78,7 @@ msgstr "登录地址" #: assets/templates/assets/system_user_list.html:21 perms/models.py:40 #: perms/templates/perms/asset_permission_detail.html:98 #: users/models/group.py:19 users/models/user.py:43 -#: users/templates/users/user_detail.html:109 +#: users/templates/users/user_detail.html:113 #: users/templates/users/user_group_detail.html:70 #: users/templates/users/user_group_list.html:14 #: users/templates/users/user_profile.html:118 @@ -134,7 +134,7 @@ msgstr "在线session" #: assets/templates/assets/asset_list.html:74 perms/models.py:33 #: perms/templates/perms/asset_permission_create_update.html:47 #: users/templates/users/_select_user_modal.html:18 -#: users/templates/users/user_detail.html:126 +#: users/templates/users/user_detail.html:130 #: users/templates/users/user_list.html:29 #: users/templates/users/user_profile.html:63 msgid "Active" @@ -234,8 +234,8 @@ msgstr "其它" #: assets/templates/assets/system_user_detail.html:137 #: perms/templates/perms/asset_permission_create_update.html:67 #: users/templates/users/_user.html:49 -#: users/templates/users/user_detail.html:158 -#: users/templates/users/user_detail.html:166 +#: users/templates/users/user_detail.html:162 +#: users/templates/users/user_detail.html:170 #: users/templates/users/user_password_update.html:58 #: users/templates/users/user_profile.html:139 #: users/templates/users/user_profile.html:147 @@ -259,7 +259,7 @@ msgstr "重置" #: audits/templates/audits/proxy_log_online_list.html:124 #: perms/templates/perms/asset_permission_create_update.html:68 #: users/templates/users/_user.html:50 -#: users/templates/users/first_login.html:60 +#: users/templates/users/first_login.html:62 #: users/templates/users/forgot_password.html:44 #: users/templates/users/user_asset_permission.html:100 #: users/templates/users/user_group_asset_permission.html:100 @@ -569,7 +569,7 @@ msgstr "主机名原始" #: assets/templates/assets/idc_detail.html:93 #: assets/templates/assets/system_user_detail.html:94 perms/models.py:37 #: perms/templates/perms/asset_permission_detail.html:94 -#: users/models/user.py:47 users/templates/users/user_detail.html:97 +#: users/models/user.py:47 users/templates/users/user_detail.html:101 msgid "Created by" msgstr "创建者" @@ -733,22 +733,17 @@ msgstr "选择系统用户" msgid "Import asset" msgstr "导入资产" -#: assets/templates/assets/_asset_import_modal.html:6 -#: users/templates/users/_user_import_modal.html:6 -msgid "Download template or use export excel format" -msgstr "下载模板" - -#: assets/templates/assets/_asset_import_modal.html:10 +#: assets/templates/assets/_asset_import_modal.html:9 #: users/templates/users/_user_import_modal.html:10 msgid "Template" msgstr "模板" -#: assets/templates/assets/_asset_import_modal.html:11 +#: assets/templates/assets/_asset_import_modal.html:10 #: users/templates/users/_user_import_modal.html:11 msgid "Download" msgstr "下载" -#: assets/templates/assets/_asset_import_modal.html:14 +#: assets/templates/assets/_asset_import_modal.html:13 msgid "Asset excel file" msgstr "资产excel" @@ -888,13 +883,13 @@ msgid "Platform" msgstr "" #: assets/templates/assets/asset_detail.html:152 -#: users/templates/users/user_detail.html:101 +#: users/templates/users/user_detail.html:105 #: users/templates/users/user_profile.html:88 msgid "Date joined" msgstr "创建日期" #: assets/templates/assets/asset_detail.html:168 -#: users/templates/users/user_detail.html:120 +#: users/templates/users/user_detail.html:124 #: users/templates/users/user_profile.html:130 msgid "Quick modify" msgstr "快速修改" @@ -929,9 +924,9 @@ msgstr "添加到资产组" #: assets/templates/assets/system_user_asset.html:96 #: assets/templates/assets/system_user_list.html:102 #: assets/templates/assets/user_asset_list.html:167 templates/_modal.html:16 -#: users/templates/users/user_detail.html:337 -#: users/templates/users/user_detail.html:362 -#: users/templates/users/user_detail.html:385 +#: users/templates/users/user_detail.html:341 +#: users/templates/users/user_detail.html:366 +#: users/templates/users/user_detail.html:389 #: users/templates/users/user_group_create_update.html:45 #: users/templates/users/user_group_list.html:92 #: users/templates/users/user_list.html:171 @@ -940,8 +935,8 @@ msgid "Confirm" msgstr "确认" #: assets/templates/assets/asset_detail.html:374 -#: users/templates/users/user_detail.html:271 -#: users/templates/users/user_detail.html:284 +#: users/templates/users/user_detail.html:275 +#: users/templates/users/user_detail.html:288 msgid "Update successfully!" msgstr "更新成功" @@ -985,8 +980,8 @@ msgstr "批量更新" #: assets/templates/assets/idc_list.html:94 #: assets/templates/assets/system_user_list.html:97 #: assets/templates/assets/user_asset_list.html:162 -#: users/templates/users/user_detail.html:332 -#: users/templates/users/user_detail.html:357 +#: users/templates/users/user_detail.html:336 +#: users/templates/users/user_detail.html:361 #: users/templates/users/user_group_list.html:87 #: users/templates/users/user_list.html:166 msgid "Are you sure?" @@ -1149,10 +1144,8 @@ msgstr "IDC删除失败" #: assets/templates/assets/system_user_asset.html:20 #: assets/templates/assets/system_user_detail.html:21 -#, fuzzy -#| msgid "Attach Asset" msgid "Attached assets" -msgstr "关联资产" +msgstr "关联的资产" #: assets/templates/assets/system_user_asset.html:28 msgid "Assets of " @@ -1179,7 +1172,6 @@ msgid "Attach AssetGroup" msgstr "添加到资产组" #: assets/templates/assets/system_user_detail.html:79 -#: templates/_header_bar.html:41 msgid "Home" msgstr "" @@ -1494,7 +1486,7 @@ msgid "Select user groups" msgstr "添加到用户组" #: perms/models.py:35 perms/templates/perms/asset_permission_detail.html:86 -#: users/models/user.py:46 users/templates/users/user_detail.html:93 +#: users/models/user.py:46 users/templates/users/user_detail.html:97 #: users/templates/users/user_profile.html:96 msgid "Date expired" msgstr "失效日期" @@ -1527,7 +1519,7 @@ msgstr "添加资产组" #: perms/templates/perms/asset_permission_asset.html:146 #: perms/templates/perms/asset_permission_detail.html:170 -#: users/templates/users/user_detail.html:194 +#: users/templates/users/user_detail.html:198 msgid "Join" msgstr "加入" @@ -1570,7 +1562,7 @@ msgstr "创建授权规则" #: perms/templates/perms/asset_permission_list.html:13 templates/_nav.html:13 #: users/models/user.py:34 users/templates/users/_select_user_modal.html:16 -#: users/templates/users/user_detail.html:177 +#: users/templates/users/user_detail.html:181 #: users/templates/users/user_list.html:28 msgid "User group" msgstr "用户组" @@ -1659,18 +1651,18 @@ msgstr "注销登录" msgid "Login" msgstr "登录" +#: templates/_header_bar.html:41 templates/_nav.html:4 +msgid "Dashboard" +msgstr "仪表盘" + #: templates/_modal.html:15 msgid "Close" msgstr "关闭" -#: templates/_nav.html:4 -msgid "Dashboard" -msgstr "仪表盘" - #: templates/_nav.html:9 users/templates/users/user_group_create_update.html:28 #: users/views/group.py:31 users/views/group.py:45 users/views/group.py:80 -#: users/views/group.py:105 users/views/login.py:185 users/views/user.py:54 -#: users/views/user.py:70 users/views/user.py:106 users/views/user.py:118 +#: users/views/group.py:105 users/views/login.py:185 users/views/user.py:58 +#: users/views/user.py:74 users/views/user.py:110 users/views/user.py:122 msgid "Users" msgstr "用户管理" @@ -1734,22 +1726,20 @@ msgid "" " " msgstr "" "\n" -" 你的信息不完整,请点击 链接 to 补充完整\n" +" 你的信息不完整,请点击 链接 " +" 补充完整\n" " " - #: templates/base.html:37 -#, python-format msgid "" "\n" " Your ssh-public-key has been expired. Please click this link to update your ssh-public-key.\n" +"\"%(user_pubkey_update)s\"> this link to update your ssh-public-key.\n" " " msgstr "" "\n" -" 你的SSH key已经过期,点击 链接 更新 \n" +" 你的SSH key已经过期,点击 链接 " +"更新 \n" " " #: templates/captcha/image.html:3 @@ -1815,7 +1805,7 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: users/forms.py:36 users/templates/users/user_detail.html:185 +#: users/forms.py:36 users/templates/users/user_detail.html:189 msgid "Join user groups" msgstr "添加到用户组" @@ -1876,7 +1866,8 @@ msgid "Wechat" msgstr "微信" #: users/models/user.py:39 users/templates/users/_user.html:36 -#: users/templates/users/user_detail.html:140 +#: users/templates/users/user_detail.html:93 +#: users/templates/users/user_detail.html:144 msgid "Enable OTP" msgstr "二次验证" @@ -1917,6 +1908,10 @@ msgstr "组" msgid "Import user" msgstr "导入" +#: users/templates/users/_user_import_modal.html:6 +msgid "Download template or use export excel format" +msgstr "下载模板" + #: users/templates/users/_user_import_modal.html:14 msgid "Users excel file" msgstr "用户excel" @@ -1934,11 +1929,11 @@ msgstr "首次登陆" msgid "Step" msgstr "Step" -#: users/templates/users/first_login.html:57 +#: users/templates/users/first_login.html:59 msgid "First step" msgstr "第一步" -#: users/templates/users/first_login.html:58 +#: users/templates/users/first_login.html:60 msgid "Prev step" msgstr "上一步" @@ -1968,8 +1963,8 @@ msgid "Captcha invalid" msgstr "验证码错误" #: users/templates/users/reset_password.html:45 -#: users/templates/users/user_detail.html:155 -#: users/templates/users/user_detail.html:323 +#: users/templates/users/user_detail.html:159 +#: users/templates/users/user_detail.html:327 #: users/templates/users/user_profile.html:136 users/utils.py:71 msgid "Reset password" msgstr "重置密码" @@ -1987,7 +1982,7 @@ msgstr "设置" #: users/templates/users/user_granted_asset.html:18 #: users/templates/users/user_group_asset_permission.html:18 #: users/templates/users/user_group_granted_asset.html:18 -#: users/views/user.py:119 +#: users/views/user.py:123 msgid "User detail" msgstr "用户详情" @@ -2015,7 +2010,7 @@ msgid "Revoke Successfully!" msgstr "回收成功" #: users/templates/users/user_create.html:4 -#: users/templates/users/user_list.html:16 users/views/user.py:70 +#: users/templates/users/user_list.html:16 users/views/user.py:74 msgid "Create user" msgstr "创建用户" @@ -2023,47 +2018,47 @@ msgstr "创建用户" msgid "Reset link will be generated and sent to the user. " msgstr "生成重置密码连接,通过邮件发送给用户" -#: users/templates/users/user_detail.html:105 +#: users/templates/users/user_detail.html:109 #: users/templates/users/user_profile.html:92 msgid "Last login" msgstr "最后登录" -#: users/templates/users/user_detail.html:163 +#: users/templates/users/user_detail.html:167 msgid "Reset ssh key" msgstr "重置密钥" -#: users/templates/users/user_detail.html:322 +#: users/templates/users/user_detail.html:326 msgid "An e-mail has been sent to the user\\'s mailbox." msgstr "已发送邮件到用户邮箱" -#: users/templates/users/user_detail.html:333 +#: users/templates/users/user_detail.html:337 msgid "" "This will reset the user's password. A password-reset email will be sent to " "the user\\'s mailbox." msgstr "" -#: users/templates/users/user_detail.html:347 +#: users/templates/users/user_detail.html:351 msgid "" "The reset-ssh-public-key E-mail has been sent successfully. Please inform " "the user to update his new ssh public key." msgstr "" -#: users/templates/users/user_detail.html:348 +#: users/templates/users/user_detail.html:352 #: users/templates/users/user_profile.html:144 msgid "Reset SSH public key" msgstr "重置SSH密钥" -#: users/templates/users/user_detail.html:358 +#: users/templates/users/user_detail.html:362 msgid "This will reset the user\\" msgstr "重置" -#: users/templates/users/user_detail.html:375 +#: users/templates/users/user_detail.html:379 #: users/templates/users/user_profile.html:170 msgid "Successfully updated the SSH public key." msgstr "" -#: users/templates/users/user_detail.html:376 #: users/templates/users/user_detail.html:380 +#: users/templates/users/user_detail.html:384 #: users/templates/users/user_profile.html:171 #: users/templates/users/user_profile.html:176 msgid "User SSH Public Key Update" @@ -2071,23 +2066,17 @@ msgstr "ssh密钥" #: users/templates/users/user_granted_asset.html:32 #: users/templates/users/user_group_granted_asset.html:32 -#, fuzzy -#| msgid "Asset group list" msgid "Assets granted of " -msgstr "资产组列表" +msgstr "授权资产" #: users/templates/users/user_granted_asset.html:68 #: users/templates/users/user_group_granted_asset.html:68 -#, fuzzy -#| msgid "Asset group list" msgid "Asset groups granted of " -msgstr "资产组列表" +msgstr "授权资产组" #: users/templates/users/user_group_asset_permission.html:71 -#, fuzzy -#| msgid "Create perm" msgid "Quick create permission for user group" -msgstr "创建权限" +msgstr "快速授权" #: users/templates/users/user_group_create_update.html:30 msgid "Select User" @@ -2194,7 +2183,7 @@ msgstr "指纹" msgid "Update public key" msgstr "更新密钥" -#: users/templates/users/user_update.html:3 users/views/user.py:106 +#: users/templates/users/user_update.html:3 users/views/user.py:110 msgid "Update user" msgstr "编辑用户" @@ -2336,12 +2325,6 @@ msgstr "用户组列表" msgid "Update user group" msgstr "编辑用户组" -#: users/views/group.py:106 -#, fuzzy -#| msgid "User group detail" -msgid "User Group Detail" -msgstr "资产组详情" - #: users/views/login.py:76 msgid "Logout success" msgstr "退出登录成功" @@ -2383,16 +2366,15 @@ msgstr "密码不一致" msgid "First login" msgstr "首次登陆" -#: users/views/user.py:55 +#: users/views/user.py:59 msgid "User list" msgstr "用户列表" -#: users/views/user.py:66 users/views/user.py:334 +#: users/views/user.py:70 users/views/user.py:344 #, python-brace-format msgid "Create user {name} successfully." msgstr "创建用户 {name} 成功" -#: users/views/user.py:175 +#: users/views/user.py:188 msgid "Invalid file." msgstr "文件错误" - diff --git a/apps/templates/_base_create_update.html b/apps/templates/_base_create_update.html index c011feb71..fdc5269e2 100644 --- a/apps/templates/_base_create_update.html +++ b/apps/templates/_base_create_update.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% load i18n %} {% load static %} -{% load bootstrap %} +{% load bootstrap3 %} {% block custom_head_css_js %} diff --git a/apps/templates/base.html b/apps/templates/base.html index 61882ae1a..b53dfe98a 100644 --- a/apps/templates/base.html +++ b/apps/templates/base.html @@ -33,9 +33,9 @@ {% block update_public_key_message %} {% if user.is_authenticated and not user.is_public_key_valid %}

- {% url 'users:user-profile' as profile_url %} + {% url 'users:user-pubkey-update' as user_pubkey_update %} {% blocktrans %} - Your ssh-public-key has been expired. Please click this link to update your ssh-public-key. + Your ssh-public-key has been expired. Please click this link to update your ssh-public-key. {% endblocktrans %}
{% endif %} diff --git a/apps/users/templates/users/first_login.html b/apps/users/templates/users/first_login.html index 402a9a5f4..99aff6ccf 100644 --- a/apps/users/templates/users/first_login.html +++ b/apps/users/templates/users/first_login.html @@ -1,7 +1,7 @@ {% extends 'base.html' %} {% load static %} {% load i18n %} -{% load bootstrap3 %} +{% load bootstrap3 %} {% block custom_head_css_js %} @@ -44,10 +44,12 @@ {% if wizard.form.forms %} {{ wizard.form.management_form }} {% for form in wizard.form.forms %} - {{ form|bootstrap }} + {% bootstrap_form form %} +{# {{ form|bootstrap }}#} {% endfor %} {% else %} - {{ wizard.form|bootstrap }} +{# {{ wizard.form|bootstrap }}#} + {% bootstrap_form wizard.form %} {% endif %} diff --git a/apps/users/templates/users/user_detail.html b/apps/users/templates/users/user_detail.html index 290c24498..d2617389f 100644 --- a/apps/users/templates/users/user_detail.html +++ b/apps/users/templates/users/user_detail.html @@ -89,6 +89,10 @@ {% trans 'Role' %}: {{ user_object.get_role_display }} + + {% trans 'Enable OTP' %}: + {{ user_object.enable_otp|yesno:"Yes,No,Unknown"}} + {% trans 'Date expired' %}: {{ user_object.date_expired|date:"Y-m-j H:i:s" }} diff --git a/apps/users/templates/users/user_pubkey_update.html b/apps/users/templates/users/user_pubkey_update.html index de10272c7..ee621eb8d 100644 --- a/apps/users/templates/users/user_pubkey_update.html +++ b/apps/users/templates/users/user_pubkey_update.html @@ -61,6 +61,7 @@ +

{% trans 'Update public key' %}

{% bootstrap_field form.public_key layout="horizontal" %}
diff --git a/apps/users/views/group.py b/apps/users/views/group.py index dda420606..12697fece 100644 --- a/apps/users/views/group.py +++ b/apps/users/views/group.py @@ -103,7 +103,7 @@ class UserGroupDetailView(AdminUserRequiredMixin, DetailView): users = User.objects.exclude(id__in=self.object.users.all()) context = { 'app': _('Users'), - 'action': _('User Group Detail'), + 'action': _('User group detail'), 'users': users, } kwargs.update(context) diff --git a/apps/users/views/user.py b/apps/users/views/user.py index 8e0d881e9..e5c2a25c9 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -4,10 +4,9 @@ from __future__ import unicode_literals import json import uuid - -from openpyxl import load_workbook -from openpyxl import Workbook -from openpyxl.writer.excel import save_virtual_workbook +import csv +import codecs +from io import StringIO from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.messages.views import SuccessMessageMixin @@ -31,7 +30,7 @@ from .. import forms from ..models import User, UserGroup from ..utils import AdminUserRequiredMixin, user_add_success_next from common.mixins import JSONResponseMixin -from common.utils import get_logger +from common.utils import get_logger, get_object_or_none from perms.models import AssetPermission __all__ = ['UserListView', 'UserCreateView', 'UserDetailView', @@ -123,34 +122,44 @@ class UserDetailView(AdminUserRequiredMixin, DetailView): return super(UserDetailView, self).get_context_data(**kwargs) +USER_ATTR_MAPPING = ( + ('name', 'Name'), + ('username', 'Username'), + ('email', 'Email'), + ('groups', 'User groups'), + ('role', 'Role'), + ('phone', 'Phone'), + ('wechat', 'Wechat'), + ('comment', 'Comment'), +) + + @method_decorator(csrf_exempt, name='dispatch') class UserExportView(View): def get(self, request): + mapping = [ + (k, _(v)) for k, v in USER_ATTR_MAPPING + ] spm = request.GET.get('spm', '') - users_id = cache.get(spm) - if not users_id and not isinstance(users_id, list): - return HttpResponse('May be expired', status=404) - + users_id = cache.get(spm, ['1']) + 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) - print(users) - wb = Workbook() - ws = wb.active - ws.title = 'User' - header = ["name", 'username', 'email', 'groups', - "role", "phone", "wechat", "comment"] - ws.append(header) + writer = csv.writer(response, dialect='excel', quoting=csv.QUOTE_MINIMAL) + + header = [v for k, v in mapping] + writer.writerow(header) for user in users: - 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]) + groups = ','.join([group.name for group in user.groups.all()]) + writer.writerow([ + user.name, user.username, user.email, groups, + 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='applications/vnd.ms-excel') - response[ - 'Content-Disposition'] = 'attachment; filename="%s"' % filename return response def post(self, request): @@ -158,7 +167,7 @@ class UserExportView(View): users_id = json.loads(request.body).get('users_id', []) except ValueError: return HttpResponse('Json object not valid', status=400) - spm = uuid.uuid4().get_hex() + spm = uuid.uuid4().hex cache.set(spm, users_id, 300) url = reverse('users:user-export') + '?spm=%s' % spm return JsonResponse({'redirect': url}) @@ -179,54 +188,51 @@ class UserBulkImportView(AdminUserRequiredMixin, JSONResponseMixin, FormView): return self.render_json_response(data) def form_valid(self, form): - 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'} + file = form.cleaned_data['file'] + data = file.read().decode('utf-8').strip(codecs.BOM_UTF8.decode('utf-8')) + csv_file = StringIO(data) + reader = csv.reader(csv_file) + csv_data = [row for row in reader] + header_ = csv_data[0] + mapping_reverse = {_(v): k for k, v in USER_ATTR_MAPPING} + user_attr = [mapping_reverse.get(n, None) for n in header_] + if None in user_attr: + data = {'valid': False, + 'msg': 'Must be same format as ' + 'template or export file'} return self.render_json_response(data) - rows = ws.rows - header_need = ["name", 'username', 'email', 'groups', - "role", "phone", "wechat", "comment"] - header = [col.value for col in next(rows)] - print(header) - if header != header_need: - 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 rows: - user_dict = dict(zip(header, [col.value for col in row])) + created, updated, failed = [], [], [] + for row in csv_data[1:]: + if set(row) == {''}: + continue + user_dict = dict(zip(user_attr, 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_add_success_next(user) - created.append(user_dict['username']) - except User.IntegrityError as e: - user = User.objects.filter(username=user_dict['username']) - if not user: - failed.append(user_dict['username']) - continue - user.update(**user_dict) - user = user[0] - updated.append(user_dict['username']) - except TypeError as e: - print(e) - failed.append(user_dict['username']) - user = None - + username = user_dict['username'] + user = get_object_or_none(User, username=username) + if not user: + try: + user = User.objects.create(**user_dict) + created.append(user_dict['username']) + user_add_success_next(user) + except Exception as e: + failed.append('%s: %s' % (user_dict['username'], str(e))) + else: + for k, v in user_dict.items(): + 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))) if user and groups: - user.groups.add(*tuple(groups)) + user.groups = groups user.save() data = {