diff --git a/README.md b/README.md index 14a2c4143..0177c1abe 100644 --- a/README.md +++ b/README.md @@ -26,36 +26,7 @@ Jumpserver是一款使用Python, Django开发的开源跳板机系统, 助力互 ### Install 安装 - 1. 安装 Python3 - 略 - - 2. 安装依赖 - - ``` - $ cd requirements && yum -y install $(cat rpm_requirements.txt) && pip install -r requirements.txt - ``` - - 3. 修改配置文件 - - ``` - $ cp config_example.py config.py - ``` - - 4. 修改表结构 - - ``` - $ cd apps && python manage.py makemigrations && python manage.py migrate - ``` - - 5. 运行 - - ``` - $ python run_server.py - ``` - - 6. 其它 - - 整合luna,coco需要nginx来配合, 详见详细安装文档 +    [详细安装](https://github.com/jumpserver/jumpserver/wiki/v0.5.0-%E5%9F%BA%E4%BA%8E-CentOS7) ### Usage 使用 diff --git a/apps/assets/api.py b/apps/assets/api.py index e5c5a44ff..5f1c73bf0 100644 --- a/apps/assets/api.py +++ b/apps/assets/api.py @@ -18,9 +18,10 @@ from rest_framework.response import Response from rest_framework_bulk import BulkModelViewSet from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView from django.shortcuts import get_object_or_404 -from django.db.models import Q +from django.db.models import Q, Count +from rest_framework.pagination import LimitOffsetPagination -from common.mixins import IDInFilterMixin +from common.mixins import CustomFilterMixin from common.utils import get_logger from .hands import IsSuperUser, IsValidUser, IsSuperUserOrAppUser, \ get_user_granted_assets @@ -34,12 +35,16 @@ from .tasks import update_asset_hardware_info_manual, test_admin_user_connectabi logger = get_logger(__file__) -class AssetViewSet(IDInFilterMixin, BulkModelViewSet): +class AssetViewSet(CustomFilterMixin, BulkModelViewSet): """ API endpoint that allows Asset to be viewed or edited. """ + filter_fields = ("hostname", "ip") + search_fields = filter_fields + ordering_fields = ("hostname", "ip", "port", "cluster", "type", "env", "cpu_cores") queryset = Asset.objects.all() serializer_class = serializers.AssetSerializer + pagination_class = LimitOffsetPagination permission_classes = (IsSuperUserOrAppUser,) def get_queryset(self): @@ -78,11 +83,11 @@ class UserAssetListView(generics.ListAPIView): return queryset -class AssetGroupViewSet(IDInFilterMixin, BulkModelViewSet): +class AssetGroupViewSet(CustomFilterMixin, BulkModelViewSet): """ Asset group api set, for add,delete,update,list,retrieve resource """ - queryset = AssetGroup.objects.all() + queryset = AssetGroup.objects.all().annotate(asset_count=Count("assets")) serializer_class = serializers.AssetGroupSerializer permission_classes = (IsSuperUser,) @@ -112,7 +117,7 @@ class GroupAddAssetsApi(generics.UpdateAPIView): return Response({'error': serializer.errors}, status=400) -class ClusterViewSet(IDInFilterMixin, BulkModelViewSet): +class ClusterViewSet(CustomFilterMixin, BulkModelViewSet): """ Cluster api set, for add,delete,update,list,retrieve resource """ @@ -153,7 +158,7 @@ class ClusterAddAssetsApi(generics.UpdateAPIView): return Response({'error': serializer.errors}, status=400) -class AdminUserViewSet(IDInFilterMixin, BulkModelViewSet): +class AdminUserViewSet(CustomFilterMixin, BulkModelViewSet): """ Admin user api set, for add,delete,update,list,retrieve resource """ @@ -189,7 +194,7 @@ class SystemUserViewSet(BulkModelViewSet): permission_classes = (IsSuperUserOrAppUser,) -class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): +class AssetListUpdateApi(CustomFilterMixin, ListBulkCreateUpdateDestroyAPIView): """ Asset bulk update api """ diff --git a/apps/assets/forms.py b/apps/assets/forms.py index 72d38f85f..824ef3fcd 100644 --- a/apps/assets/forms.py +++ b/apps/assets/forms.py @@ -93,7 +93,7 @@ class AssetBulkUpdateForm(forms.ModelForm): model = Asset fields = [ 'assets', 'port', 'groups', "cluster", - 'type', 'env', 'status', + 'type', 'env', ] widgets = { 'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': _('Select asset groups')}), @@ -124,20 +124,25 @@ class AssetGroupForm(forms.ModelForm): label=_('Asset'), required=False, widget=forms.SelectMultiple( - attrs={'class': 'select2', 'data-placeholder': _('Select assets')}) + attrs={'class': 'select2', 'data-placeholder': _('Select assets')} ) + ) - def __init__(self, *args, **kwargs): - if kwargs.get('instance', None): + def __init__(self, **kwargs): + instance = kwargs.get('instance') + if instance: initial = kwargs.get('initial', {}) - initial['assets'] = kwargs['instance'].assets.all() - super(AssetGroupForm, self).__init__(*args, **kwargs) + initial.update({ + 'assets': instance.assets.all(), + }) + kwargs['initial'] = initial + super().__init__(**kwargs) - def _save_m2m(self): - super(AssetGroupForm, self)._save_m2m() - assets = self.cleaned_data['assets'] - self.instance.assets.clear() - self.instance.assets.add(*tuple(assets)) + def save(self, commit=True): + group = super().save(commit=commit) + assets= self.cleaned_data['assets'] + group.assets.set(assets) + return group class Meta: model = AssetGroup @@ -253,9 +258,10 @@ class SystemUserForm(forms.ModelForm): # Admin user assets define, let user select, save it in form not in view auto_generate_key = forms.BooleanField(initial=True, required=False) # Form field name can not start with `_`, so redefine it, - password = forms.CharField(widget=forms.PasswordInput, required=False, max_length=128, strip=True) + password = forms.CharField(widget=forms.PasswordInput, required=False, + max_length=128, strip=True, label=_("Password")) # Need use upload private key file except paste private key content - private_key_file = forms.FileField(required=False) + private_key_file = forms.FileField(required=False, label=_("Private key")) def save(self, commit=True): # Because we define custom field, so we need rewrite :method: `save` @@ -302,8 +308,11 @@ class SystemUserForm(forms.ModelForm): 'name': forms.TextInput(attrs={'placeholder': _('Name')}), 'username': forms.TextInput(attrs={'placeholder': _('Username')}), 'cluster': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _(' Select clusters')}), + attrs={ + 'class': 'select2', + 'data-placeholder': _(' Select clusters') + } + ), } help_texts = { 'name': '* required', diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 276b767d3..c83443077 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -18,6 +18,16 @@ __all__ = ['Asset'] logger = logging.getLogger(__name__) +def default_cluster(): + from .cluster import Cluster + name = "Default" + defaults = {"name": name} + cluster, created = Cluster.objects.get_or_create( + defaults=defaults, name=name + ) + return cluster.id + + class Asset(models.Model): # Todo: Move them to settings STATUS_CHOICES = ( @@ -44,7 +54,7 @@ class Asset(models.Model): hostname = models.CharField(max_length=128, unique=True, verbose_name=_('Hostname')) port = models.IntegerField(default=22, verbose_name=_('Port')) groups = models.ManyToManyField(AssetGroup, blank=True, related_name='assets', verbose_name=_('Asset groups')) - cluster = models.ForeignKey(Cluster, blank=True, null=True, related_name='assets', on_delete=models.SET_NULL, verbose_name=_('Cluster')) + cluster = models.ForeignKey(Cluster, related_name='assets', default=default_cluster, on_delete=models.SET_DEFAULT, verbose_name=_('Cluster')) is_active = models.BooleanField(default=True, verbose_name=_('Is active')) type = models.CharField(choices=TYPE_CHOICES, max_length=16, blank=True, null=True, default='Server', verbose_name=_('Asset type'),) env = models.CharField(choices=ENV_CHOICES, max_length=8, blank=True, null=True, default='Prod', verbose_name=_('Asset environment'),) diff --git a/apps/assets/serializers.py b/apps/assets/serializers.py index 401aeac2a..024d88e22 100644 --- a/apps/assets/serializers.py +++ b/apps/assets/serializers.py @@ -22,7 +22,7 @@ class AssetGroupSerializer(BulkSerializerMixin, serializers.ModelSerializer): @staticmethod def get_assets_amount(obj): - return obj.assets.count() + return obj.asset_count class AssetUpdateSystemUserSerializer(serializers.ModelSerializer): @@ -191,9 +191,11 @@ class AssetGrantedSerializer(serializers.ModelSerializer): class Meta(object): model = Asset - fields = ("id", "hostname", "ip", "port", "system_users_granted", - "is_inherited", "is_active", "system_users_join", "os", - "platform", "comment",) + fields = ( + "id", "hostname", "ip", "port", "system_users_granted", + "is_inherited", "is_active", "system_users_join", "os", + "platform", "comment" + ) @staticmethod def get_is_inherited(obj): @@ -214,8 +216,11 @@ class MyAssetGrantedSerializer(AssetGrantedSerializer): class Meta(object): model = Asset - fields = ("id", "hostname", "system_users_granted", "is_inherited", - "is_active", "system_users_join", "comment") + fields = ( + "id", "hostname", "system_users_granted", + "is_inherited", "is_active", "system_users_join", + "os", "platform", "comment", + ) class ClusterSerializer(BulkSerializerMixin, serializers.ModelSerializer): diff --git a/apps/assets/signals_handler.py b/apps/assets/signals_handler.py index b21c0377f..171cfa6d9 100644 --- a/apps/assets/signals_handler.py +++ b/apps/assets/signals_handler.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- # - -from django.db.models.signals import post_save, post_init, m2m_changed, pre_save +from django.db.models.signals import post_save, post_init from django.dispatch import receiver from django.utils.translation import gettext as _ @@ -27,16 +26,17 @@ def test_asset_conn_on_created(asset): def push_cluster_system_users_to_asset(asset): - logger.info("Push cluster system user to asset: {}".format(asset)) - task_name = _("Push cluster system users to asset") - system_users = asset.cluster.systemuser_set.all() - push_system_user_util.delay(system_users, [asset], task_name) + if asset.cluster: + logger.info("Push cluster system user to asset: {}".format(asset)) + task_name = _("Push cluster system users to asset") + system_users = asset.cluster.systemuser_set.all() + push_system_user_util.delay(system_users, [asset], task_name) @receiver(post_save, sender=Asset, dispatch_uid="my_unique_identifier") def on_asset_created(sender, instance=None, created=False, **kwargs): if instance and created: - logger.info("Asset `` create signal received".format(instance)) + logger.info("Asset `{}` create signal received".format(instance)) update_asset_hardware_info_on_created(instance) test_asset_conn_on_created(instance) push_cluster_system_users_to_asset(instance) diff --git a/apps/assets/templates/assets/admin_user_assets.html b/apps/assets/templates/assets/admin_user_assets.html index dcaaef312..7ec123fe2 100644 --- a/apps/assets/templates/assets/admin_user_assets.html +++ b/apps/assets/templates/assets/admin_user_assets.html @@ -34,7 +34,7 @@
- {% trans 'Asset list of ' %} {{ admin_user.name }} {{ total_amount }} {{ unreachable_amount }} + {% trans 'Asset list of ' %} {{ admin_user.name }}
@@ -121,7 +121,7 @@ function initTable() { {data: "type" }, {data: "is_connective" }], op_html: $('#actions').html() }; - jumpserver.initDataTable(options); + jumpserver.initServerSideDataTable(options); } $(document).ready(function () { diff --git a/apps/assets/templates/assets/asset_group_create.html b/apps/assets/templates/assets/asset_group_create.html index 3b951d4d1..36ca40fc1 100644 --- a/apps/assets/templates/assets/asset_group_create.html +++ b/apps/assets/templates/assets/asset_group_create.html @@ -1,119 +1,31 @@ -{% extends 'base.html' %} -{% load i18n %} +{% extends '_base_create_update.html' %} {% load static %} {% load bootstrap3 %} -{% block custom_head_css_js %} - - -{% endblock %} -{% block content %} -
-
-
-
- +{% load i18n %} -
-
-
-
-
- {% csrf_token %} -

资产组信息

- {% bootstrap_field form.name layout="horizontal" %} - {% bootstrap_field form.comment layout="horizontal" %} -{#
#} -{#

用户选择的资产

#} -{#
#} -{# #} -{#
#} -{#
#} -{#

#} -{# {% for asset in assets_on_list %}#} -{# #} -{# {% endfor %}#} -{#

#} -{#
#} -{#
#} -{#
#} -
-
-
- - -
-
-
-
-
-
-
-
-
+{% block form %} +
+ {% csrf_token %} + {% bootstrap_field form.name layout="horizontal" %} + {% bootstrap_field form.assets layout="horizontal" %} + {% bootstrap_field form.comment layout="horizontal" %} + +
+
+
+ +
-
- - - - + {% endblock %} {% block custom_foot_js %} {% endblock %} \ No newline at end of file diff --git a/apps/assets/templates/assets/asset_group_detail.html b/apps/assets/templates/assets/asset_group_detail.html index 9a691036b..dda393e0d 100644 --- a/apps/assets/templates/assets/asset_group_detail.html +++ b/apps/assets/templates/assets/asset_group_detail.html @@ -17,6 +17,11 @@
  • {% trans 'Update' %}
  • +
  • + + {% trans 'Delete' %} + +
  • @@ -179,7 +184,7 @@ function initTable() { {data: "get_type_display" }, {data: "is_connective" }, {data: "id"}], op_html: $('#actions').html() }; - jumpserver.initDataTable(options); + jumpserver.initServerSideDataTable(options); } @@ -212,7 +217,6 @@ $(document).ready(function () { addAssets(assets_id); }) - .on('click', '.btn-leave-group', function () { var $this = $(this); var the_url = "{% url 'api-assets:group-update-assets' pk=asset_group.id %}"; @@ -225,6 +229,13 @@ $(document).ready(function () { assets.remove(delete_asset_id); var data = {"assets": assets}; leaveGroup($this, name, the_url, data); +}).on('click', '.btn-del', function () { + var $this = $(this); + var name = "{{ asset_group.name}}"; + var uid = "{{ asset_group.id }}"; + var the_url = '{% url "api-assets:asset-group-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid); + var redirect_url = "{% url 'assets:asset-group-list' %}"; + objectDelete($this, name, the_url, redirect_url); }) diff --git a/apps/assets/templates/assets/asset_group_list.html b/apps/assets/templates/assets/asset_group_list.html index e27d78867..58fbbc51c 100644 --- a/apps/assets/templates/assets/asset_group_list.html +++ b/apps/assets/templates/assets/asset_group_list.html @@ -34,10 +34,6 @@ $(document).ready(function(){ var detail_btn = '' + cellData + ''; $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); }}, - {targets: 3, createdCell: function (td, cellData) { - var innerHtml = cellData.length > 30 ? cellData.substring(0, 30) + '...': cellData; - $(td).html('' + innerHtml + ''); - }}, {targets: 4, createdCell: function (td, cellData, rowData) { var update_btn = '{% trans "Update" %}'.replace('{{ DEFAULT_PK }}', cellData); var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData); diff --git a/apps/assets/templates/assets/asset_list.html b/apps/assets/templates/assets/asset_list.html index 0a19ba3de..625fc030d 100644 --- a/apps/assets/templates/assets/asset_list.html +++ b/apps/assets/templates/assets/asset_list.html @@ -31,8 +31,6 @@ {% trans 'IP' %} {% trans 'Port' %} {% trans 'Cluster' %} - {% trans 'Type' %} - {% trans 'Env' %} {% trans 'Hardware' %} {% trans 'Active' %} {% trans 'Reachable' %} @@ -71,18 +69,23 @@ function initTable() { columnDefs: [ {targets: 1, createdCell: function (td, cellData, rowData) { {% url 'assets:asset-detail' pk=DEFAULT_PK as the_url %} - console.log('{{ the_url }}'); var detail_btn = '' + cellData + ''; $(td).html(detail_btn.replace('{{ DEFAULT_PK }}', rowData.id)); }}, - {targets: 8, createdCell: function (td, cellData) { + {targets: 4, createdCell: function (td, cellData, rowData) { + $(td).html(rowData.cluster_name) + }}, + {targets: 5, createdCell: function (td, cellData, rowData) { + $(td).html(rowData.hardware_info) + }}, + {targets: 6, createdCell: function (td, cellData) { if (!cellData) { $(td).html('') } else { $(td).html('') } }}, - {targets: 9, createdCell: function (td, cellData) { + {targets: 7, createdCell: function (td, cellData) { if (cellData == 'Unknown'){ $(td).html('') } else if (!cellData) { @@ -91,19 +94,22 @@ function initTable() { $(td).html('') } }}, - {targets: 10, createdCell: function (td, cellData, rowData) { + {targets: 8, createdCell: function (td, cellData, rowData) { var update_btn = '{% trans "Update" %}'.replace("{{ DEFAULT_PK }}", cellData); var del_btn = '{% trans "Delete" %}'.replace('{{ DEFAULT_PK }}', cellData); $(td).html(update_btn + del_btn) }} ], ajax_url: '{% url "api-assets:asset-list" %}', - columns: [{data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "cluster_name"}, - {data: "get_type_display" }, {data: "get_env_display"}, {data: "hardware_info"}, - {data: "is_active" }, {data: "is_connective"}, {data: "id" }], + columns: [ + {data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "port" }, + {data: "cluster"}, + {data: "cpu_cores"}, {data: "is_active", orderable: false }, + {data: "is_connective", orderable: false}, {data: "id", orderable: false } + ], op_html: $('#actions').html() }; - return jumpserver.initDataTable(options); + return jumpserver.initServerSideDataTable(options); } $(document).ready(function(){ @@ -178,9 +184,15 @@ $(document).ready(function(){ var obj = {"pk": object_id, "is_active": false}; data.push(obj); }); - APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(data)}); - $data_table.ajax.reload(); - jumpserver.checked = false; + function success() { + location.reload() + } + APIUpdateAttr({ + url: the_url, + method: 'PATCH', + body: JSON.stringify(data), + success: success + }); } function doActive() { var data = []; @@ -188,9 +200,15 @@ $(document).ready(function(){ var obj = {"pk": object_id, "is_active": true}; data.push(obj); }); - APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(data)}); - $data_table.ajax.reload(); - jumpserver.checked = false; + function success() { + location.reload(); + } + APIUpdateAttr({ + url: the_url, + method: 'PATCH', + body: JSON.stringify(data), + success: success + }); } function doDelete() { swal({ diff --git a/apps/assets/templates/assets/cluster_assets.html b/apps/assets/templates/assets/cluster_assets.html index b194a5034..b2e33e578 100644 --- a/apps/assets/templates/assets/cluster_assets.html +++ b/apps/assets/templates/assets/cluster_assets.html @@ -28,7 +28,7 @@
    - {% trans 'Cluster assets' %} {{ cluster.name }} {{ cluster.assets.all.count }} + {% trans 'Cluster assets' %} {{ cluster.name }}
    @@ -176,7 +176,7 @@ function initTable() { {data: "get_type_display" }, {data: "is_connective" }, {data: "id"}], op_html: $('#actions').html() }; - jumpserver.initDataTable(options); + jumpserver.initServerSideDataTable(options); } diff --git a/apps/assets/templates/assets/cluster_create_update.html b/apps/assets/templates/assets/cluster_create_update.html index 38fc6e3de..66c36bece 100644 --- a/apps/assets/templates/assets/cluster_create_update.html +++ b/apps/assets/templates/assets/cluster_create_update.html @@ -38,7 +38,7 @@ {% bootstrap_field form.contact layout="horizontal" %} {% bootstrap_field form.phone layout="horizontal" %} -

    {% trans 'Settings' %}

    +

    {% trans 'Setting' %}

    {% bootstrap_field form.admin_user layout="horizontal" %} {% bootstrap_field form.system_users layout="horizontal" %} diff --git a/apps/assets/templates/assets/cluster_list.html b/apps/assets/templates/assets/cluster_list.html index b6c0abfb2..e4743b346 100644 --- a/apps/assets/templates/assets/cluster_list.html +++ b/apps/assets/templates/assets/cluster_list.html @@ -8,7 +8,7 @@ {% block table_search %}{% endblock %} {% block table_container %}
    diff --git a/apps/assets/templates/assets/system_user_asset.html b/apps/assets/templates/assets/system_user_asset.html index afff889f6..91c2ffbe0 100644 --- a/apps/assets/templates/assets/system_user_asset.html +++ b/apps/assets/templates/assets/system_user_asset.html @@ -121,7 +121,7 @@ function initAssetsTable() { columns: [{data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "hostname" }], op_html: $('#actions').html() }; - jumpserver.initDataTable(options); + jumpserver.initServerSideDataTable(options); } $(document).ready(function () { diff --git a/apps/assets/templates/assets/system_user_detail.html b/apps/assets/templates/assets/system_user_detail.html index db9cf9670..0bf591bb2 100644 --- a/apps/assets/templates/assets/system_user_detail.html +++ b/apps/assets/templates/assets/system_user_detail.html @@ -25,6 +25,11 @@
  • {% trans 'Update' %}
  • +
  • + + {% trans 'Delete' %} + +
  • @@ -259,6 +264,13 @@ $(document).ready(function () { return $(this).data('gid'); }).get(); updateSystemUserCluster(clusters); +}).on('click', '.btn-del', function () { + var $this = $(this); + var name = "{{ system_user.name}}"; + var uid = "{{ system_user.id }}"; + var the_url = '{% url "api-assets:system-user-detail" pk=DEFAULT_PK %}'.replace('{{ DEFAULT_PK }}', uid); + var redirect_url = "{% url 'assets:system-user-list' %}"; + objectDelete($this, name, the_url, redirect_url); }) {% endblock %} diff --git a/apps/assets/templates/assets/user_asset_list.html b/apps/assets/templates/assets/user_asset_list.html index 20c7d4d50..4a91d4f65 100644 --- a/apps/assets/templates/assets/user_asset_list.html +++ b/apps/assets/templates/assets/user_asset_list.html @@ -24,7 +24,6 @@
    - @@ -61,16 +60,16 @@ function initTable() { $(td).html('') } }}, - {targets: 9, createdCell: function (td, cellData, rowData) { - var conn_btn = '{% trans "Connect" %}'.replace("{{ DEFAULT_PK }}", cellData); - $(td).html(conn_btn) - }} +{# {targets: 9, createdCell: function (td, cellData, rowData) {#} +{# var conn_btn = '{% trans "Connect" %}'.replace("{{ DEFAULT_PK }}", cellData);#} +{# $(td).html(conn_btn)#} +{# }}#} ], ajax_url: '{% url "api-assets:user-asset-list" %}', columns: [ {data: "id"}, {data: "hostname" }, {data: "ip" }, {data: "port" }, {data: "get_type_display" }, {data: "get_env_display"}, {data: "hardware_info"}, - {data: "is_active" }, {data: "is_connective"}, {data: "id"} + {data: "is_active" }, {data: "is_connective"} ], op_html: $('#actions').html() }; diff --git a/apps/assets/views/admin_user.py b/apps/assets/views/admin_user.py index 713f1fc53..ce7b8bfb4 100644 --- a/apps/assets/views/admin_user.py +++ b/apps/assets/views/admin_user.py @@ -2,20 +2,22 @@ from __future__ import absolute_import, unicode_literals from django.utils.translation import ugettext as _ from django.conf import settings -from django.views.generic import TemplateView, ListView, View -from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateView from django.urls import reverse_lazy +from django.views.generic import TemplateView, ListView +from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.contrib.messages.views import SuccessMessageMixin from django.views.generic.detail import DetailView, SingleObjectMixin +from common.const import create_success_msg, update_success_msg from .. import forms -from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser +from ..models import AdminUser, Cluster from ..hands import AdminUserRequiredMixin -__all__ = ['AdminUserCreateView', 'AdminUserDetailView', - 'AdminUserDeleteView', 'AdminUserListView', - 'AdminUserUpdateView', 'AdminUserAssetsView', - ] +__all__ = [ + 'AdminUserCreateView', 'AdminUserDetailView', + 'AdminUserDeleteView', 'AdminUserListView', + 'AdminUserUpdateView', 'AdminUserAssetsView', +] class AdminUserListView(AdminUserRequiredMixin, TemplateView): @@ -38,6 +40,7 @@ class AdminUserCreateView(AdminUserRequiredMixin, form_class = forms.AdminUserForm template_name = 'assets/admin_user_create_update.html' success_url = reverse_lazy('assets:admin-user-list') + success_message = create_success_msg def get_context_data(self, **kwargs): context = { @@ -47,20 +50,13 @@ class AdminUserCreateView(AdminUserRequiredMixin, kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - success_message = _( - 'Create admin user {name} successfully.'.format( - url=reverse_lazy('assets:admin-user-detail', - kwargs={'pk': self.object.pk}), - name=self.object.name, - )) - return success_message - -class AdminUserUpdateView(AdminUserRequiredMixin, UpdateView): +class AdminUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = AdminUser form_class = forms.AdminUserForm template_name = 'assets/admin_user_create_update.html' + success_url = reverse_lazy('assets:admin-user-list') + success_message = update_success_msg def get_context_data(self, **kwargs): context = { @@ -70,11 +66,6 @@ class AdminUserUpdateView(AdminUserRequiredMixin, UpdateView): kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_url(self): - success_url = reverse_lazy('assets:admin-user-detail', - kwargs={'pk': self.object.pk}) - return success_url - class AdminUserDetailView(AdminUserRequiredMixin, DetailView): model = AdminUser @@ -94,7 +85,7 @@ class AdminUserDetailView(AdminUserRequiredMixin, DetailView): class AdminUserAssetsView(AdminUserRequiredMixin, SingleObjectMixin, ListView): - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE template_name = 'assets/admin_user_assets.html' context_object_name = 'admin_user' object = None diff --git a/apps/assets/views/asset.py b/apps/assets/views/asset.py index 1c0e2ce1a..b2f57e323 100644 --- a/apps/assets/views/asset.py +++ b/apps/assets/views/asset.py @@ -21,10 +21,11 @@ from django.core.cache import cache from django.utils import timezone from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import redirect - +from django.contrib.messages.views import SuccessMessageMixin from common.mixins import JSONResponseMixin from common.utils import get_object_or_none, get_logger, is_uuid +from common.const import create_success_msg, update_success_msg from .. import forms from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser from ..hands import AdminUserRequiredMixin @@ -46,7 +47,6 @@ class AssetListView(AdminUserRequiredMixin, TemplateView): context = { 'app': _('Assets'), 'action': _('Asset list'), - # 'groups': AssetGroup.objects.all(), 'system_users': SystemUser.objects.all(), } kwargs.update(context) @@ -66,7 +66,7 @@ class UserAssetListView(LoginRequiredMixin, TemplateView): return super().get_context_data(**kwargs) -class AssetCreateView(AdminUserRequiredMixin, CreateView): +class AssetCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): model = Asset form_class = forms.AssetCreateForm template_name = 'assets/asset_create.html' @@ -87,9 +87,12 @@ class AssetCreateView(AdminUserRequiredMixin, CreateView): kwargs.update(context) return super().get_context_data(**kwargs) + def get_success_message(self, cleaned_data): + return create_success_msg % ({"name": cleaned_data["hostname"]}) + class AssetModalListView(AdminUserRequiredMixin, ListView): - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE model = Asset context_object_name = 'asset_modal_list' template_name = 'assets/asset_modal_list.html' @@ -147,7 +150,7 @@ class AssetBulkUpdateView(AdminUserRequiredMixin, ListView): return super().get_context_data(**kwargs) -class AssetUpdateView(AdminUserRequiredMixin, UpdateView): +class AssetUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = Asset form_class = forms.AssetUpdateForm template_name = 'assets/asset_update.html' @@ -161,6 +164,9 @@ class AssetUpdateView(AdminUserRequiredMixin, UpdateView): kwargs.update(context) return super().get_context_data(**kwargs) + def get_success_message(self, cleaned_data): + return update_success_msg % ({"name": cleaned_data["hostname"]}) + class AssetDeleteView(AdminUserRequiredMixin, DeleteView): model = Asset diff --git a/apps/assets/views/cluster.py b/apps/assets/views/cluster.py index f8540bed1..835229fc1 100644 --- a/apps/assets/views/cluster.py +++ b/apps/assets/views/cluster.py @@ -1,17 +1,21 @@ # coding:utf-8 -from __future__ import absolute_import, unicode_literals from django.utils.translation import ugettext 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 from django.views.generic.detail import DetailView, SingleObjectMixin +from django.contrib.messages.views import SuccessMessageMixin + +from common.const import create_success_msg, update_success_msg from .. import forms from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser from ..hands import AdminUserRequiredMixin -__all__ = ['ClusterListView', 'ClusterCreateView', 'ClusterUpdateView', - 'ClusterDetailView', 'ClusterDeleteView', 'ClusterAssetsView'] +__all__ = [ + 'ClusterListView', 'ClusterCreateView', 'ClusterUpdateView', + 'ClusterDetailView', 'ClusterDeleteView', 'ClusterAssetsView', +] class ClusterListView(AdminUserRequiredMixin, TemplateView): @@ -21,39 +25,40 @@ class ClusterListView(AdminUserRequiredMixin, TemplateView): context = { 'app': _('Assets'), 'action': _('Cluster list'), - # 'keyword': self.request.GET.get('keyword', '') } kwargs.update(context) return super().get_context_data(**kwargs) -class ClusterCreateView(AdminUserRequiredMixin, CreateView): +class ClusterCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): model = Cluster form_class = forms.ClusterForm template_name = 'assets/cluster_create_update.html' success_url = reverse_lazy('assets:cluster-list') + success_message = create_success_msg def get_context_data(self, **kwargs): context = { 'app': _('assets'), - 'action': _('Create Cluster'), + 'action': _('Create cluster'), } kwargs.update(context) return super().get_context_data(**kwargs) def form_valid(self, form): - cluster = form.save(commit=False) - cluster.created_by = self.request.user.username or 'System' + cluster = form.save() + cluster.created_by = self.request.user.username cluster.save() return super().form_valid(form) -class ClusterUpdateView(AdminUserRequiredMixin, UpdateView): +class ClusterUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = Cluster form_class = forms.ClusterForm template_name = 'assets/cluster_create_update.html' context_object_name = 'cluster' success_url = reverse_lazy('assets:cluster-list') + success_message = update_success_msg def form_valid(self, form): cluster = form.save(commit=False) diff --git a/apps/assets/views/group.py b/apps/assets/views/group.py index 5e87d9dc7..0ae65eaa0 100644 --- a/apps/assets/views/group.py +++ b/apps/assets/views/group.py @@ -7,42 +7,41 @@ from django.views.generic.edit import CreateView, DeleteView, FormView, UpdateVi from django.urls import reverse_lazy from django.views.generic.detail import DetailView, SingleObjectMixin from django.shortcuts import get_object_or_404, reverse, redirect +from django.contrib.messages.views import SuccessMessageMixin +from common.const import create_success_msg, update_success_msg from .. import forms from ..models import Asset, AssetGroup, AdminUser, Cluster, SystemUser from ..hands import AdminUserRequiredMixin -__all__ = ['AssetGroupCreateView', 'AssetGroupDetailView', - 'AssetGroupUpdateView', 'AssetGroupListView', - 'AssetGroupDeleteView', - ] +__all__ = [ + 'AssetGroupCreateView', 'AssetGroupDetailView', + 'AssetGroupUpdateView', 'AssetGroupListView', + 'AssetGroupDeleteView', +] -class AssetGroupCreateView(AdminUserRequiredMixin, CreateView): +class AssetGroupCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): model = AssetGroup form_class = forms.AssetGroupForm template_name = 'assets/asset_group_create.html' success_url = reverse_lazy('assets:asset-group-list') + success_message = create_success_msg def get_context_data(self, **kwargs): context = { 'app': _('Assets'), 'action': _('Create asset group'), - 'assets_count': 0, } kwargs.update(context) - return super(AssetGroupCreateView, self).get_context_data(**kwargs) + return super().get_context_data(**kwargs) def form_valid(self, form): - asset_group = form.save() - assets_id_list = self.request.POST.getlist('assets', []) - assets = [get_object_or_404(Asset, id=int(asset_id)) - for asset_id in assets_id_list] - asset_group.created_by = self.request.user.username or 'Admin' - asset_group.assets.add(*tuple(assets)) - asset_group.save() - return super(AssetGroupCreateView, self).form_valid(form) + group = form.save() + group.created_by = self.request.user.username + group.save() + return super().form_valid(form) class AssetGroupListView(AdminUserRequiredMixin, TemplateView): @@ -54,7 +53,6 @@ class AssetGroupListView(AdminUserRequiredMixin, TemplateView): 'action': _('Asset group list'), 'assets': Asset.objects.all(), 'system_users': SystemUser.objects.all(), - 'keyword': self.request.GET.get('keyword', '') } kwargs.update(context) return super(AssetGroupListView, self).get_context_data(**kwargs) @@ -77,27 +75,20 @@ class AssetGroupDetailView(AdminUserRequiredMixin, DetailView): return super().get_context_data(**kwargs) -class AssetGroupUpdateView(AdminUserRequiredMixin, UpdateView): +class AssetGroupUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = AssetGroup form_class = forms.AssetGroupForm template_name = 'assets/asset_group_create.html' success_url = reverse_lazy('assets:asset-group-list') - - def get(self, request, *args, **kwargs): - self.object = self.get_object(queryset=AssetGroup.objects.all()) - return super(AssetGroupUpdateView, self).get(request, *args, **kwargs) + success_message = update_success_msg def get_context_data(self, **kwargs): - assets_all = self.object.assets.all() context = { 'app': _('Assets'), 'action': _('Create asset group'), - 'assets_on_list': assets_all, - 'assets_count': len(assets_all), - 'group_id': self.object.id, } kwargs.update(context) - return super(AssetGroupUpdateView, self).get_context_data(**kwargs) + return super().get_context_data(**kwargs) class AssetGroupDeleteView(AdminUserRequiredMixin, DeleteView): diff --git a/apps/assets/views/system_user.py b/apps/assets/views/system_user.py index b79827598..f7e7eaa35 100644 --- a/apps/assets/views/system_user.py +++ b/apps/assets/views/system_user.py @@ -1,24 +1,23 @@ # ~*~ coding: utf-8 ~*~ -from django.contrib import messages -from django.shortcuts import redirect, reverse from django.utils.translation import ugettext as _ -from django.db import transaction -from django.views.generic import TemplateView, ListView, FormView +from django.views.generic import TemplateView from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.urls import reverse_lazy from django.contrib.messages.views import SuccessMessageMixin -from django.views.generic.detail import DetailView, SingleObjectMixin +from django.views.generic.detail import DetailView -from ..forms import SystemUserForm, SystemUserUpdateForm, SystemUserAuthForm +from common.const import create_success_msg, update_success_msg +from ..forms import SystemUserForm, SystemUserUpdateForm from ..models import SystemUser, Cluster from ..hands import AdminUserRequiredMixin -__all__ = ['SystemUserCreateView', 'SystemUserUpdateView', - 'SystemUserDetailView', 'SystemUserDeleteView', - 'SystemUserAssetView', 'SystemUserListView', - ] +__all__ = [ + 'SystemUserCreateView', 'SystemUserUpdateView', + 'SystemUserDetailView', 'SystemUserDeleteView', + 'SystemUserAssetView', 'SystemUserListView', +] class SystemUserListView(AdminUserRequiredMixin, TemplateView): @@ -38,10 +37,7 @@ class SystemUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateVi form_class = SystemUserForm template_name = 'assets/system_user_create.html' success_url = reverse_lazy('assets:system-user-list') - - @transaction.atomic - def post(self, request, *args, **kwargs): - return super(SystemUserCreateView, self).post(request, *args, **kwargs) + success_message = create_success_msg def get_context_data(self, **kwargs): context = { @@ -51,20 +47,13 @@ class SystemUserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateVi kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - url = reverse('assets:system-user-detail', kwargs={'pk': self.object.pk}) - success_message = _( - 'Create system user {name} ' - 'successfully.'.format(url=url, name=self.object.name) - ) - return success_message - - -class SystemUserUpdateView(AdminUserRequiredMixin, UpdateView): +class SystemUserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = SystemUser form_class = SystemUserUpdateForm template_name = 'assets/system_user_update.html' + success_url = reverse_lazy('assets:system-user-list') + success_message = update_success_msg def get_context_data(self, **kwargs): context = { @@ -74,11 +63,6 @@ class SystemUserUpdateView(AdminUserRequiredMixin, UpdateView): kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_url(self): - success_url = reverse_lazy('assets:system-user-detail', - kwargs={'pk': self.object.pk}) - return success_url - class SystemUserDetailView(AdminUserRequiredMixin, DetailView): template_name = 'assets/system_user_detail.html' diff --git a/apps/common/api.py b/apps/common/api.py new file mode 100644 index 000000000..e8680e85e --- /dev/null +++ b/apps/common/api.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# +import json + +from rest_framework.views import APIView +from rest_framework.views import Response +from ldap3 import Server, Connection +from django.core.mail import get_connection, send_mail +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings + +from .permissions import IsSuperUser +from .serializers import MailTestSerializer, LDAPTestSerializer + + +class MailTestingAPI(APIView): + permission_classes = (IsSuperUser,) + serializer_class = MailTestSerializer + success_message = _("Test mail sent to {}, please check") + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + email_host_user = serializer.validated_data["EMAIL_HOST_USER"] + kwargs = { + "host": serializer.validated_data["EMAIL_HOST"], + "port": serializer.validated_data["EMAIL_PORT"], + "username": serializer.validated_data["EMAIL_HOST_USER"], + "password": serializer.validated_data["EMAIL_HOST_PASSWORD"], + "use_ssl": serializer.validated_data["EMAIL_USE_SSL"], + "use_tls": serializer.validated_data["EMAIL_USE_TLS"] + } + connection = get_connection(timeout=5, **kwargs) + try: + connection.open() + except Exception as e: + return Response({"error": str(e)}, status=401) + + try: + send_mail("Test", "Test smtp setting", email_host_user, + [email_host_user], connection=connection) + except Exception as e: + return Response({"error": str(e)}, status=401) + + return Response({"msg": self.success_message.format(email_host_user)}) + else: + return Response({"error": str(serializer.errors)}, status=401) + + +class LDAPTestingAPI(APIView): + permission_classes = (IsSuperUser,) + serializer_class = LDAPTestSerializer + success_message = _("Test ldap success") + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + host = serializer.validated_data["AUTH_LDAP_SERVER_URI"] + bind_dn = serializer.validated_data["AUTH_LDAP_BIND_DN"] + password = serializer.validated_data["AUTH_LDAP_BIND_PASSWORD"] + use_ssl = serializer.validated_data.get("AUTH_LDAP_START_TLS", False) + search_ou = serializer.validated_data["AUTH_LDAP_SEARCH_OU"] + search_filter = serializer.validated_data["AUTH_LDAP_SEARCH_FILTER"] + attr_map = serializer.validated_data["AUTH_LDAP_USER_ATTR_MAP"] + + print(serializer.validated_data) + + try: + attr_map = json.loads(attr_map) + except json.JSONDecodeError: + return Response({"error": "AUTH_LDAP_USER_ATTR_MAP not valid"}, status=401) + + server = Server(host, use_ssl=use_ssl) + conn = Connection(server, bind_dn, password) + try: + conn.bind() + except Exception as e: + return Response({"error": str(e)}, status=401) + + print(search_ou) + print(search_filter % ({"user": "*"})) + print(attr_map.values()) + ok = conn.search(search_ou, search_filter % ({"user": "*"}), + attributes=list(attr_map.values())) + if not ok: + return Response({"error": "Search no entry matched"}, status=401) + + users = [] + for entry in conn.entries: + user = {} + for attr, mapping in attr_map.items(): + if hasattr(entry, mapping): + user[attr] = getattr(entry, mapping) + users.append(user) + if len(users) > 0: + return Response({"msg": "Match {} s users".format(len(users))}) + else: + return Response({"error": "Have user but attr mapping error"}, status=401) + else: + return Response({"error": str(serializer.errors)}, status=401) + + +class DjangoSettingsAPI(APIView): + def get(self, request): + configs = {} + for i in dir(settings): + if i.isupper(): + configs[i] = str(getattr(settings, i)) + return Response(configs) + diff --git a/apps/common/apps.py b/apps/common/apps.py index 6664a6438..bc6db2151 100644 --- a/apps/common/apps.py +++ b/apps/common/apps.py @@ -5,3 +5,9 @@ from django.apps import AppConfig class CommonConfig(AppConfig): name = 'common' + + def ready(self): + from . import signals_handler + from .signals import django_ready + django_ready.send(self.__class__) + return super().ready() diff --git a/apps/common/const.py b/apps/common/const.py new file mode 100644 index 000000000..a28b0f1db --- /dev/null +++ b/apps/common/const.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# + +from django.utils.translation import ugettext as _ + +create_success_msg = _("%(name)s was created successfully") +update_success_msg = _("%(name)s was updated successfully") \ No newline at end of file diff --git a/apps/common/fields.py b/apps/common/fields.py new file mode 100644 index 000000000..36a8bdf9a --- /dev/null +++ b/apps/common/fields.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# +import json + +from django import forms +from django.utils import six +from django.core.exceptions import ValidationError + + +class DictField(forms.Field): + widget = forms.Textarea + + def to_python(self, value): + """Returns a Python boolean object.""" + # Explicitly check for the string 'False', which is what a hidden field + # will submit for False. Also check for '0', since this is what + # RadioSelect will provide. Because bool("True") == bool('1') == True, + # we don't need to handle that explicitly. + if isinstance(value, six.string_types): + try: + print(value) + value = json.loads(value) + return value + except json.JSONDecodeError: + pass + value = {} + return value + + def validate(self, value): + print(value) + if not value and self.required: + raise ValidationError(self.error_messages['required'], code='required') + + def has_changed(self, initial, data): + # Sometimes data or initial may be a string equivalent of a boolean + # so we should run it through to_python first to get a boolean value + return self.to_python(initial) != self.to_python(data) diff --git a/apps/common/forms.py b/apps/common/forms.py new file mode 100644 index 000000000..6b83c54cc --- /dev/null +++ b/apps/common/forms.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +import json + +from django import forms +from django.utils.translation import ugettext_lazy as _ +from django.db import transaction + +from .models import Setting +from .fields import DictField + + +def to_model_value(value): + try: + return json.dumps(value) + except json.JSONDecodeError: + return None + + +def to_form_value(value): + try: + data = json.loads(value) + if isinstance(data, dict): + data = value + return data + except json.JSONDecodeError: + return '' + + +class BaseForm(forms.Form): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + settings = Setting.objects.all() + for name, field in self.fields.items(): + db_value = getattr(settings, name).value + if db_value: + field.initial = to_form_value(db_value) + + def save(self): + if not self.is_bound: + raise ValueError("Form is not bound") + + settings = Setting.objects.all() + if self.is_valid(): + with transaction.atomic(): + for name, value in self.cleaned_data.items(): + field = self.fields[name] + if isinstance(field.widget, forms.PasswordInput) and not value: + continue + if value == to_form_value(getattr(settings, name).value): + continue + + defaults = { + 'name': name, + 'value': to_model_value(value) + } + Setting.objects.update_or_create(defaults=defaults, name=name) + else: + raise ValueError(self.errors) + + +class BasicSettingForm(BaseForm): + SITE_URL = forms.URLField( + label=_("Current SITE URL"), + help_text="http://jumpserver.abc.com:8080" + ) + USER_GUIDE_URL = forms.URLField( + label=_("User Guide URL"), + help_text=_("User first login update profile done redirect to it") + ) + EMAIL_SUBJECT_PREFIX = forms.CharField( + max_length=1024, label=_("Email Subject Prefix"), + initial="[Jumpserver] " + ) + + +class EmailSettingForm(BaseForm): + EMAIL_HOST = forms.CharField( + max_length=1024, label=_("SMTP host"), initial='smtp.jumpserver.org' + ) + EMAIL_PORT = forms.CharField(max_length=5, label=_("SMTP port"), initial=25) + EMAIL_HOST_USER = forms.CharField( + max_length=128, label=_("SMTP user"), initial='noreply@jumpserver.org' + ) + EMAIL_HOST_PASSWORD = forms.CharField( + max_length=1024, label=_("SMTP password"), widget=forms.PasswordInput, + required=False, help_text=_("Some provider use token except password") + ) + EMAIL_USE_SSL = forms.BooleanField( + label=_("Use SSL"), initial=False, required=False, + help_text=_("If SMTP port is 465, may be select") + ) + EMAIL_USE_TLS = forms.BooleanField( + label=_("Use TLS"), initial=False, required=False, + help_text=_("If SMTP port is 587, may be select") + ) + + +class LDAPSettingForm(BaseForm): + AUTH_LDAP_SERVER_URI = forms.CharField( + label=_("LDAP server"), initial='ldap://localhost:389' + ) + AUTH_LDAP_BIND_DN = forms.CharField( + label=_("Bind DN"), initial='cn=admin,dc=jumpserver,dc=org' + ) + AUTH_LDAP_BIND_PASSWORD = forms.CharField( + label=_("Password"), initial='', + widget=forms.PasswordInput, required=False + ) + AUTH_LDAP_SEARCH_OU = forms.CharField( + label=_("User OU"), initial='ou=tech,dc=jumpserver,dc=org' + ) + AUTH_LDAP_SEARCH_FILTER = forms.CharField( + label=_("User search filter"), initial='(cn=%(user)s)' + ) + AUTH_LDAP_USER_ATTR_MAP = DictField( + label=_("User attr map"), + initial=json.dumps({ + "username": "cn", + "name": "sn", + "email": "mail" + }) + ) + # AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU + # AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER + AUTH_LDAP_START_TLS = forms.BooleanField( + label=_("Use SSL"), initial=False, required=False + ) + AUTH_LDAP = forms.BooleanField( + label=_("Enable LDAP Auth"), initial=False, required=False + ) diff --git a/apps/common/migrations/__init__.py b/apps/common/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/apps/common/mixins.py b/apps/common/mixins.py index 1371cf65b..d1afb081b 100644 --- a/apps/common/mixins.py +++ b/apps/common/mixins.py @@ -1,10 +1,10 @@ # coding: utf-8 -import inspect from django.db import models from django.http import JsonResponse from django.utils import timezone from django.utils.translation import ugettext_lazy as _ +from django.contrib.auth.mixins import UserPassesTestMixin class NoDeleteQuerySet(models.query.QuerySet): @@ -47,8 +47,9 @@ class JSONResponseMixin(object): return JsonResponse(context) -class IDInFilterMixin(object): +class CustomFilterMixin(object): def filter_queryset(self, queryset): + queryset = super(CustomFilterMixin, self).filter_queryset(queryset) id_list = self.request.query_params.get('id__in') if id_list: import json @@ -113,4 +114,14 @@ class DatetimeSearchMixin: ) else: self.date_to = timezone.now() - return super().get(request, *args, **kwargs) \ No newline at end of file + return super().get(request, *args, **kwargs) + + +class AdminUserRequiredMixin(UserPassesTestMixin): + def test_func(self): + if not self.request.user.is_authenticated: + return False + elif not self.request.user.is_superuser: + self.raise_exception = True + return False + return True diff --git a/apps/common/models.py b/apps/common/models.py index beeb30826..091b8b83a 100644 --- a/apps/common/models.py +++ b/apps/common/models.py @@ -1,2 +1,68 @@ -from django.db import models +import json +import ldap +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from django_auth_ldap.config import LDAPSearch + + +class SettingQuerySet(models.QuerySet): + def __getattr__(self, item): + instances = self.filter(name=item) + if len(instances) == 1: + return instances[0] + else: + return Setting() + + +class SettingManager(models.Manager): + def get_queryset(self): + return SettingQuerySet(self.model, using=self._db) + + +class Setting(models.Model): + name = models.CharField(max_length=128, unique=True, verbose_name=_("Name")) + value = models.TextField(verbose_name=_("Value")) + enabled = models.BooleanField(verbose_name=_("Enabled"), default=True) + comment = models.TextField(verbose_name=_("Comment")) + + objects = SettingManager() + + def __str__(self): + return self.name + + @property + def value_(self): + try: + return json.loads(self.value) + except json.JSONDecodeError: + return None + + @classmethod + def refresh_all_settings(cls): + settings_list = cls.objects.all() + for setting in settings_list: + setting.refresh_setting() + + def refresh_setting(self): + try: + value = json.loads(self.value) + except json.JSONDecodeError: + return + setattr(settings, self.name, value) + + if self.name == "AUTH_LDAP": + if self.value_ and settings.AUTH_LDAP_BACKEND not in settings.AUTHENTICATION_BACKENDS: + settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) + elif not self.value_ and settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: + settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) + + if self.name == "AUTH_LDAP_SEARCH_FILTER": + settings.AUTH_LDAP_USER_SEARCH = LDAPSearch( + settings.AUTH_LDAP_SEARCH_OU, ldap.SCOPE_SUBTREE, + settings.AUTH_LDAP_SEARCH_FILTER, + ) + + class Meta: + db_table = "settings" diff --git a/apps/common/permissions.py b/apps/common/permissions.py new file mode 100644 index 000000000..6a1cb8230 --- /dev/null +++ b/apps/common/permissions.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# + +from rest_framework import permissions + + +class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission): + """Allows access to valid user, is active and not expired""" + + def has_permission(self, request, view): + return super(IsValidUser, self).has_permission(request, view) \ + and request.user.is_valid + + +class IsAppUser(IsValidUser): + """Allows access only to app user """ + + def has_permission(self, request, view): + return super(IsAppUser, self).has_permission(request, view) \ + and request.user.is_app + + +class IsSuperUser(IsValidUser): + """Allows access only to superuser""" + + def has_permission(self, request, view): + return super(IsSuperUser, self).has_permission(request, view) \ + and request.user.is_superuser + + +class IsSuperUserOrAppUser(IsValidUser): + """Allows access between superuser and app user""" + + def has_permission(self, request, view): + return super(IsSuperUserOrAppUser, self).has_permission(request, view) \ + and (request.user.is_superuser or request.user.is_app) + + +class IsSuperUserOrAppUserOrUserReadonly(IsSuperUserOrAppUser): + def has_permission(self, request, view): + if IsValidUser.has_permission(self, request, view) \ + and request.method in permissions.SAFE_METHODS: + return True + else: + return IsSuperUserOrAppUser.has_permission(self, request, view) + + +class IsCurrentUserOrReadOnly(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + return obj == request.user diff --git a/apps/common/serializers.py b/apps/common/serializers.py new file mode 100644 index 000000000..9d389776d --- /dev/null +++ b/apps/common/serializers.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + + +class MailTestSerializer(serializers.Serializer): + EMAIL_HOST = serializers.CharField(max_length=1024, required=True) + EMAIL_PORT = serializers.IntegerField(default=25) + EMAIL_HOST_USER = serializers.CharField(max_length=1024) + EMAIL_HOST_PASSWORD = serializers.CharField() + EMAIL_USE_SSL = serializers.BooleanField(default=False) + EMAIL_USE_TLS = serializers.BooleanField(default=False) + + +class LDAPTestSerializer(serializers.Serializer): + AUTH_LDAP_SERVER_URI = serializers.CharField(max_length=1024) + AUTH_LDAP_BIND_DN = serializers.CharField(max_length=1024) + AUTH_LDAP_BIND_PASSWORD = serializers.CharField() + AUTH_LDAP_SEARCH_OU = serializers.CharField() + AUTH_LDAP_SEARCH_FILTER = serializers.CharField() + AUTH_LDAP_USER_ATTR_MAP = serializers.CharField() + AUTH_LDAP_START_TLS = serializers.BooleanField(required=False) + diff --git a/apps/common/signals.py b/apps/common/signals.py new file mode 100644 index 000000000..6edf140e2 --- /dev/null +++ b/apps/common/signals.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# + +from django.dispatch import Signal + +django_ready = Signal() +ldap_auth_enable = Signal(providing_args=["enabled"]) diff --git a/apps/common/signals_handler.py b/apps/common/signals_handler.py new file mode 100644 index 000000000..df2ea5a5e --- /dev/null +++ b/apps/common/signals_handler.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# +from django.dispatch import receiver +from django.db.models.signals import post_save +from django.conf import settings +from django.db.utils import ProgrammingError, OperationalError + +from .models import Setting +from .utils import get_logger +from .signals import django_ready, ldap_auth_enable + +logger = get_logger(__file__) + + +@receiver(post_save, sender=Setting, dispatch_uid="my_unique_identifier") +def refresh_settings_on_changed(sender, instance=None, **kwargs): + logger.debug("Receive setting item change") + logger.debug(" - refresh setting: {}".format(instance.name)) + if instance: + instance.refresh_setting() + + +@receiver(django_ready, dispatch_uid="my_unique_identifier") +def refresh_all_settings_on_django_ready(sender, **kwargs): + logger.debug("Receive django ready signal") + logger.debug(" - fresh all settings") + try: + Setting.refresh_all_settings() + except (ProgrammingError, OperationalError): + pass + + +@receiver(ldap_auth_enable, dispatch_uid="my_unique_identifier") +def ldap_auth_on_changed(sender, enabled=True, **kwargs): + if enabled: + logger.debug("Enable LDAP auth") + if settings.AUTH_LDAP_BACKEND not in settings.AUTH_LDAP_BACKEND: + settings.AUTHENTICATION_BACKENDS.insert(0, settings.AUTH_LDAP_BACKEND) + + else: + logger.debug("Disable LDAP auth") + if settings.AUTH_LDAP_BACKEND in settings.AUTHENTICATION_BACKENDS: + settings.AUTHENTICATION_BACKENDS.remove(settings.AUTH_LDAP_BACKEND) + diff --git a/apps/common/templates/common/basic_setting.html b/apps/common/templates/common/basic_setting.html new file mode 100644 index 000000000..fb5039795 --- /dev/null +++ b/apps/common/templates/common/basic_setting.html @@ -0,0 +1,100 @@ +{% extends 'base.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} +{% load common_tags %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} + {% for field in form %} + {% if not field.field|is_bool_field %} + {% bootstrap_field field layout="horizontal" %} + {% else %} +
    + +
    +
    + {{ field }} +
    +
    + {{ field.help_text }} +
    +
    +
    + {% endif %} + {% endfor %} +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/common/templates/common/email_setting.html b/apps/common/templates/common/email_setting.html new file mode 100644 index 000000000..7561849bd --- /dev/null +++ b/apps/common/templates/common/email_setting.html @@ -0,0 +1,101 @@ +{% extends 'base.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} +{% load common_tags %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} + {% for field in form %} + {% if not field.field|is_bool_field %} + {% bootstrap_field field layout="horizontal" %} + {% else %} +
    + +
    +
    + {{ field }} +
    +
    + {{ field.help_text }} +
    +
    +
    + {% endif %} + {% endfor %} +
    +
    +
    + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/common/templates/common/ldap_setting.html b/apps/common/templates/common/ldap_setting.html new file mode 100644 index 000000000..26f021569 --- /dev/null +++ b/apps/common/templates/common/ldap_setting.html @@ -0,0 +1,100 @@ +{% extends 'base.html' %} +{% load static %} +{% load bootstrap3 %} +{% load i18n %} +{% load common_tags %} + +{% block content %} +
    +
    +
    +
    + +
    +
    +
    +
    + {% if form.non_field_errors %} +
    + {{ form.non_field_errors }} +
    + {% endif %} + {% csrf_token %} + {% for field in form %} + {% if not field.field|is_bool_field %} + {% bootstrap_field field layout="horizontal" %} + {% else %} +
    + +
    +
    + {{ field }} +
    +
    + {{ field.help_text }} +
    +
    +
    + {% endif %} + {% endfor %} +
    +
    +
    + + + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +{% endblock %} +{% block custom_foot_js %} + +{% endblock %} diff --git a/apps/common/templatetags/common_tags.py b/apps/common/templatetags/common_tags.py index 9c3e58871..c10c228c8 100644 --- a/apps/common/templatetags/common_tags.py +++ b/apps/common/templatetags/common_tags.py @@ -4,6 +4,7 @@ from django import template from django.utils import timezone from django.utils.translation import gettext as _ from django.utils.html import escape +from django import forms register = template.Library() @@ -83,3 +84,11 @@ def time_util_with_seconds(date_from, date_to): return '{} h'.format(seconds//3600) else: return '' + + +@register.filter +def is_bool_field(field): + if isinstance(field, forms.BooleanField): + return True + else: + return False diff --git a/apps/common/urls/api_urls.py b/apps/common/urls/api_urls.py new file mode 100644 index 000000000..a9075e66e --- /dev/null +++ b/apps/common/urls/api_urls.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import + +from django.conf.urls import url + +from .. import api + +app_name = 'common' + +urlpatterns = [ + url(r'^v1/mail/testing/$', api.MailTestingAPI.as_view(), name='mail-testing'), + url(r'^v1/ldap/testing/$', api.LDAPTestingAPI.as_view(), name='ldap-testing'), + url(r'^v1/django-settings/$', api.DjangoSettingsAPI.as_view(), name='django-settings'), +] diff --git a/apps/common/urls/view_urls.py b/apps/common/urls/view_urls.py new file mode 100644 index 000000000..ff8086bde --- /dev/null +++ b/apps/common/urls/view_urls.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import + +from django.conf.urls import url + +from .. import views + +app_name = 'common' + +urlpatterns = [ + url(r'^$', views.BasicSettingView.as_view(), name='basic-setting'), + url(r'^email/$', views.EmailSettingView.as_view(), name='email-setting'), + url(r'^ldap/$', views.LDAPSettingView.as_view(), name='ldap-setting'), +] diff --git a/apps/common/utils.py b/apps/common/utils.py index 753fdd541..f366e6786 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -91,7 +91,7 @@ class Signer(metaclass=Singleton): def date_expired_default(): try: - years = int(settings.CONFIG.DEFAULT_EXPIRED_YEARS) + years = int(settings.DEFAULT_EXPIRED_YEARS) except TypeError: years = 70 return timezone.now() + timezone.timedelta(days=365*years) diff --git a/apps/common/views.py b/apps/common/views.py index 4cee32ba7..43b249cee 100644 --- a/apps/common/views.py +++ b/apps/common/views.py @@ -1,2 +1,88 @@ -from __future__ import absolute_import, unicode_literals +from django.views.generic import View, TemplateView +from django.shortcuts import render, redirect +from django.contrib import messages +from django.utils.translation import ugettext as _ +from .forms import EmailSettingForm, LDAPSettingForm, BasicSettingForm +from .mixins import AdminUserRequiredMixin +from .signals import ldap_auth_enable + + +class BasicSettingView(AdminUserRequiredMixin, TemplateView): + form_class = BasicSettingForm + template_name = "common/basic_setting.html" + + def get_context_data(self, **kwargs): + context = { + 'app': _('Settings'), + 'action': _('Basic setting'), + 'form': self.form_class(), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def post(self, request): + form = self.form_class(request.POST) + if form.is_valid(): + form.save() + msg = _("Update setting successfully, please restart program") + messages.success(request, msg) + return redirect('settings:basic-setting') + else: + context = self.get_context_data() + context.update({"form": form}) + return render(request, self.template_name, context) + + +class EmailSettingView(AdminUserRequiredMixin, TemplateView): + form_class = EmailSettingForm + template_name = "common/email_setting.html" + + def get_context_data(self, **kwargs): + context = { + 'app': _('Settings'), + 'action': _('Email setting'), + 'form': self.form_class(), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def post(self, request): + form = self.form_class(request.POST) + if form.is_valid(): + form.save() + msg = _("Update setting successfully, please restart program") + messages.success(request, msg) + return redirect('settings:email-setting') + else: + context = self.get_context_data() + context.update({"form": form}) + return render(request, self.template_name, context) + + +class LDAPSettingView(AdminUserRequiredMixin, TemplateView): + form_class = LDAPSettingForm + template_name = "common/ldap_setting.html" + + def get_context_data(self, **kwargs): + context = { + 'app': _('Settings'), + 'action': _('LDAP setting'), + 'form': self.form_class(), + } + kwargs.update(context) + return super().get_context_data(**kwargs) + + def post(self, request): + form = self.form_class(request.POST) + if form.is_valid(): + form.save() + if "AUTH_LDAP" in form.cleaned_data: + ldap_auth_enable.send(form.cleaned_data["AUTH_LDAP"]) + msg = _("Update setting successfully, please restart program") + messages.success(request, msg) + return redirect('settings:ldap-setting') + else: + context = self.get_context_data() + context.update({"form": form}) + return render(request, self.template_name, context) diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 214aba3aa..7a8aa422e 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -15,7 +15,6 @@ import sys import ldap from django_auth_ldap.config import LDAPSearch - from django.urls import reverse_lazy @@ -121,15 +120,6 @@ MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' # Database # https://docs.djangoproject.com/en/1.10/ref/settings/#databases -# if CONFIG.DB_ENGINE == 'sqlite': -# DATABASES = { -# 'default': { -# 'ENGINE': 'django.db.backends.sqlite3', -# 'NAME': CONFIG.DB_NAME or os.path.join(BASE_DIR, 'data', 'db.sqlite3'), -# 'ATOMIC_REQUESTS': True, -# } -# } - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.{}'.format(CONFIG.DB_ENGINE), @@ -284,7 +274,7 @@ EMAIL_HOST_USER = CONFIG.EMAIL_HOST_USER EMAIL_HOST_PASSWORD = CONFIG.EMAIL_HOST_PASSWORD EMAIL_USE_SSL = CONFIG.EMAIL_USE_SSL EMAIL_USE_TLS = CONFIG.EMAIL_USE_TLS -EMAIL_SUBJECT_PREFIX = CONFIG.EMAIL_SUBJECT_PREFIX +EMAIL_SUBJECT_PREFIX = CONFIG.EMAIL_SUBJECT_PREFIX or '' REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, @@ -298,9 +288,17 @@ REST_FRAMEWORK = { 'users.authentication.PrivateTokenAuthentication', 'users.authentication.SessionAuthentication', ), - 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',), + 'DEFAULT_FILTER_BACKENDS': ( + 'django_filters.rest_framework.DjangoFilterBackend', + 'rest_framework.filters.SearchFilter', + 'rest_framework.filters.OrderingFilter', + ), + 'ORDERING_PARAM': "order", + 'SEARCH_PARAM': "search", 'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z', 'DATETIME_INPUT_FORMATS': ['%Y-%m-%d %H:%M:%S %z'], + # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', + 'PAGE_SIZE': 15 } AUTHENTICATION_BACKENDS = [ @@ -312,18 +310,28 @@ AUTH_USER_MODEL = 'users.User' # Auth LDAP settings -if CONFIG.AUTH_LDAP: - AUTHENTICATION_BACKENDS.insert(0, 'django_auth_ldap.backend.LDAPBackend') - AUTH_LDAP_SERVER_URI = CONFIG.AUTH_LDAP_SERVER_URI - AUTH_LDAP_BIND_DN = CONFIG.AUTH_LDAP_BIND_DN - AUTH_LDAP_BIND_PASSWORD = CONFIG.AUTH_LDAP_BIND_PASSWORD - AUTH_LDAP_USER_SEARCH = LDAPSearch( - CONFIG.AUTH_LDAP_SEARCH_OU, - ldap.SCOPE_SUBTREE, - CONFIG.AUTH_LDAP_SEARCH_FILTER - ) - AUTH_LDAP_START_TLS = CONFIG.AUTH_LDAP_START_TLS - AUTH_LDAP_USER_ATTR_MAP = CONFIG.AUTH_LDAP_USER_ATTR_MAP +AUTH_LDAP = CONFIG.AUTH_LDAP +AUTH_LDAP_SERVER_URI = CONFIG.AUTH_LDAP_SERVER_URI +AUTH_LDAP_BIND_DN = CONFIG.AUTH_LDAP_BIND_DN +AUTH_LDAP_BIND_PASSWORD = CONFIG.AUTH_LDAP_BIND_PASSWORD +AUTH_LDAP_SEARCH_OU = CONFIG.AUTH_LDAP_SEARCH_OU +AUTH_LDAP_SEARCH_FILTER = CONFIG.AUTH_LDAP_SEARCH_FILTER +AUTH_LDAP_START_TLS = CONFIG.AUTH_LDAP_START_TLS +AUTH_LDAP_USER_ATTR_MAP = CONFIG.AUTH_LDAP_USER_ATTR_MAP +AUTH_LDAP_USER_SEARCH = LDAPSearch( + AUTH_LDAP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_SEARCH_FILTER, +) +AUTH_LDAP_GROUP_SEARCH_OU = CONFIG.AUTH_LDAP_GROUP_SEARCH_OU +AUTH_LDAP_GROUP_SEARCH_FILTER = CONFIG.AUTH_LDAP_GROUP_SEARCH_FILTER +AUTH_LDAP_GROUP_SEARCH = LDAPSearch( + AUTH_LDAP_GROUP_SEARCH_OU, ldap.SCOPE_SUBTREE, AUTH_LDAP_GROUP_SEARCH_FILTER +) +AUTH_LDAP_ALWAYS_UPDATE_USER = True +AUTH_LDAP_BACKEND = 'django_auth_ldap.backend.LDAPBackend' + +if AUTH_LDAP: + AUTHENTICATION_BACKENDS.insert(0, AUTH_LDAP_BACKEND) + # Celery using redis as broker CELERY_BROKER_URL = 'redis://:%(password)s@%(host)s:%(port)s/3' % { @@ -374,4 +382,10 @@ BOOTSTRAP3 = { 'horizontal_field_class': 'col-md-9', # Set placeholder attributes to label if no placeholder is provided 'set_placeholder': True, + 'success_css_class': '', } + +TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION or 3600 +DISPLAY_PER_PAGE = CONFIG.DISPLAY_PER_PAGE +DEFAULT_EXPIRED_YEARS = 70 +USER_GUIDE_URL = "" diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 21d42d0dd..2eb54d87d 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -19,6 +19,8 @@ urlpatterns = [ url(r'^perms/', include('perms.urls.views_urls', namespace='perms')), url(r'^terminal/', include('terminal.urls.views_urls', namespace='terminal')), url(r'^ops/', include('ops.urls.view_urls', namespace='ops')), + url(r'^settings/', include('common.urls.view_urls', namespace='settings')), + url(r'^common/', include('common.urls.view_urls', namespace='common')), # Api url view map url(r'^api/users/', include('users.urls.api_urls', namespace='api-users')), @@ -26,16 +28,15 @@ urlpatterns = [ url(r'^api/perms/', include('perms.urls.api_urls', namespace='api-perms')), url(r'^api/terminal/', include('terminal.urls.api_urls', namespace='api-terminal')), url(r'^api/ops/', include('ops.urls.api_urls', namespace='api-ops')), + url(r'^api/common/', include('common.urls.api_urls', namespace='api-common')), # External apps url url(r'^captcha/', include('captcha.urls')), - ] - if settings.DEBUG: urlpatterns += [ url(r'^docs/', schema_view, name="docs"), - ] + static(settings.STATIC_URL, document_root=settings.STATIC_DIR) \ + ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 4eed75216..7be8a996a 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 910a4fa02..ebcc5ac66 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: 2018-01-08 15:58+0800\n" +"POT-Creation-Date: 2018-01-17 17:26+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: ibuler \n" "Language-Team: Jumpserver team\n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" #: assets/forms.py:23 assets/forms.py:53 assets/forms.py:99 perms/forms.py:37 -#: perms/templates/perms/asset_permission_asset.html:116 users/forms.py:242 +#: perms/templates/perms/asset_permission_asset.html:116 users/forms.py:246 msgid "Select asset groups" msgstr "选择资产组" @@ -43,15 +43,15 @@ msgid "Default using cluster admin user" msgstr "默认使用管理用户" #: assets/forms.py:76 assets/forms.py:81 assets/forms.py:127 -#: assets/templates/assets/asset_group_detail.html:70 perms/forms.py:34 -#: perms/templates/perms/asset_permission_asset.html:88 users/forms.py:239 +#: assets/templates/assets/asset_group_detail.html:75 perms/forms.py:34 +#: perms/templates/perms/asset_permission_asset.html:88 users/forms.py:243 msgid "Select assets" msgstr "选择资产" -#: assets/forms.py:86 assets/models/asset.py:45 +#: assets/forms.py:86 assets/models/asset.py:55 #: assets/templates/assets/admin_user_assets.html:61 #: assets/templates/assets/asset_detail.html:69 -#: assets/templates/assets/asset_group_detail.html:47 +#: assets/templates/assets/asset_group_detail.html:52 #: assets/templates/assets/asset_list.html:32 #: assets/templates/assets/cluster_assets.html:53 #: assets/templates/assets/system_user_asset.html:54 @@ -60,7 +60,7 @@ msgstr "选择资产" msgid "Port" msgstr "端口" -#: assets/forms.py:124 assets/models/asset.py:161 +#: assets/forms.py:124 assets/models/asset.py:171 #: assets/templates/assets/admin_user_list.html:24 #: assets/templates/assets/asset_group_list.html:16 #: assets/templates/assets/system_user_list.html:26 perms/models.py:17 @@ -76,39 +76,39 @@ msgstr "端口" msgid "Asset" msgstr "资产" -#: assets/forms.py:156 perms/forms.py:40 -#: perms/templates/perms/asset_permission_detail.html:144 users/forms.py:245 +#: assets/forms.py:161 perms/forms.py:40 +#: perms/templates/perms/asset_permission_detail.html:144 users/forms.py:249 msgid "Select system users" msgstr "选择系统用户" -#: assets/forms.py:158 +#: assets/forms.py:163 #: assets/templates/assets/_asset_group_bulk_update_modal.html:22 #: assets/templates/assets/cluster_list.html:22 msgid "System users" msgstr "系统用户" -#: assets/forms.py:160 +#: assets/forms.py:165 msgid "Selected system users will be create at cluster assets" msgstr "选择的系统用户将会在该集群资产上创建" -#: assets/forms.py:168 assets/forms.py:243 assets/forms.py:302 +#: assets/forms.py:173 assets/forms.py:248 assets/forms.py:308 #: assets/models/cluster.py:18 assets/models/group.py:20 #: assets/models/user.py:28 assets/templates/assets/admin_user_detail.html:56 #: assets/templates/assets/admin_user_list.html:22 #: assets/templates/assets/asset_group_list.html:15 #: assets/templates/assets/cluster_detail.html:57 #: assets/templates/assets/cluster_list.html:19 -#: assets/templates/assets/system_user_detail.html:53 -#: assets/templates/assets/system_user_list.html:24 ops/models.py:31 -#: ops/templates/ops/task_detail.html:56 ops/templates/ops/task_list.html:34 -#: perms/models.py:14 +#: assets/templates/assets/system_user_detail.html:58 +#: assets/templates/assets/system_user_list.html:24 common/models.py:25 +#: ops/models.py:31 ops/templates/ops/task_detail.html:56 +#: ops/templates/ops/task_list.html:34 perms/models.py:14 #: perms/templates/perms/asset_permission_create_update.html:33 #: perms/templates/perms/asset_permission_detail.html:62 #: perms/templates/perms/asset_permission_list.html:25 #: perms/templates/perms/asset_permission_user.html:54 terminal/models.py:14 #: terminal/models.py:118 terminal/templates/terminal/terminal_detail.html:43 #: terminal/templates/terminal/terminal_list.html:29 users/models/group.py:14 -#: users/models/user.py:36 users/templates/users/_select_user_modal.html:13 +#: users/models/user.py:35 users/templates/users/_select_user_modal.html:13 #: users/templates/users/user_detail.html:62 #: users/templates/users/user_granted_asset.html:81 #: users/templates/users/user_group_detail.html:55 @@ -120,16 +120,17 @@ msgstr "选择的系统用户将会在该集群资产上创建" msgid "Name" msgstr "名称" -#: assets/forms.py:174 +#: assets/forms.py:179 msgid "Cluster level admin user" msgstr "集群级别管理用户" -#: assets/forms.py:195 +#: assets/forms.py:200 msgid "Password or private key password" msgstr "密码或秘钥不合法" -#: assets/forms.py:196 assets/models/user.py:30 users/forms.py:16 -#: users/forms.py:24 users/templates/users/login.html:56 +#: assets/forms.py:201 assets/forms.py:262 assets/models/user.py:30 +#: common/forms.py:107 users/forms.py:16 users/forms.py:24 +#: users/templates/users/login.html:56 #: users/templates/users/reset_password.html:52 #: users/templates/users/user_create.html:11 #: users/templates/users/user_password_update.html:40 @@ -138,25 +139,25 @@ msgstr "密码或秘钥不合法" msgid "Password" msgstr "密码" -#: assets/forms.py:199 users/models/user.py:46 +#: assets/forms.py:204 assets/forms.py:264 users/models/user.py:45 msgid "Private key" msgstr "ssh私钥" -#: assets/forms.py:224 assets/forms.py:284 assets/forms.py:345 +#: assets/forms.py:229 assets/forms.py:290 assets/forms.py:354 msgid "Invalid private key" msgstr "ssh密钥不合法" -#: assets/forms.py:235 +#: assets/forms.py:240 msgid "Password and private key file must be input one" msgstr "密码和私钥, 必须输入一个" -#: assets/forms.py:244 assets/forms.py:303 assets/models/user.py:29 +#: assets/forms.py:249 assets/forms.py:309 assets/models/user.py:29 #: assets/templates/assets/admin_user_detail.html:60 #: assets/templates/assets/admin_user_list.html:23 -#: assets/templates/assets/system_user_detail.html:57 +#: assets/templates/assets/system_user_detail.html:62 #: assets/templates/assets/system_user_list.html:25 #: perms/templates/perms/asset_permission_user.html:55 users/forms.py:14 -#: users/models/authentication.py:44 users/models/user.py:35 +#: users/models/authentication.py:44 users/models/user.py:34 #: users/templates/users/_select_user_modal.html:14 #: users/templates/users/login.html:53 #: users/templates/users/login_log_list.html:49 @@ -166,75 +167,75 @@ msgstr "密码和私钥, 必须输入一个" msgid "Username" msgstr "用户名" -#: assets/forms.py:291 assets/forms.py:351 +#: assets/forms.py:297 assets/forms.py:360 msgid "Auth info required, private_key or password" msgstr "密钥和密码必须填写一个" -#: assets/forms.py:306 +#: assets/forms.py:313 msgid " Select clusters" msgstr "选择集群" -#: assets/forms.py:311 +#: assets/forms.py:320 msgid "If auto push checked, system user will be create at cluster assets" msgstr "如果选择了自动推送,系统用户将会创建在集群资产上" -#: assets/forms.py:312 +#: assets/forms.py:321 msgid "Auto push system user to asset" msgstr "自动推送系统用户到资产" -#: assets/forms.py:313 +#: assets/forms.py:322 msgid "" "High level will be using login asset as default, if user was granted more " "than 2 system user" msgstr "高优先级的系统用户将会作为默认登录用户" -#: assets/models/asset.py:24 +#: assets/models/asset.py:34 msgid "In use" msgstr "使用中" -#: assets/models/asset.py:25 +#: assets/models/asset.py:35 msgid "Out of use" msgstr "未使用" -#: assets/models/asset.py:28 +#: assets/models/asset.py:38 msgid "Server" msgstr "物理机" -#: assets/models/asset.py:29 +#: assets/models/asset.py:39 msgid "VM" msgstr "虚拟机" -#: assets/models/asset.py:30 +#: assets/models/asset.py:40 msgid "Switch" msgstr "交换机" -#: assets/models/asset.py:31 +#: assets/models/asset.py:41 msgid "Router" msgstr "路由器" -#: assets/models/asset.py:32 +#: assets/models/asset.py:42 msgid "Firewall" msgstr "防火墙" -#: assets/models/asset.py:33 +#: assets/models/asset.py:43 msgid "Storage" msgstr "存储" -#: assets/models/asset.py:36 +#: assets/models/asset.py:46 msgid "Production" msgstr "生产环境" -#: assets/models/asset.py:37 +#: assets/models/asset.py:47 msgid "Development" msgstr "开发环境" -#: assets/models/asset.py:38 +#: assets/models/asset.py:48 msgid "Testing" msgstr "测试环境" -#: assets/models/asset.py:43 assets/templates/assets/admin_user_assets.html:60 +#: assets/models/asset.py:53 assets/templates/assets/admin_user_assets.html:60 #: assets/templates/assets/asset_detail.html:61 -#: assets/templates/assets/asset_group_detail.html:46 +#: assets/templates/assets/asset_group_detail.html:51 #: assets/templates/assets/asset_list.html:31 #: assets/templates/assets/cluster_assets.html:52 #: assets/templates/assets/system_user_asset.html:53 @@ -246,9 +247,9 @@ msgstr "测试环境" msgid "IP" msgstr "IP" -#: assets/models/asset.py:44 assets/templates/assets/admin_user_assets.html:59 +#: assets/models/asset.py:54 assets/templates/assets/admin_user_assets.html:59 #: assets/templates/assets/asset_detail.html:57 -#: assets/templates/assets/asset_group_detail.html:45 +#: assets/templates/assets/asset_group_detail.html:50 #: assets/templates/assets/asset_list.html:30 #: assets/templates/assets/cluster_assets.html:51 #: assets/templates/assets/system_user_asset.html:52 @@ -259,131 +260,131 @@ msgstr "IP" msgid "Hostname" msgstr "主机名" -#: assets/models/asset.py:46 assets/templates/assets/asset_detail.html:213 -#: assets/views/asset.py:212 assets/views/asset.py:252 +#: assets/models/asset.py:56 assets/templates/assets/asset_detail.html:213 +#: assets/views/asset.py:218 assets/views/asset.py:258 msgid "Asset groups" msgstr "资产组" -#: assets/models/asset.py:47 assets/models/cluster.py:40 -#: assets/models/user.py:215 assets/templates/assets/asset_detail.html:85 +#: assets/models/asset.py:57 assets/models/cluster.py:40 +#: assets/models/user.py:219 assets/templates/assets/asset_detail.html:85 #: assets/templates/assets/asset_list.html:33 templates/_nav.html:24 msgid "Cluster" msgstr "集群" -#: assets/models/asset.py:48 assets/templates/assets/asset_detail.html:129 +#: assets/models/asset.py:58 assets/templates/assets/asset_detail.html:129 msgid "Is active" msgstr "激活" -#: assets/models/asset.py:49 assets/templates/assets/asset_detail.html:133 +#: assets/models/asset.py:59 assets/templates/assets/asset_detail.html:133 msgid "Asset type" msgstr "系统类型" -#: assets/models/asset.py:50 assets/templates/assets/asset_detail.html:137 +#: assets/models/asset.py:60 assets/templates/assets/asset_detail.html:137 msgid "Asset environment" msgstr "资产环境" -#: assets/models/asset.py:51 assets/templates/assets/asset_detail.html:125 +#: assets/models/asset.py:61 assets/templates/assets/asset_detail.html:125 msgid "Asset status" msgstr "资产状态" -#: assets/models/asset.py:54 assets/models/cluster.py:19 -#: assets/models/user.py:186 assets/templates/assets/asset_detail.html:73 +#: assets/models/asset.py:64 assets/models/cluster.py:19 +#: assets/models/user.py:190 assets/templates/assets/asset_detail.html:73 #: assets/templates/assets/cluster_list.html:20 templates/_nav.html:25 msgid "Admin user" msgstr "管理用户" -#: assets/models/asset.py:57 assets/templates/assets/asset_detail.html:65 +#: assets/models/asset.py:67 assets/templates/assets/asset_detail.html:65 msgid "Public IP" msgstr "公网IP" -#: assets/models/asset.py:58 +#: assets/models/asset.py:68 msgid "Remote control card IP" msgstr "远控卡IP" -#: assets/models/asset.py:59 assets/templates/assets/asset_detail.html:89 +#: assets/models/asset.py:69 assets/templates/assets/asset_detail.html:89 msgid "Cabinet number" msgstr "机柜编号" -#: assets/models/asset.py:60 assets/templates/assets/asset_detail.html:93 +#: assets/models/asset.py:70 assets/templates/assets/asset_detail.html:93 msgid "Cabinet position" msgstr "机柜层号" -#: assets/models/asset.py:61 assets/templates/assets/asset_detail.html:145 +#: assets/models/asset.py:71 assets/templates/assets/asset_detail.html:145 msgid "Asset number" msgstr "资产编号" -#: assets/models/asset.py:64 assets/templates/assets/asset_detail.html:97 +#: assets/models/asset.py:74 assets/templates/assets/asset_detail.html:97 msgid "Vendor" msgstr "制造商" -#: assets/models/asset.py:65 assets/templates/assets/asset_detail.html:101 +#: assets/models/asset.py:75 assets/templates/assets/asset_detail.html:101 msgid "Model" msgstr "型号" -#: assets/models/asset.py:66 assets/templates/assets/asset_detail.html:141 +#: assets/models/asset.py:76 assets/templates/assets/asset_detail.html:141 msgid "Serial number" msgstr "序列号" -#: assets/models/asset.py:68 +#: assets/models/asset.py:78 msgid "CPU model" msgstr "CPU型号" -#: assets/models/asset.py:69 +#: assets/models/asset.py:79 msgid "CPU count" msgstr "CPU数量" -#: assets/models/asset.py:70 +#: assets/models/asset.py:80 msgid "CPU cores" msgstr "CPU核数" -#: assets/models/asset.py:71 assets/templates/assets/asset_detail.html:109 +#: assets/models/asset.py:81 assets/templates/assets/asset_detail.html:109 msgid "Memory" msgstr "内存" -#: assets/models/asset.py:72 +#: assets/models/asset.py:82 msgid "Disk total" msgstr "硬盘大小" -#: assets/models/asset.py:73 +#: assets/models/asset.py:83 msgid "Disk info" msgstr "硬盘信息" -#: assets/models/asset.py:75 assets/templates/assets/asset_detail.html:117 +#: assets/models/asset.py:85 assets/templates/assets/asset_detail.html:117 msgid "Platform" msgstr "系统平台" -#: assets/models/asset.py:76 assets/templates/assets/asset_detail.html:121 +#: assets/models/asset.py:86 assets/templates/assets/asset_detail.html:121 msgid "OS" msgstr "操作系统" -#: assets/models/asset.py:77 +#: assets/models/asset.py:87 msgid "OS version" msgstr "系统版本" -#: assets/models/asset.py:78 +#: assets/models/asset.py:88 msgid "OS arch" msgstr "系统架构" -#: assets/models/asset.py:79 +#: assets/models/asset.py:89 msgid "Hostname raw" msgstr "主机名原始" -#: assets/models/asset.py:81 assets/models/cluster.py:28 +#: assets/models/asset.py:91 assets/models/cluster.py:28 #: assets/models/group.py:21 assets/models/user.py:36 #: assets/templates/assets/admin_user_detail.html:68 #: assets/templates/assets/asset_detail.html:149 #: assets/templates/assets/cluster_detail.html:93 -#: assets/templates/assets/system_user_detail.html:91 +#: assets/templates/assets/system_user_detail.html:96 #: ops/templates/ops/adhoc_detail.html:86 perms/models.py:22 #: perms/templates/perms/asset_permission_detail.html:94 -#: users/models/user.py:51 users/templates/users/user_detail.html:98 +#: users/models/user.py:50 users/templates/users/user_detail.html:98 msgid "Created by" msgstr "创建者" -#: assets/models/asset.py:82 assets/models/cluster.py:26 +#: assets/models/asset.py:92 assets/models/cluster.py:26 #: assets/models/group.py:22 assets/templates/assets/admin_user_detail.html:64 #: assets/templates/assets/cluster_detail.html:89 -#: assets/templates/assets/system_user_detail.html:87 +#: assets/templates/assets/system_user_detail.html:92 #: ops/templates/ops/adhoc_detail.html:90 ops/templates/ops/task_detail.html:60 #: perms/models.py:23 perms/templates/perms/asset_permission_detail.html:90 #: terminal/templates/terminal/terminal_detail.html:59 users/models/group.py:17 @@ -391,19 +392,19 @@ msgstr "创建者" msgid "Date created" msgstr "创建日期" -#: assets/models/asset.py:83 assets/models/cluster.py:29 +#: assets/models/asset.py:93 assets/models/cluster.py:29 #: assets/models/group.py:23 assets/models/user.py:33 #: assets/templates/assets/admin_user_detail.html:72 #: assets/templates/assets/admin_user_list.html:28 #: assets/templates/assets/asset_detail.html:157 #: assets/templates/assets/asset_group_list.html:17 #: assets/templates/assets/cluster_detail.html:97 -#: assets/templates/assets/system_user_detail.html:95 -#: assets/templates/assets/system_user_list.html:30 ops/models.py:37 -#: perms/models.py:24 perms/templates/perms/asset_permission_detail.html:98 -#: terminal/models.py:22 terminal/templates/terminal/terminal_detail.html:63 -#: users/models/group.py:15 users/models/user.py:48 -#: users/templates/users/user_detail.html:110 +#: assets/templates/assets/system_user_detail.html:100 +#: assets/templates/assets/system_user_list.html:30 common/models.py:28 +#: ops/models.py:37 perms/models.py:24 +#: perms/templates/perms/asset_permission_detail.html:98 terminal/models.py:22 +#: terminal/templates/terminal/terminal_detail.html:63 users/models/group.py:15 +#: users/models/user.py:47 users/templates/users/user_detail.html:110 #: users/templates/users/user_group_detail.html:67 #: users/templates/users/user_group_list.html:14 #: users/templates/users/user_profile.html:118 @@ -419,7 +420,7 @@ msgid "Contact" msgstr "联系人" #: assets/models/cluster.py:22 assets/templates/assets/cluster_detail.html:69 -#: users/models/user.py:42 users/templates/users/user_detail.html:75 +#: users/models/user.py:41 users/templates/users/user_detail.html:75 msgid "Phone" msgstr "手机" @@ -443,7 +444,7 @@ msgstr "运营商" msgid "Default" msgstr "默认" -#: assets/models/cluster.py:36 users/models/user.py:259 +#: assets/models/cluster.py:36 users/models/user.py:258 msgid "System" msgstr "系统" @@ -468,29 +469,29 @@ msgstr "ssh密钥" msgid "SSH public key" msgstr "ssh公钥" -#: assets/models/user.py:216 +#: assets/models/user.py:220 msgid "Priority" msgstr "优先级" -#: assets/models/user.py:217 assets/templates/assets/system_user_detail.html:61 +#: assets/models/user.py:221 assets/templates/assets/system_user_detail.html:66 msgid "Protocol" msgstr "协议" -#: assets/models/user.py:218 assets/templates/assets/_system_user.html:59 -#: assets/templates/assets/system_user_detail.html:113 +#: assets/models/user.py:222 assets/templates/assets/_system_user.html:59 +#: assets/templates/assets/system_user_detail.html:118 #: assets/templates/assets/system_user_update.html:11 msgid "Auto push" msgstr "自动推送" -#: assets/models/user.py:219 assets/templates/assets/system_user_detail.html:65 +#: assets/models/user.py:223 assets/templates/assets/system_user_detail.html:70 msgid "Sudo" msgstr "Sudo" -#: assets/models/user.py:220 assets/templates/assets/system_user_detail.html:70 +#: assets/models/user.py:224 assets/templates/assets/system_user_detail.html:75 msgid "Shell" msgstr "Shell" -#: assets/models/user.py:265 perms/models.py:19 +#: assets/models/user.py:269 perms/models.py:19 #: perms/templates/perms/asset_permission_detail.html:136 #: perms/templates/perms/asset_permission_list.html:30 templates/_nav.html:26 #: terminal/backends/command/models.py:12 terminal/models.py:94 @@ -552,11 +553,11 @@ msgstr "测试系统用户可连接性: {}" msgid "Test system user connectability period: {}" msgstr "定期测试系统用户可连接性: {}" -#: assets/tasks.py:372 +#: assets/tasks.py:376 msgid "Push system user to cluster assets: {}" msgstr "推送系统用户到资产: {}" -#: assets/tasks.py:393 +#: assets/tasks.py:397 msgid "Push cluster system users to assets period: {}" msgstr "定期推送集群系统用户到资产: {}" @@ -570,16 +571,16 @@ msgstr "仅修改你需要更新的字段" #: assets/templates/assets/_asset_group_bulk_update_modal.html:12 #: assets/templates/assets/system_user_asset.html:21 -#: assets/views/admin_user.py:27 assets/views/admin_user.py:44 -#: assets/views/admin_user.py:67 assets/views/admin_user.py:88 -#: assets/views/admin_user.py:115 assets/views/asset.py:47 -#: assets/views/asset.py:61 assets/views/asset.py:84 assets/views/asset.py:141 -#: assets/views/asset.py:158 assets/views/asset.py:179 -#: assets/views/cluster.py:22 assets/views/cluster.py:80 -#: assets/views/cluster.py:97 assets/views/group.py:30 assets/views/group.py:53 -#: assets/views/group.py:71 assets/views/group.py:93 -#: assets/views/system_user.py:29 assets/views/system_user.py:48 -#: assets/views/system_user.py:71 assets/views/system_user.py:91 +#: assets/views/admin_user.py:29 assets/views/admin_user.py:47 +#: assets/views/admin_user.py:63 assets/views/admin_user.py:79 +#: assets/views/admin_user.py:106 assets/views/asset.py:48 +#: assets/views/asset.py:61 assets/views/asset.py:84 assets/views/asset.py:144 +#: assets/views/asset.py:161 assets/views/asset.py:185 +#: assets/views/cluster.py:26 assets/views/cluster.py:85 +#: assets/views/cluster.py:102 assets/views/group.py:34 +#: assets/views/group.py:52 assets/views/group.py:69 assets/views/group.py:87 +#: assets/views/system_user.py:28 assets/views/system_user.py:44 +#: assets/views/system_user.py:60 assets/views/system_user.py:75 #: templates/_nav.html:19 msgid "Assets" msgstr "资产管理" @@ -620,7 +621,7 @@ msgstr "如果设置了id,则会使用该行信息更新该id的资产" #: assets/templates/assets/_system_user.html:16 #: assets/templates/assets/system_user_list.html:16 -#: assets/views/system_user.py:49 +#: assets/views/system_user.py:45 msgid "Create system user" msgstr "创建系统用户" @@ -657,8 +658,12 @@ msgstr "其它" #: assets/templates/assets/admin_user_create_update.html:45 #: assets/templates/assets/asset_bulk_update.html:23 #: assets/templates/assets/asset_create.html:40 +#: assets/templates/assets/asset_group_create.html:16 #: assets/templates/assets/asset_update.html:55 #: assets/templates/assets/cluster_create_update.html:54 +#: common/templates/common/basic_setting.html:55 +#: common/templates/common/email_setting.html:56 +#: common/templates/common/ldap_setting.html:56 #: perms/templates/perms/asset_permission_create_update.html:67 #: terminal/templates/terminal/terminal_update.html:45 #: users/templates/users/_user.html:49 @@ -675,9 +680,13 @@ msgstr "重置" #: assets/templates/assets/admin_user_create_update.html:46 #: assets/templates/assets/asset_bulk_update.html:24 #: assets/templates/assets/asset_create.html:41 -#: assets/templates/assets/asset_list.html:55 +#: assets/templates/assets/asset_group_create.html:17 +#: assets/templates/assets/asset_list.html:53 #: assets/templates/assets/asset_update.html:56 #: assets/templates/assets/cluster_create_update.html:55 +#: common/templates/common/basic_setting.html:56 +#: common/templates/common/email_setting.html:57 +#: common/templates/common/ldap_setting.html:57 #: perms/templates/perms/asset_permission_create_update.html:68 #: terminal/templates/terminal/terminal_update.html:46 #: users/templates/users/_user.html:50 @@ -711,15 +720,62 @@ msgstr "详情" msgid "Assets list" msgstr "资产列表" +#: assets/templates/assets/admin_user_assets.html:24 +#: assets/templates/assets/admin_user_detail.html:24 +#: assets/templates/assets/admin_user_list.html:83 +#: assets/templates/assets/asset_detail.html:24 +#: assets/templates/assets/asset_group_detail.html:18 +#: assets/templates/assets/asset_group_detail.html:177 +#: assets/templates/assets/asset_group_list.html:38 +#: assets/templates/assets/asset_list.html:98 +#: assets/templates/assets/cluster_assets.html:170 +#: assets/templates/assets/cluster_detail.html:25 +#: assets/templates/assets/cluster_list.html:43 +#: assets/templates/assets/system_user_asset.html:25 +#: assets/templates/assets/system_user_detail.html:26 +#: assets/templates/assets/system_user_list.html:84 +#: perms/templates/perms/asset_permission_detail.html:30 +#: perms/templates/perms/asset_permission_list.html:73 +#: terminal/templates/terminal/terminal_detail.html:16 +#: terminal/templates/terminal/terminal_list.html:71 +#: users/templates/users/user_detail.html:25 +#: users/templates/users/user_group_detail.html:28 +#: users/templates/users/user_group_list.html:39 +#: users/templates/users/user_list.html:76 +msgid "Update" +msgstr "更新" + +#: assets/templates/assets/admin_user_assets.html:28 +#: assets/templates/assets/admin_user_detail.html:28 +#: assets/templates/assets/admin_user_list.html:84 +#: assets/templates/assets/asset_detail.html:28 +#: assets/templates/assets/asset_group_detail.html:22 +#: assets/templates/assets/asset_group_list.html:39 +#: assets/templates/assets/asset_list.html:99 +#: assets/templates/assets/cluster_detail.html:29 +#: assets/templates/assets/cluster_list.html:44 +#: assets/templates/assets/system_user_detail.html:30 +#: assets/templates/assets/system_user_list.html:85 +#: ops/templates/ops/task_list.html:71 +#: perms/templates/perms/asset_permission_detail.html:34 +#: perms/templates/perms/asset_permission_list.html:74 +#: terminal/templates/terminal/terminal_list.html:73 +#: users/templates/users/user_detail.html:29 +#: users/templates/users/user_group_detail.html:32 +#: users/templates/users/user_group_list.html:41 +#: users/templates/users/user_list.html:80 +#: users/templates/users/user_list.html:84 +msgid "Delete" +msgstr "删除" + #: assets/templates/assets/admin_user_assets.html:37 -#: assets/templates/assets/asset_group_detail.html:26 +#: assets/templates/assets/asset_group_detail.html:31 #: perms/templates/perms/asset_permission_asset.html:35 msgid "Asset list of " msgstr "资产列表" #: assets/templates/assets/admin_user_assets.html:62 -#: assets/templates/assets/asset_group_detail.html:48 -#: assets/templates/assets/asset_list.html:34 +#: assets/templates/assets/asset_group_detail.html:53 #: assets/templates/assets/cluster_assets.html:54 #: assets/templates/assets/user_asset_list.html:22 #: users/templates/users/login_log_list.html:50 @@ -729,7 +785,7 @@ msgstr "类型" #: assets/templates/assets/admin_user_assets.html:63 #: assets/templates/assets/admin_user_list.html:25 #: assets/templates/assets/asset_detail.html:376 -#: assets/templates/assets/asset_list.html:38 +#: assets/templates/assets/asset_list.html:36 #: assets/templates/assets/system_user_asset.html:55 #: assets/templates/assets/system_user_list.html:27 msgid "Reachable" @@ -738,7 +794,7 @@ msgstr "可连接" #: assets/templates/assets/admin_user_assets.html:75 #: assets/templates/assets/cluster_assets.html:68 #: assets/templates/assets/system_user_asset.html:67 -#: assets/templates/assets/system_user_detail.html:107 +#: assets/templates/assets/system_user_detail.html:112 #: perms/templates/perms/asset_permission_detail.html:110 msgid "Quick update" msgstr "快速更新" @@ -760,7 +816,7 @@ msgstr "任务已下发,查看左侧资产状态" #: assets/templates/assets/admin_user_create_update.html:16 #: assets/templates/assets/admin_user_list.html:14 -#: assets/views/admin_user.py:45 +#: assets/views/admin_user.py:48 msgid "Create admin user" msgstr "创建管理用户" @@ -770,19 +826,19 @@ msgstr "使用集群管理用户" #: assets/templates/assets/admin_user_detail.html:101 #: assets/templates/assets/asset_detail.html:230 -#: assets/templates/assets/asset_group_list.html:85 -#: assets/templates/assets/asset_list.html:202 +#: assets/templates/assets/asset_group_list.html:81 +#: assets/templates/assets/asset_list.html:220 #: assets/templates/assets/cluster_assets.html:104 #: assets/templates/assets/cluster_list.html:89 -#: assets/templates/assets/system_user_detail.html:159 +#: assets/templates/assets/system_user_detail.html:164 #: assets/templates/assets/system_user_list.html:134 templates/_modal.html:16 #: terminal/templates/terminal/session_detail.html:108 #: users/templates/users/user_detail.html:338 #: users/templates/users/user_detail.html:363 #: users/templates/users/user_detail.html:386 -#: users/templates/users/user_group_create_update.html:46 +#: users/templates/users/user_group_create_update.html:32 #: users/templates/users/user_group_list.html:82 -#: users/templates/users/user_list.html:184 +#: users/templates/users/user_list.html:196 #: users/templates/users/user_profile.html:181 msgid "Confirm" msgstr "确认" @@ -800,13 +856,12 @@ msgid "Ratio" msgstr "比例" #: assets/templates/assets/admin_user_list.html:29 -#: assets/templates/assets/asset_group_detail.html:50 +#: assets/templates/assets/asset_group_detail.html:55 #: assets/templates/assets/asset_group_list.html:18 -#: assets/templates/assets/asset_list.html:39 +#: assets/templates/assets/asset_list.html:37 #: assets/templates/assets/cluster_assets.html:56 #: assets/templates/assets/cluster_list.html:23 #: assets/templates/assets/system_user_list.html:31 -#: assets/templates/assets/user_asset_list.html:27 #: ops/templates/ops/adhoc_history.html:59 ops/templates/ops/task_adhoc.html:61 #: ops/templates/ops/task_history.html:62 ops/templates/ops/task_list.html:41 #: perms/templates/perms/asset_permission_list.html:32 @@ -817,41 +872,13 @@ msgstr "比例" msgid "Action" msgstr "动作" -#: assets/templates/assets/admin_user_list.html:83 -#: assets/templates/assets/asset_group_detail.html:172 -#: assets/templates/assets/asset_group_list.html:42 -#: assets/templates/assets/asset_list.html:95 -#: assets/templates/assets/cluster_assets.html:170 -#: assets/templates/assets/cluster_list.html:43 -#: assets/templates/assets/system_user_list.html:84 -#: perms/templates/perms/asset_permission_list.html:73 -#: terminal/templates/terminal/terminal_list.html:71 -#: users/templates/users/user_group_list.html:39 -#: users/templates/users/user_list.html:76 -msgid "Update" -msgstr "更新" - -#: assets/templates/assets/admin_user_list.html:84 -#: assets/templates/assets/asset_group_list.html:43 -#: assets/templates/assets/asset_list.html:96 -#: assets/templates/assets/cluster_list.html:44 -#: assets/templates/assets/system_user_list.html:85 -#: ops/templates/ops/task_list.html:70 -#: perms/templates/perms/asset_permission_list.html:74 -#: terminal/templates/terminal/terminal_list.html:73 -#: users/templates/users/user_group_list.html:41 -#: users/templates/users/user_list.html:80 -#: users/templates/users/user_list.html:84 -msgid "Delete" -msgstr "删除" - #: assets/templates/assets/asset_create.html:28 #: assets/templates/assets/asset_update.html:33 msgid "Group" msgstr "组" -#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:180 -#: assets/views/cluster.py:98 +#: assets/templates/assets/asset_detail.html:20 assets/views/asset.py:186 +#: assets/views/cluster.py:103 msgid "Asset detail" msgstr "资产详情" @@ -881,7 +908,7 @@ msgid "Quick modify" msgstr "快速修改" #: assets/templates/assets/asset_detail.html:175 -#: assets/templates/assets/asset_list.html:37 +#: assets/templates/assets/asset_list.html:35 #: assets/templates/assets/user_asset_list.html:25 perms/models.py:20 #: perms/templates/perms/asset_permission_create_update.html:47 #: perms/templates/perms/asset_permission_detail.html:116 @@ -914,17 +941,17 @@ msgstr "更新成功" msgid "Group assets" msgstr "组下资产" -#: assets/templates/assets/asset_group_detail.html:49 +#: assets/templates/assets/asset_group_detail.html:54 #: assets/templates/assets/cluster_assets.html:55 #: terminal/templates/terminal/terminal_list.html:35 msgid "Alive" msgstr "在线" -#: assets/templates/assets/asset_group_detail.html:62 +#: assets/templates/assets/asset_group_detail.html:67 msgid "Add assets to this group" msgstr "添加资产到该组" -#: assets/templates/assets/asset_group_detail.html:79 +#: assets/templates/assets/asset_group_detail.html:84 #: perms/templates/perms/asset_permission_asset.html:97 #: perms/templates/perms/asset_permission_detail.html:153 #: perms/templates/perms/asset_permission_user.html:97 @@ -933,49 +960,49 @@ msgstr "添加资产到该组" msgid "Add" msgstr "添加" -#: assets/templates/assets/asset_group_detail.html:173 +#: assets/templates/assets/asset_group_detail.html:178 msgid "Remove" msgstr "移除" -#: assets/templates/assets/asset_group_list.html:7 assets/views/group.py:31 -#: assets/views/group.py:94 +#: assets/templates/assets/asset_group_list.html:7 assets/views/group.py:35 +#: assets/views/group.py:88 msgid "Create asset group" msgstr "创建资产组" -#: assets/templates/assets/asset_group_list.html:80 -#: assets/templates/assets/asset_list.html:197 +#: assets/templates/assets/asset_group_list.html:76 +#: assets/templates/assets/asset_list.html:215 #: assets/templates/assets/cluster_list.html:84 #: assets/templates/assets/system_user_list.html:129 #: users/templates/users/user_detail.html:333 #: users/templates/users/user_detail.html:358 #: users/templates/users/user_group_list.html:77 -#: users/templates/users/user_list.html:179 +#: users/templates/users/user_list.html:191 msgid "Are you sure?" msgstr "你确认吗?" -#: assets/templates/assets/asset_group_list.html:81 +#: assets/templates/assets/asset_group_list.html:77 #: users/templates/users/user_group_list.html:78 msgid "This will delete the selected groups !!!" msgstr "删除选择组" -#: assets/templates/assets/asset_group_list.html:89 +#: assets/templates/assets/asset_group_list.html:85 msgid "Group deleted" msgstr "组已被删除" -#: assets/templates/assets/asset_group_list.html:90 -#: assets/templates/assets/asset_group_list.html:95 +#: assets/templates/assets/asset_group_list.html:86 +#: assets/templates/assets/asset_group_list.html:91 msgid "Group Delete" msgstr "删除" -#: assets/templates/assets/asset_group_list.html:94 +#: assets/templates/assets/asset_group_list.html:90 msgid "Group deleting failed." msgstr "删除失败" -#: assets/templates/assets/asset_group_list.html:157 +#: assets/templates/assets/asset_group_list.html:153 msgid "The selected asset groups has been updated successfully." msgstr "更新成功" -#: assets/templates/assets/asset_group_list.html:158 +#: assets/templates/assets/asset_group_list.html:154 msgid "AssetGroup Updated" msgstr "资产组更新" @@ -993,52 +1020,47 @@ msgstr "导出" msgid "Create asset" msgstr "创建资产" -#: assets/templates/assets/asset_list.html:35 -#: assets/templates/assets/user_asset_list.html:23 -msgid "Env" -msgstr "环境" - -#: assets/templates/assets/asset_list.html:36 +#: assets/templates/assets/asset_list.html:34 #: assets/templates/assets/user_asset_list.html:24 msgid "Hardware" msgstr "硬件" -#: assets/templates/assets/asset_list.html:48 +#: assets/templates/assets/asset_list.html:46 #: users/templates/users/user_list.html:37 msgid "Delete selected" msgstr "批量删除" -#: assets/templates/assets/asset_list.html:49 +#: assets/templates/assets/asset_list.html:47 #: users/templates/users/user_list.html:38 msgid "Update selected" msgstr "批量更新" -#: assets/templates/assets/asset_list.html:50 +#: assets/templates/assets/asset_list.html:48 #: users/templates/users/user_list.html:39 msgid "Deactive selected" msgstr "禁用所选" -#: assets/templates/assets/asset_list.html:51 +#: assets/templates/assets/asset_list.html:49 #: users/templates/users/user_list.html:40 msgid "Active selected" msgstr "激活所选" -#: assets/templates/assets/asset_list.html:198 +#: assets/templates/assets/asset_list.html:216 msgid "This will delete the selected assets !!!" msgstr "删除选择资产" # msgid "Deleted!" # msgstr "删除" -#: assets/templates/assets/asset_list.html:206 +#: assets/templates/assets/asset_list.html:224 msgid "Asset Deleted." msgstr "已被删除" -#: assets/templates/assets/asset_list.html:207 -#: assets/templates/assets/asset_list.html:212 +#: assets/templates/assets/asset_list.html:225 +#: assets/templates/assets/asset_list.html:230 msgid "Asset Delete" msgstr "删除" -#: assets/templates/assets/asset_list.html:211 +#: assets/templates/assets/asset_list.html:229 msgid "Asset Deleting failed." msgstr "删除失败" @@ -1062,6 +1084,7 @@ msgid "Test assets connective" msgstr "测试资产可连接性" #: assets/templates/assets/cluster_assets.html:77 +#: ops/templates/ops/task_list.html:70 msgid "Run" msgstr "执行" @@ -1079,13 +1102,14 @@ msgid "Task has been send, seen left assets status" msgstr "任务已下发,查看左侧资产状态" #: assets/templates/assets/cluster_create_update.html:41 +#: users/templates/users/reset_password.html:57 #: users/templates/users/user_profile.html:20 -msgid "Settings" +msgid "Setting" msgstr "设置" -#: assets/templates/assets/cluster_list.html:11 assets/views/cluster.py:39 -msgid "Create Cluster" -msgstr "创建Cluster" +#: assets/templates/assets/cluster_list.html:11 assets/views/cluster.py:43 +msgid "Create cluster" +msgstr "创建集群" #: assets/templates/assets/cluster_list.html:21 #: users/templates/users/_select_user_modal.html:17 @@ -1139,19 +1163,19 @@ msgstr "任务已下发,查看ops任务列表" msgid "Attached assets" msgstr "关联的资产" -#: assets/templates/assets/system_user_detail.html:76 +#: assets/templates/assets/system_user_detail.html:81 msgid "Home" msgstr "家目录" -#: assets/templates/assets/system_user_detail.html:82 +#: assets/templates/assets/system_user_detail.html:87 msgid "Uid" msgstr "Uid" -#: assets/templates/assets/system_user_detail.html:142 +#: assets/templates/assets/system_user_detail.html:147 msgid "Clusters" msgstr "集群" -#: assets/templates/assets/system_user_detail.html:150 +#: assets/templates/assets/system_user_detail.html:155 msgid "Add to cluster" msgstr "添加到集群" @@ -1172,93 +1196,177 @@ msgstr "删除系统用户" msgid "System Users Deleting failed." msgstr "系统用户删除失败" +#: assets/templates/assets/user_asset_list.html:23 +msgid "Env" +msgstr "环境" + #: assets/templates/assets/user_asset_list.html:26 msgid "Connective" msgstr "连接性" -#: assets/templates/assets/user_asset_list.html:65 -msgid "Connect" -msgstr "连接" - -#: assets/views/admin_user.py:28 +#: assets/views/admin_user.py:30 msgid "Admin user list" msgstr "管理用户列表" -#: assets/views/admin_user.py:52 -#, python-brace-format -msgid "Create admin user {name} successfully." -msgstr "创建管理用户 {name} 成功" - -#: assets/views/admin_user.py:68 +#: assets/views/admin_user.py:64 msgid "Update admin user" msgstr "更新管理用户" -#: assets/views/admin_user.py:89 assets/views/admin_user.py:116 +#: assets/views/admin_user.py:80 assets/views/admin_user.py:107 msgid "Admin user detail" msgstr "管理用户详情" -#: assets/views/asset.py:48 assets/views/asset.py:62 +#: assets/views/asset.py:49 assets/views/asset.py:62 msgid "Asset list" msgstr "资产列表" -#: assets/views/asset.py:142 +#: assets/views/asset.py:145 msgid "Bulk update asset" msgstr "批量更新资产" -#: assets/views/asset.py:159 +#: assets/views/asset.py:162 msgid "Update asset" msgstr "编辑资产" -#: assets/views/asset.py:292 +#: assets/views/asset.py:298 msgid "already exists" msgstr "已经存在" -#: assets/views/cluster.py:23 +#: assets/views/cluster.py:27 msgid "Cluster list" msgstr "集群列表" -#: assets/views/cluster.py:38 assets/views/cluster.py:65 -#: assets/views/system_user.py:112 +#: assets/views/cluster.py:42 assets/views/cluster.py:70 +#: assets/views/system_user.py:96 msgid "assets" msgstr "资产管理" -#: assets/views/cluster.py:66 +#: assets/views/cluster.py:71 msgid "Update Cluster" msgstr "更新Cluster" -#: assets/views/cluster.py:81 +#: assets/views/cluster.py:86 msgid "Cluster detail" msgstr "集群详情" -#: assets/views/group.py:54 +#: assets/views/group.py:53 msgid "Asset group list" msgstr "资产组列表" -#: assets/views/group.py:72 +#: assets/views/group.py:70 msgid "Asset group detail" msgstr "资产组详情" -#: assets/views/system_user.py:30 +#: assets/views/system_user.py:29 msgid "System user list" msgstr "系统用户列表" -#: assets/views/system_user.py:57 -#, python-brace-format -msgid "Create system user {name} successfully." -msgstr "创建系统用户 {name} 成功" - -#: assets/views/system_user.py:72 +#: assets/views/system_user.py:61 msgid "Update system user" msgstr "更新系统用户" -#: assets/views/system_user.py:92 +#: assets/views/system_user.py:76 msgid "System user detail" msgstr "系统用户详情" -#: assets/views/system_user.py:113 +#: assets/views/system_user.py:97 msgid "System user asset" msgstr "系统用户集群资产" +#: common/api.py:19 +msgid "Test mail sent to {}, please check" +msgstr "邮件已经发送{}, 请检查" + +#: common/api.py:53 +msgid "Test ldap success" +msgstr "连接LDAP成功" + +#: common/const.py:6 +#, python-format +msgid "%(name)s was created successfully" +msgstr "%(name)s 创建成功" + +#: common/const.py:7 +#, python-format +msgid "%(name)s was updated successfully" +msgstr "%(name)s 更新成功" + +#: common/forms.py:64 +msgid "Current SITE URL" +msgstr "当前站点URL" + +#: common/forms.py:68 +msgid "User Guide URL" +msgstr "用户向导URL" + +#: common/forms.py:69 +msgid "User first login update profile done redirect to it" +msgstr "用户第一次登录,修改profile后重定向到地址" + +#: common/forms.py:72 +msgid "Email Subject Prefix" +msgstr "Email主题前缀" + +#: common/forms.py:79 +msgid "SMTP host" +msgstr "SMTP主机" + +#: common/forms.py:81 +msgid "SMTP port" +msgstr "SMTP端口" + +#: common/forms.py:83 +msgid "SMTP user" +msgstr "SMTP账号" + +#: common/forms.py:86 +msgid "SMTP password" +msgstr "SMTP密码" + +#: common/forms.py:87 +msgid "Some provider use token except password" +msgstr "一些邮件提供商需要输入的是Token" + +#: common/forms.py:90 common/forms.py:127 +msgid "Use SSL" +msgstr "使用SSL" + +#: common/forms.py:91 +msgid "If SMTP port is 465, may be select" +msgstr "如果SMTP端口是465,通常需要启用SSL" + +#: common/forms.py:94 +msgid "Use TLS" +msgstr "使用TLS" + +#: common/forms.py:95 +msgid "If SMTP port is 587, may be select" +msgstr "如果SMTP端口是587,通常需要启用TLS" + +#: common/forms.py:101 +msgid "LDAP server" +msgstr "LDAP地址" + +#: common/forms.py:104 +msgid "Bind DN" +msgstr "绑定DN" + +#: common/forms.py:111 +msgid "User OU" +msgstr "用户OU" + +#: common/forms.py:114 +msgid "User search filter" +msgstr "用户过滤器" + +#: common/forms.py:117 +msgid "User attr map" +msgstr "LDAP属性映射" + +#: common/forms.py:130 +msgid "Enable LDAP Auth" +msgstr "开启LDAP认证" + #: common/mixins.py:29 msgid "is discard" msgstr "" @@ -1267,6 +1375,46 @@ msgstr "" msgid "discard time" msgstr "" +#: common/models.py:26 +msgid "Value" +msgstr "值" + +#: common/models.py:27 +msgid "Enabled" +msgstr "启用" + +#: common/templates/common/basic_setting.html:15 +#: common/templates/common/email_setting.html:15 +#: common/templates/common/ldap_setting.html:15 common/views.py:18 +msgid "Basic setting" +msgstr "基本设置" + +#: common/templates/common/basic_setting.html:18 +#: common/templates/common/email_setting.html:18 +#: common/templates/common/ldap_setting.html:18 common/views.py:44 +msgid "Email setting" +msgstr "邮件设置" + +#: common/templates/common/basic_setting.html:21 +#: common/templates/common/email_setting.html:21 +#: common/templates/common/ldap_setting.html:21 common/views.py:70 +msgid "LDAP setting" +msgstr "LDAP设置" + +#: common/templates/common/email_setting.html:55 +#: common/templates/common/ldap_setting.html:55 +msgid "Test connection" +msgstr "测试连接" + +#: common/views.py:17 common/views.py:43 common/views.py:69 +#: templates/_nav.html:69 +msgid "Settings" +msgstr "系统设置" + +#: common/views.py:28 common/views.py:54 common/views.py:82 +msgid "Update setting successfully, please restart program" +msgstr "更新设置成功, 请手动重启程序" + #: ops/models.py:32 msgid "Interval" msgstr "间隔" @@ -1517,6 +1665,10 @@ msgstr "成功" msgid "Date" msgstr "日期" +#: ops/templates/ops/task_list.html:125 +msgid "Task start: " +msgstr "任务开始: " + #: ops/views.py:36 ops/views.py:52 ops/views.py:65 ops/views.py:78 #: ops/views.py:91 ops/views.py:104 ops/views.py:117 msgid "Ops" @@ -1530,8 +1682,8 @@ msgstr "任务列表" msgid "Task run history" msgstr "执行历史" -#: perms/forms.py:16 users/forms.py:144 users/forms.py:149 users/forms.py:161 -#: users/forms.py:191 +#: perms/forms.py:16 users/forms.py:147 users/forms.py:152 users/forms.py:164 +#: users/forms.py:195 msgid "Select users" msgstr "选择用户" @@ -1542,9 +1694,9 @@ msgstr "选择用户" #: terminal/templates/terminal/command_list.html:32 #: terminal/templates/terminal/command_list.html:72 #: terminal/templates/terminal/session_list.html:33 -#: terminal/templates/terminal/session_list.html:71 users/forms.py:187 -#: users/models/user.py:31 users/templates/users/user_group_detail.html:78 -#: users/views/user.py:348 +#: terminal/templates/terminal/session_list.html:71 users/forms.py:191 +#: users/models/user.py:30 users/templates/users/user_group_detail.html:78 +#: users/views/user.py:337 msgid "User" msgstr "用户" @@ -1570,7 +1722,7 @@ msgid "" msgstr "资产 {}(组 {}) 所在集群 {} 不包含系统用户 [{}] 请检查\n" #: perms/models.py:16 perms/templates/perms/asset_permission_list.html:27 -#: templates/_nav.html:13 users/models/user.py:38 +#: templates/_nav.html:13 users/models/user.py:37 #: users/templates/users/_select_user_modal.html:16 #: users/templates/users/user_detail.html:178 #: users/templates/users/user_list.html:26 @@ -1578,7 +1730,7 @@ msgid "User group" msgstr "用户组" #: perms/models.py:21 perms/templates/perms/asset_permission_detail.html:86 -#: users/models/user.py:50 users/templates/users/user_detail.html:94 +#: users/models/user.py:49 users/templates/users/user_detail.html:94 #: users/templates/users/user_profile.html:96 msgid "Date expired" msgstr "失效日期" @@ -1658,47 +1810,32 @@ msgstr "选择用户" msgid "Add user group to asset permission" msgstr "添加用户组" -#: perms/views.py:27 perms/views.py:77 perms/views.py:103 perms/views.py:128 -#: perms/views.py:165 perms/views.py:195 templates/_nav.html:30 +#: perms/views.py:28 perms/views.py:44 perms/views.py:60 perms/views.py:74 +#: perms/views.py:111 perms/views.py:141 templates/_nav.html:30 msgid "Perms" msgstr "权限管理" -#: perms/views.py:28 +#: perms/views.py:29 msgid "Asset permission list" msgstr "资产授权列表" -#: perms/views.py:63 -#, python-brace-format -msgid "Create asset permission {name} successfully." -msgstr "创建授权 {name} 成功" - -#: perms/views.py:78 +#: perms/views.py:45 msgid "Create asset permission" msgstr "创建权限规则" -#: perms/views.py:89 -#, python-brace-format -msgid "Create asset permission {name} success." -msgstr "创建授权 {name} 成功" - -#: perms/views.py:104 +#: perms/views.py:61 msgid "Update asset permission" msgstr "更新资产授权" -#: perms/views.py:115 -#, python-brace-format -msgid "Update asset permission {name} success." -msgstr "更新授权 {name} 成功" - -#: perms/views.py:129 +#: perms/views.py:75 msgid "Asset permission detail" msgstr "资产授权详情" -#: perms/views.py:166 +#: perms/views.py:112 msgid "Asset permission user list" msgstr "资产授权包含用户" -#: perms/views.py:196 +#: perms/views.py:142 msgid "Asset permission asset list" msgstr "资产组授权包含资产" @@ -1716,32 +1853,28 @@ msgstr "帮助" #: users/templates/users/user_profile.html:17 #: users/templates/users/user_profile_update.html:37 #: users/templates/users/user_profile_update.html:57 -#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:323 +#: users/templates/users/user_pubkey_update.html:37 users/views/user.py:319 msgid "Profile" msgstr "个人信息" -#: templates/_header_bar.html:34 -msgid "Profile settings" -msgstr "个人信息设置" - -#: templates/_header_bar.html:38 +#: templates/_header_bar.html:37 msgid "Admin page" msgstr "管理页面" -#: templates/_header_bar.html:40 +#: templates/_header_bar.html:39 msgid "User page" msgstr "用户页面" -#: templates/_header_bar.html:43 +#: templates/_header_bar.html:42 msgid "Logout" msgstr "注销登录" -#: templates/_header_bar.html:47 users/templates/users/login.html:42 +#: templates/_header_bar.html:46 users/templates/users/login.html:42 #: users/templates/users/login.html:61 msgid "Login" msgstr "登录" -#: templates/_header_bar.html:60 templates/_nav.html:4 +#: templates/_header_bar.html:59 templates/_nav.html:4 msgid "Dashboard" msgstr "仪表盘" @@ -1775,11 +1908,11 @@ msgstr "" msgid "Close" msgstr "关闭" -#: templates/_nav.html:9 users/views/group.py:30 users/views/group.py:48 -#: users/views/group.py:74 users/views/group.py:91 users/views/login.py:193 -#: users/views/login.py:242 users/views/user.py:55 users/views/user.py:70 -#: users/views/user.py:95 users/views/user.py:151 users/views/user.py:308 -#: users/views/user.py:322 users/views/user.py:366 users/views/user.py:388 +#: templates/_nav.html:9 users/views/group.py:28 users/views/group.py:44 +#: users/views/group.py:62 users/views/group.py:79 users/views/login.py:197 +#: users/views/login.py:246 users/views/user.py:57 users/views/user.py:72 +#: users/views/user.py:91 users/views/user.py:147 users/views/user.py:304 +#: users/views/user.py:318 users/views/user.py:355 users/views/user.py:377 msgid "Users" msgstr "用户管理" @@ -1806,7 +1939,7 @@ msgstr "任务" #: terminal/views/terminal.py:31 terminal/views/terminal.py:46 #: terminal/views/terminal.py:58 msgid "Terminal" -msgstr "终端" +msgstr "终端管理" #: templates/_nav.html:51 msgid "Session online" @@ -1828,6 +1961,10 @@ msgstr "命令" msgid "My assets" msgstr "我的资产" +#: templates/_nav_user.html:14 +msgid "Web terminal" +msgstr "Web终端" + #: templates/captcha/image.html:3 msgid "Play CAPTCHA as audio file" msgstr "语言播放验证码" @@ -2084,47 +2221,47 @@ msgstr "" msgid "Invalid token or cache refreshed." msgstr "" -#: users/forms.py:42 users/templates/users/user_detail.html:186 +#: users/forms.py:43 users/templates/users/user_detail.html:186 msgid "Join user groups" msgstr "添加到用户组" -#: users/forms.py:71 +#: users/forms.py:74 msgid "Old password" msgstr "原来密码" -#: users/forms.py:76 +#: users/forms.py:79 msgid "New password" msgstr "新密码" -#: users/forms.py:81 +#: users/forms.py:84 msgid "Confirm password" msgstr "确认密码" -#: users/forms.py:91 +#: users/forms.py:94 msgid "Old password error" msgstr "原来密码错误" -#: users/forms.py:99 +#: users/forms.py:102 msgid "Password does not match" msgstr "密码不一致" -#: users/forms.py:111 +#: users/forms.py:114 msgid "ssh public key" msgstr "ssh公钥" -#: users/forms.py:112 +#: users/forms.py:115 msgid "ssh-rsa AAAA..." msgstr "" -#: users/forms.py:113 +#: users/forms.py:116 msgid "Paste your id_rsa.pub here." msgstr "复制你的公钥到这里" -#: users/forms.py:126 +#: users/forms.py:129 msgid "Public key should not be the same as your old one." msgstr "不能和原来的密钥相同" -#: users/forms.py:130 users/serializers.py:42 +#: users/forms.py:133 users/serializers.py:42 msgid "Not a valid ssh public key" msgstr "ssh密钥不合法" @@ -2152,46 +2289,46 @@ msgstr "Agent" msgid "Date login" msgstr "登录日期" -#: users/models/user.py:30 users/models/user.py:255 +#: users/models/user.py:29 users/models/user.py:254 msgid "Administrator" msgstr "管理员" -#: users/models/user.py:32 +#: users/models/user.py:31 msgid "Application" msgstr "应用程序" -#: users/models/user.py:37 users/templates/users/user_detail.html:70 +#: users/models/user.py:36 users/templates/users/user_detail.html:70 #: users/templates/users/user_profile.html:59 msgid "Email" msgstr "邮件" -#: users/models/user.py:39 users/templates/users/_select_user_modal.html:15 +#: users/models/user.py:38 users/templates/users/_select_user_modal.html:15 #: users/templates/users/user_detail.html:86 #: users/templates/users/user_list.html:25 #: users/templates/users/user_profile.html:55 msgid "Role" msgstr "角色" -#: users/models/user.py:40 +#: users/models/user.py:39 msgid "Avatar" msgstr "头像" -#: users/models/user.py:41 users/templates/users/user_detail.html:81 +#: users/models/user.py:40 users/templates/users/user_detail.html:81 msgid "Wechat" msgstr "微信" -#: users/models/user.py:43 +#: users/models/user.py:42 msgid "Enable OTP" msgstr "二次验证" -#: users/models/user.py:47 users/templates/users/user_password_update.html:43 +#: users/models/user.py:46 users/templates/users/user_password_update.html:43 #: users/templates/users/user_profile.html:71 #: users/templates/users/user_profile_update.html:43 #: users/templates/users/user_pubkey_update.html:43 msgid "Public key" msgstr "ssh公钥" -#: users/models/user.py:258 +#: users/models/user.py:257 msgid "Administrator is the super user of system" msgstr "Administrator是初始的超级管理员" @@ -2288,12 +2425,8 @@ msgstr "重置密码" msgid "Password again" msgstr "再次输入密码" -#: users/templates/users/reset_password.html:57 -msgid "Setting" -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:72 msgid "Create user" msgstr "创建用户" @@ -2304,7 +2437,7 @@ msgstr "生成重置密码连接,通过邮件发送给用户" #: users/templates/users/user_detail.html:19 #: users/templates/users/user_granted_asset.html:18 #: users/templates/users/user_group_granted_asset.html:18 -#: users/views/user.py:152 +#: users/views/user.py:148 msgid "User detail" msgstr "用户详情" @@ -2379,11 +2512,11 @@ msgstr "授权资产" msgid "Asset groups granted of " msgstr "授权资产组" -#: users/templates/users/user_group_create_update.html:45 +#: users/templates/users/user_group_create_update.html:31 msgid "Cancel" msgstr "取消" -#: users/templates/users/user_group_detail.html:22 users/views/group.py:92 +#: users/templates/users/user_group_detail.html:22 users/views/group.py:80 msgid "User group detail" msgstr "资产组详情" @@ -2395,7 +2528,7 @@ msgstr "添加用户" msgid "Valid" msgstr "可用" -#: users/templates/users/user_group_list.html:5 users/views/group.py:49 +#: users/templates/users/user_group_list.html:5 users/views/group.py:45 msgid "Create user group" msgstr "创建用户组" @@ -2412,20 +2545,20 @@ msgstr "用户组删除" msgid "UserGroup Deleting failed." msgstr "用户组删除失败" -#: users/templates/users/user_list.html:180 +#: users/templates/users/user_list.html:192 msgid "This will delete the selected users !!!" msgstr "删除选中用户 !!!" -#: users/templates/users/user_list.html:188 +#: users/templates/users/user_list.html:200 msgid "User Deleted." msgstr "已被删除" -#: users/templates/users/user_list.html:189 -#: users/templates/users/user_list.html:194 +#: users/templates/users/user_list.html:201 +#: users/templates/users/user_list.html:206 msgid "User Delete" msgstr "删除" -#: users/templates/users/user_list.html:193 +#: users/templates/users/user_list.html:205 msgid "User Deleting failed." msgstr "用户删除失败" @@ -2433,8 +2566,8 @@ msgstr "用户删除失败" msgid "OTP" msgstr "" -#: users/templates/users/user_profile.html:100 users/views/user.py:181 -#: users/views/user.py:233 +#: users/templates/users/user_profile.html:100 users/views/user.py:177 +#: users/views/user.py:229 msgid "User groups" msgstr "用户组" @@ -2458,7 +2591,7 @@ msgstr "指纹" msgid "Update public key" msgstr "更新密钥" -#: users/templates/users/user_update.html:4 users/views/user.py:95 +#: users/templates/users/user_update.html:4 users/views/user.py:91 msgid "Update user" msgstr "编辑用户" @@ -2592,17 +2725,11 @@ msgstr "禁用或失效" msgid "Password or SSH public key invalid" msgstr "密码或秘钥不合法" -#: users/views/group.py:31 +#: users/views/group.py:29 msgid "User group list" msgstr "用户组列表" -#: users/views/group.py:43 -#, fuzzy, python-brace-format -#| msgid "Create user {name} successfully." -msgid "User group {name} was created successfully" -msgstr "创建用户 {name} 成功" - -#: users/views/group.py:75 +#: users/views/group.py:63 msgid "Update user group" msgstr "编辑用户组" @@ -2610,82 +2737,78 @@ msgstr "编辑用户组" msgid "Please enable cookies and try again." msgstr "设置你的浏览器支持cookie" -#: users/views/login.py:83 +#: users/views/login.py:87 msgid "Logout success" msgstr "退出登录成功" -#: users/views/login.py:84 +#: users/views/login.py:88 msgid "Logout success, return login page" msgstr "退出登录成功,返回到登录页面" -#: users/views/login.py:100 +#: users/views/login.py:104 msgid "Email address invalid, please input again" msgstr "邮箱地址错误,重新输入" -#: users/views/login.py:113 +#: users/views/login.py:117 msgid "Send reset password message" msgstr "发送重置密码邮件" -#: users/views/login.py:114 +#: users/views/login.py:118 msgid "Send reset password mail success, login your mail box and follow it " msgstr "" "发送重置邮件成功, 请登录邮箱查看, 按照提示操作 (如果没收到,请等待3-5分钟)" -#: users/views/login.py:128 +#: users/views/login.py:132 msgid "Reset password success" msgstr "重置密码成功" -#: users/views/login.py:129 +#: users/views/login.py:133 msgid "Reset password success, return to login page" msgstr "重置密码成功,返回到登录页面" -#: users/views/login.py:146 users/views/login.py:159 +#: users/views/login.py:150 users/views/login.py:163 msgid "Token invalid or expired" msgstr "Token错误或失效" -#: users/views/login.py:155 +#: users/views/login.py:159 msgid "Password not same" msgstr "密码不一致" -#: users/views/login.py:193 +#: users/views/login.py:197 msgid "First login" msgstr "首次登陆" -#: users/views/login.py:243 +#: users/views/login.py:247 msgid "Login log list" msgstr "登录日志" -#: users/views/user.py:56 +#: users/views/user.py:58 msgid "User list" msgstr "用户列表" -#: users/views/user.py:66 users/views/user.py:335 -#, python-brace-format -msgid "Create user {name} successfully." -msgstr "创建用户 {name} 成功" - -#: users/views/user.py:105 +#: users/views/user.py:101 msgid "Bulk update user success" msgstr "批量更新用户成功" -#: users/views/user.py:210 +#: users/views/user.py:206 msgid "Invalid file." msgstr "文件不合法" -#: users/views/user.py:309 +#: users/views/user.py:305 msgid "User granted assets" msgstr "用户授权资产" -#: users/views/user.py:349 +#: users/views/user.py:338 msgid "Profile setting" msgstr "个人信息设置" -#: users/views/user.py:367 +#: users/views/user.py:356 msgid "Password update" msgstr "密码更新" -#: users/views/user.py:389 +#: users/views/user.py:378 msgid "Public key update" msgstr "秘钥更新" - +#~ msgid "Connect" +#~ msgstr "连接" diff --git a/apps/ops/models.py b/apps/ops/models.py index accdfe78f..471df25d7 100644 --- a/apps/ops/models.py +++ b/apps/ops/models.py @@ -165,7 +165,7 @@ class AdHoc(models.Model): if item and isinstance(item, list): self._tasks = json.dumps(item) else: - raise SyntaxError('Tasks should be a list') + raise SyntaxError('Tasks should be a list: {}'.format(item)) @property def hosts(self): @@ -218,8 +218,8 @@ class AdHoc(models.Model): history.result = raw history.summary = summary return raw, summary - except: - return {}, {} + except Exception as e: + return {}, {"dark": {"all": str(e)}, "contacted": []} finally: history.date_finished = timezone.now() history.timedelta = time.time() - time_start diff --git a/apps/ops/serializers.py b/apps/ops/serializers.py index 86a029c12..a395a427e 100644 --- a/apps/ops/serializers.py +++ b/apps/ops/serializers.py @@ -43,8 +43,8 @@ class AdHocRunHistorySerializer(serializers.ModelSerializer): def get_stat(obj): return { "total": len(obj.adhoc.hosts), - "success": len(obj.summary["contacted"]), - "failed": len(obj.summary["dark"]), + "success": len(obj.summary.get("contacted", [])), + "failed": len(obj.summary.get("dark", [])), } def get_field_names(self, declared_fields, info): diff --git a/apps/ops/utils.py b/apps/ops/utils.py index 0a9dce8c9..8ff9c321f 100644 --- a/apps/ops/utils.py +++ b/apps/ops/utils.py @@ -16,6 +16,8 @@ def update_or_create_ansible_task( run_as_admin=False, run_as="", become_info=None, created_by=None, ): + if not hosts or not tasks or not task_name: + return defaults = { 'name': task_name, diff --git a/apps/ops/views.py b/apps/ops/views.py index 4b090a8a3..ba9e2cfeb 100644 --- a/apps/ops/views.py +++ b/apps/ops/views.py @@ -10,7 +10,7 @@ from .hands import AdminUserRequiredMixin class TaskListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE model = Task ordering = ('-date_created',) context_object_name = 'task_list' diff --git a/apps/perms/api.py b/apps/perms/api.py index ee2d12ff8..9165a9098 100644 --- a/apps/perms/api.py +++ b/apps/perms/api.py @@ -351,7 +351,7 @@ class UserGroupGrantedAssetGroupsApi(ListAPIView): class ValidateUserAssetPermissionView(APIView): - permission_classes = (IsAppUser,) + permission_classes = (IsSuperUserOrAppUser,) @staticmethod def get(request): diff --git a/apps/perms/templates/perms/asset_permission_detail.html b/apps/perms/templates/perms/asset_permission_detail.html index 942013852..5540ca515 100644 --- a/apps/perms/templates/perms/asset_permission_detail.html +++ b/apps/perms/templates/perms/asset_permission_detail.html @@ -237,6 +237,16 @@ $(document).ready(function () { }).get(); updateSystemUser(system_users); $tr.remove() +}).on('click', '#is_active', function () { + var the_url = '{% url "api-perms:asset-permission-detail" pk=asset_permission.id %}'; + var checked = $(this).prop('checked'); + var body = { + 'is_active': checked + }; + APIUpdateAttr({ + url: the_url, + body: JSON.stringify(body), + }); }) {% endblock %} diff --git a/apps/perms/views.py b/apps/perms/views.py index d3759d064..39a901557 100644 --- a/apps/perms/views.py +++ b/apps/perms/views.py @@ -11,6 +11,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.views.generic.detail import DetailView, SingleObjectMixin from django.contrib import messages +from common.const import create_success_msg, update_success_msg from .hands import AdminUserRequiredMixin, User, UserGroup, SystemUser, \ Asset, AssetGroup from .models import AssetPermission @@ -31,46 +32,12 @@ class AssetPermissionListView(AdminUserRequiredMixin, ListView): return super().get_context_data(**kwargs) -class MessageMixin: - def form_valid(self, form): - response = super().form_valid(form) - errors = self.object.check_system_user_in_assets() - if errors: - message = self.get_warning_messages(errors) - messages.warning(self.request, message) - else: - message = self.get_success_message(form.cleaned_data) - messages.success(self.request, message) - - success_message = self.get_success_message(form.cleaned_data) - if success_message: - messages.success(self.request, success_message) - return response - - @staticmethod - def get_warning_messages(errors): - message = "WARNING: System user " \ - "should in behind clusters, so that " \ - "system user cat auto push to the cluster assets:
    " - for system_user, clusters in errors.items(): - message += " >>> {}: {} ".format(system_user.name, ", ".join((cluster.name for cluster in clusters))) - return message - - def get_success_message(self, cleaned_data): - url = reverse_lazy('perms:asset-permission-detail', - kwargs={'pk': self.object.pk}) - success_message = _( - 'Create asset permission {name} ' - 'successfully.'.format(url=url, name=self.object.name)) - return success_message - - class AssetPermissionCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): model = AssetPermission form_class = AssetPermissionForm template_name = 'perms/asset_permission_create_update.html' success_url = reverse_lazy('perms:asset-permission-list') - warning = None + success_message = create_success_msg def get_context_data(self, **kwargs): context = { @@ -80,23 +47,13 @@ class AssetPermissionCreateView(AdminUserRequiredMixin, SuccessMessageMixin, Cre kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - url = reverse_lazy( - 'perms:asset-permission-detail', - kwargs={'pk': self.object.pk} - ) - success_message = _( - 'Create asset permission {name} ' - 'success.'.format(url=url, name=self.object.name) - ) - return success_message - class AssetPermissionUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = AssetPermission form_class = AssetPermissionForm template_name = 'perms/asset_permission_create_update.html' success_url = reverse_lazy("perms:asset-permission-list") + success_message = update_success_msg def get_context_data(self, **kwargs): context = { @@ -106,17 +63,6 @@ class AssetPermissionUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, Upd kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - url = reverse_lazy( - 'perms:asset-permission-detail', - kwargs={'pk': self.object.pk} - ) - success_message = _( - 'Update asset permission {name} ' - 'success.'.format(url=url, name=self.object.name) - ) - return success_message - class AssetPermissionDetailView(AdminUserRequiredMixin, DetailView): template_name = 'perms/asset_permission_detail.html' @@ -147,7 +93,7 @@ class AssetPermissionUserView(AdminUserRequiredMixin, ListView): template_name = 'perms/asset_permission_user.html' context_object_name = 'asset_permission' - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE object = None def get(self, request, *args, **kwargs): @@ -177,7 +123,7 @@ class AssetPermissionAssetView(AdminUserRequiredMixin, ListView): template_name = 'perms/asset_permission_asset.html' context_object_name = 'asset_permission' - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE object = None def get(self, request, *args, **kwargs): diff --git a/apps/static/css/font-awesome.css b/apps/static/css/font-awesome.css deleted file mode 100644 index 4040b3cf8..000000000 --- a/apps/static/css/font-awesome.css +++ /dev/null @@ -1,1672 +0,0 @@ -/*! - * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ -/* FONT PATH - * -------------------------- */ -@font-face { - font-family: 'FontAwesome'; - src: url('../fonts/fontawesome-webfont.eot?v=4.2.0'); - src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg'); - font-weight: normal; - font-style: normal; -} -.fa { - display: inline-block; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -/* makes the font 33% larger relative to the icon container */ -.fa-lg { - font-size: 1.33333333em; - line-height: 0.75em; - vertical-align: -15%; -} -.fa-2x { - font-size: 2em; -} -.fa-3x { - font-size: 3em; -} -.fa-4x { - font-size: 4em; -} -.fa-5x { - font-size: 5em; -} -.fa-fw { - width: 1.28571429em; - text-align: center; -} -.fa-ul { - padding-left: 0; - margin-left: 2.14285714em; - list-style-type: none; -} -.fa-ul > li { - position: relative; -} -.fa-li { - position: absolute; - left: -2.14285714em; - width: 2.14285714em; - top: 0.14285714em; - text-align: center; -} -.fa-li.fa-lg { - left: -1.85714286em; -} -.fa-border { - padding: .2em .25em .15em; - border: solid 0.08em #eeeeee; - border-radius: .1em; -} -.pull-right { - float: right; -} -.pull-left { - float: left; -} -.fa.pull-left { - margin-right: .3em; -} -.fa.pull-right { - margin-left: .3em; -} -.fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -.fa-rotate-90 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); - -webkit-transform: rotate(90deg); - -ms-transform: rotate(90deg); - transform: rotate(90deg); -} -.fa-rotate-180 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); - -webkit-transform: rotate(180deg); - -ms-transform: rotate(180deg); - transform: rotate(180deg); -} -.fa-rotate-270 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); - -webkit-transform: rotate(270deg); - -ms-transform: rotate(270deg); - transform: rotate(270deg); -} -.fa-flip-horizontal { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); - -webkit-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); -} -.fa-flip-vertical { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); - -webkit-transform: scale(1, -1); - -ms-transform: scale(1, -1); - transform: scale(1, -1); -} -:root .fa-rotate-90, -:root .fa-rotate-180, -:root .fa-rotate-270, -:root .fa-flip-horizontal, -:root .fa-flip-vertical { - filter: none; -} -.fa-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.fa-stack-1x, -.fa-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.fa-stack-1x { - line-height: inherit; -} -.fa-stack-2x { - font-size: 2em; -} -.fa-inverse { - color: #ffffff; -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.fa-glass:before { - content: "\f000"; -} -.fa-music:before { - content: "\f001"; -} -.fa-search:before { - content: "\f002"; -} -.fa-envelope-o:before { - content: "\f003"; -} -.fa-heart:before { - content: "\f004"; -} -.fa-star:before { - content: "\f005"; -} -.fa-star-o:before { - content: "\f006"; -} -.fa-user:before { - content: "\f007"; -} -.fa-film:before { - content: "\f008"; -} -.fa-th-large:before { - content: "\f009"; -} -.fa-th:before { - content: "\f00a"; -} -.fa-th-list:before { - content: "\f00b"; -} -.fa-check:before { - content: "\f00c"; -} -.fa-remove:before, -.fa-close:before, -.fa-times:before { - content: "\f00d"; -} -.fa-search-plus:before { - content: "\f00e"; -} -.fa-search-minus:before { - content: "\f010"; -} -.fa-power-off:before { - content: "\f011"; -} -.fa-signal:before { - content: "\f012"; -} -.fa-gear:before, -.fa-cog:before { - content: "\f013"; -} -.fa-trash-o:before { - content: "\f014"; -} -.fa-home:before { - content: "\f015"; -} -.fa-file-o:before { - content: "\f016"; -} -.fa-clock-o:before { - content: "\f017"; -} -.fa-road:before { - content: "\f018"; -} -.fa-download:before { - content: "\f019"; -} -.fa-arrow-circle-o-down:before { - content: "\f01a"; -} -.fa-arrow-circle-o-up:before { - content: "\f01b"; -} -.fa-inbox:before { - content: "\f01c"; -} -.fa-play-circle-o:before { - content: "\f01d"; -} -.fa-rotate-right:before, -.fa-repeat:before { - content: "\f01e"; -} -.fa-refresh:before { - content: "\f021"; -} -.fa-list-alt:before { - content: "\f022"; -} -.fa-lock:before { - content: "\f023"; -} -.fa-flag:before { - content: "\f024"; -} -.fa-headphones:before { - content: "\f025"; -} -.fa-volume-off:before { - content: "\f026"; -} -.fa-volume-down:before { - content: "\f027"; -} -.fa-volume-up:before { - content: "\f028"; -} -.fa-qrcode:before { - content: "\f029"; -} -.fa-barcode:before { - content: "\f02a"; -} -.fa-tag:before { - content: "\f02b"; -} -.fa-tags:before { - content: "\f02c"; -} -.fa-book:before { - content: "\f02d"; -} -.fa-bookmark:before { - content: "\f02e"; -} -.fa-print:before { - content: "\f02f"; -} -.fa-camera:before { - content: "\f030"; -} -.fa-font:before { - content: "\f031"; -} -.fa-bold:before { - content: "\f032"; -} -.fa-italic:before { - content: "\f033"; -} -.fa-text-height:before { - content: "\f034"; -} -.fa-text-width:before { - content: "\f035"; -} -.fa-align-left:before { - content: "\f036"; -} -.fa-align-center:before { - content: "\f037"; -} -.fa-align-right:before { - content: "\f038"; -} -.fa-align-justify:before { - content: "\f039"; -} -.fa-list:before { - content: "\f03a"; -} -.fa-dedent:before, -.fa-outdent:before { - content: "\f03b"; -} -.fa-indent:before { - content: "\f03c"; -} -.fa-video-camera:before { - content: "\f03d"; -} -.fa-photo:before, -.fa-image:before, -.fa-picture-o:before { - content: "\f03e"; -} -.fa-pencil:before { - content: "\f040"; -} -.fa-map-marker:before { - content: "\f041"; -} -.fa-adjust:before { - content: "\f042"; -} -.fa-tint:before { - content: "\f043"; -} -.fa-edit:before, -.fa-pencil-square-o:before { - content: "\f044"; -} -.fa-share-square-o:before { - content: "\f045"; -} -.fa-check-square-o:before { - content: "\f046"; -} -.fa-arrows:before { - content: "\f047"; -} -.fa-step-backward:before { - content: "\f048"; -} -.fa-fast-backward:before { - content: "\f049"; -} -.fa-backward:before { - content: "\f04a"; -} -.fa-play:before { - content: "\f04b"; -} -.fa-pause:before { - content: "\f04c"; -} -.fa-stop:before { - content: "\f04d"; -} -.fa-forward:before { - content: "\f04e"; -} -.fa-fast-forward:before { - content: "\f050"; -} -.fa-step-forward:before { - content: "\f051"; -} -.fa-eject:before { - content: "\f052"; -} -.fa-chevron-left:before { - content: "\f053"; -} -.fa-chevron-right:before { - content: "\f054"; -} -.fa-plus-circle:before { - content: "\f055"; -} -.fa-minus-circle:before { - content: "\f056"; -} -.fa-times-circle:before { - content: "\f057"; -} -.fa-check-circle:before { - content: "\f058"; -} -.fa-question-circle:before { - content: "\f059"; -} -.fa-info-circle:before { - content: "\f05a"; -} -.fa-crosshairs:before { - content: "\f05b"; -} -.fa-times-circle-o:before { - content: "\f05c"; -} -.fa-check-circle-o:before { - content: "\f05d"; -} -.fa-ban:before { - content: "\f05e"; -} -.fa-arrow-left:before { - content: "\f060"; -} -.fa-arrow-right:before { - content: "\f061"; -} -.fa-arrow-up:before { - content: "\f062"; -} -.fa-arrow-down:before { - content: "\f063"; -} -.fa-mail-forward:before, -.fa-share:before { - content: "\f064"; -} -.fa-expand:before { - content: "\f065"; -} -.fa-compress:before { - content: "\f066"; -} -.fa-plus:before { - content: "\f067"; -} -.fa-minus:before { - content: "\f068"; -} -.fa-asterisk:before { - content: "\f069"; -} -.fa-exclamation-circle:before { - content: "\f06a"; -} -.fa-gift:before { - content: "\f06b"; -} -.fa-leaf:before { - content: "\f06c"; -} -.fa-fire:before { - content: "\f06d"; -} -.fa-eye:before { - content: "\f06e"; -} -.fa-eye-slash:before { - content: "\f070"; -} -.fa-warning:before, -.fa-exclamation-triangle:before { - content: "\f071"; -} -.fa-plane:before { - content: "\f072"; -} -.fa-calendar:before { - content: "\f073"; -} -.fa-random:before { - content: "\f074"; -} -.fa-comment:before { - content: "\f075"; -} -.fa-magnet:before { - content: "\f076"; -} -.fa-chevron-up:before { - content: "\f077"; -} -.fa-chevron-down:before { - content: "\f078"; -} -.fa-retweet:before { - content: "\f079"; -} -.fa-shopping-cart:before { - content: "\f07a"; -} -.fa-folder:before { - content: "\f07b"; -} -.fa-folder-open:before { - content: "\f07c"; -} -.fa-arrows-v:before { - content: "\f07d"; -} -.fa-arrows-h:before { - content: "\f07e"; -} -.fa-bar-chart-o:before, -.fa-bar-chart:before { - content: "\f080"; -} -.fa-twitter-square:before { - content: "\f081"; -} -.fa-facebook-square:before { - content: "\f082"; -} -.fa-camera-retro:before { - content: "\f083"; -} -.fa-key:before { - content: "\f084"; -} -.fa-gears:before, -.fa-cogs:before { - content: "\f085"; -} -.fa-comments:before { - content: "\f086"; -} -.fa-thumbs-o-up:before { - content: "\f087"; -} -.fa-thumbs-o-down:before { - content: "\f088"; -} -.fa-star-half:before { - content: "\f089"; -} -.fa-heart-o:before { - content: "\f08a"; -} -.fa-sign-out:before { - content: "\f08b"; -} -.fa-linkedin-square:before { - content: "\f08c"; -} -.fa-thumb-tack:before { - content: "\f08d"; -} -.fa-external-link:before { - content: "\f08e"; -} -.fa-sign-in:before { - content: "\f090"; -} -.fa-trophy:before { - content: "\f091"; -} -.fa-github-square:before { - content: "\f092"; -} -.fa-upload:before { - content: "\f093"; -} -.fa-lemon-o:before { - content: "\f094"; -} -.fa-phone:before { - content: "\f095"; -} -.fa-square-o:before { - content: "\f096"; -} -.fa-bookmark-o:before { - content: "\f097"; -} -.fa-phone-square:before { - content: "\f098"; -} -.fa-twitter:before { - content: "\f099"; -} -.fa-facebook:before { - content: "\f09a"; -} -.fa-github:before { - content: "\f09b"; -} -.fa-unlock:before { - content: "\f09c"; -} -.fa-credit-card:before { - content: "\f09d"; -} -.fa-rss:before { - content: "\f09e"; -} -.fa-hdd-o:before { - content: "\f0a0"; -} -.fa-bullhorn:before { - content: "\f0a1"; -} -.fa-bell:before { - content: "\f0f3"; -} -.fa-certificate:before { - content: "\f0a3"; -} -.fa-hand-o-right:before { - content: "\f0a4"; -} -.fa-hand-o-left:before { - content: "\f0a5"; -} -.fa-hand-o-up:before { - content: "\f0a6"; -} -.fa-hand-o-down:before { - content: "\f0a7"; -} -.fa-arrow-circle-left:before { - content: "\f0a8"; -} -.fa-arrow-circle-right:before { - content: "\f0a9"; -} -.fa-arrow-circle-up:before { - content: "\f0aa"; -} -.fa-arrow-circle-down:before { - content: "\f0ab"; -} -.fa-globe:before { - content: "\f0ac"; -} -.fa-wrench:before { - content: "\f0ad"; -} -.fa-tasks:before { - content: "\f0ae"; -} -.fa-filter:before { - content: "\f0b0"; -} -.fa-briefcase:before { - content: "\f0b1"; -} -.fa-arrows-alt:before { - content: "\f0b2"; -} -.fa-group:before, -.fa-users:before { - content: "\f0c0"; -} -.fa-chain:before, -.fa-link:before { - content: "\f0c1"; -} -.fa-cloud:before { - content: "\f0c2"; -} -.fa-flask:before { - content: "\f0c3"; -} -.fa-cut:before, -.fa-scissors:before { - content: "\f0c4"; -} -.fa-copy:before, -.fa-files-o:before { - content: "\f0c5"; -} -.fa-paperclip:before { - content: "\f0c6"; -} -.fa-save:before, -.fa-floppy-o:before { - content: "\f0c7"; -} -.fa-square:before { - content: "\f0c8"; -} -.fa-navicon:before, -.fa-reorder:before, -.fa-bars:before { - content: "\f0c9"; -} -.fa-list-ul:before { - content: "\f0ca"; -} -.fa-list-ol:before { - content: "\f0cb"; -} -.fa-strikethrough:before { - content: "\f0cc"; -} -.fa-underline:before { - content: "\f0cd"; -} -.fa-table:before { - content: "\f0ce"; -} -.fa-magic:before { - content: "\f0d0"; -} -.fa-truck:before { - content: "\f0d1"; -} -.fa-pinterest:before { - content: "\f0d2"; -} -.fa-pinterest-square:before { - content: "\f0d3"; -} -.fa-google-plus-square:before { - content: "\f0d4"; -} -.fa-google-plus:before { - content: "\f0d5"; -} -.fa-money:before { - content: "\f0d6"; -} -.fa-caret-down:before { - content: "\f0d7"; -} -.fa-caret-up:before { - content: "\f0d8"; -} -.fa-caret-left:before { - content: "\f0d9"; -} -.fa-caret-right:before { - content: "\f0da"; -} -.fa-columns:before { - content: "\f0db"; -} -.fa-unsorted:before, -.fa-sort:before { - content: "\f0dc"; -} -.fa-sort-down:before, -.fa-sort-desc:before { - content: "\f0dd"; -} -.fa-sort-up:before, -.fa-sort-asc:before { - content: "\f0de"; -} -.fa-envelope:before { - content: "\f0e0"; -} -.fa-linkedin:before { - content: "\f0e1"; -} -.fa-rotate-left:before, -.fa-undo:before { - content: "\f0e2"; -} -.fa-legal:before, -.fa-gavel:before { - content: "\f0e3"; -} -.fa-dashboard:before, -.fa-tachometer:before { - content: "\f0e4"; -} -.fa-comment-o:before { - content: "\f0e5"; -} -.fa-comments-o:before { - content: "\f0e6"; -} -.fa-flash:before, -.fa-bolt:before { - content: "\f0e7"; -} -.fa-sitemap:before { - content: "\f0e8"; -} -.fa-umbrella:before { - content: "\f0e9"; -} -.fa-paste:before, -.fa-clipboard:before { - content: "\f0ea"; -} -.fa-lightbulb-o:before { - content: "\f0eb"; -} -.fa-exchange:before { - content: "\f0ec"; -} -.fa-cloud-download:before { - content: "\f0ed"; -} -.fa-cloud-upload:before { - content: "\f0ee"; -} -.fa-user-md:before { - content: "\f0f0"; -} -.fa-stethoscope:before { - content: "\f0f1"; -} -.fa-suitcase:before { - content: "\f0f2"; -} -.fa-bell-o:before { - content: "\f0a2"; -} -.fa-coffee:before { - content: "\f0f4"; -} -.fa-cutlery:before { - content: "\f0f5"; -} -.fa-file-text-o:before { - content: "\f0f6"; -} -.fa-building-o:before { - content: "\f0f7"; -} -.fa-hospital-o:before { - content: "\f0f8"; -} -.fa-ambulance:before { - content: "\f0f9"; -} -.fa-medkit:before { - content: "\f0fa"; -} -.fa-fighter-jet:before { - content: "\f0fb"; -} -.fa-beer:before { - content: "\f0fc"; -} -.fa-h-square:before { - content: "\f0fd"; -} -.fa-plus-square:before { - content: "\f0fe"; -} -.fa-angle-double-left:before { - content: "\f100"; -} -.fa-angle-double-right:before { - content: "\f101"; -} -.fa-angle-double-up:before { - content: "\f102"; -} -.fa-angle-double-down:before { - content: "\f103"; -} -.fa-angle-left:before { - content: "\f104"; -} -.fa-angle-right:before { - content: "\f105"; -} -.fa-angle-up:before { - content: "\f106"; -} -.fa-angle-down:before { - content: "\f107"; -} -.fa-desktop:before { - content: "\f108"; -} -.fa-laptop:before { - content: "\f109"; -} -.fa-tablet:before { - content: "\f10a"; -} -.fa-mobile-phone:before, -.fa-mobile:before { - content: "\f10b"; -} -.fa-circle-o:before { - content: "\f10c"; -} -.fa-quote-left:before { - content: "\f10d"; -} -.fa-quote-right:before { - content: "\f10e"; -} -.fa-spinner:before { - content: "\f110"; -} -.fa-circle:before { - content: "\f111"; -} -.fa-mail-reply:before, -.fa-reply:before { - content: "\f112"; -} -.fa-github-alt:before { - content: "\f113"; -} -.fa-folder-o:before { - content: "\f114"; -} -.fa-folder-open-o:before { - content: "\f115"; -} -.fa-smile-o:before { - content: "\f118"; -} -.fa-frown-o:before { - content: "\f119"; -} -.fa-meh-o:before { - content: "\f11a"; -} -.fa-gamepad:before { - content: "\f11b"; -} -.fa-keyboard-o:before { - content: "\f11c"; -} -.fa-flag-o:before { - content: "\f11d"; -} -.fa-flag-checkered:before { - content: "\f11e"; -} -.fa-terminal:before { - content: "\f120"; -} -.fa-code:before { - content: "\f121"; -} -.fa-mail-reply-all:before, -.fa-reply-all:before { - content: "\f122"; -} -.fa-star-half-empty:before, -.fa-star-half-full:before, -.fa-star-half-o:before { - content: "\f123"; -} -.fa-location-arrow:before { - content: "\f124"; -} -.fa-crop:before { - content: "\f125"; -} -.fa-code-fork:before { - content: "\f126"; -} -.fa-unlink:before, -.fa-chain-broken:before { - content: "\f127"; -} -.fa-question:before { - content: "\f128"; -} -.fa-info:before { - content: "\f129"; -} -.fa-exclamation:before { - content: "\f12a"; -} -.fa-superscript:before { - content: "\f12b"; -} -.fa-subscript:before { - content: "\f12c"; -} -.fa-eraser:before { - content: "\f12d"; -} -.fa-puzzle-piece:before { - content: "\f12e"; -} -.fa-microphone:before { - content: "\f130"; -} -.fa-microphone-slash:before { - content: "\f131"; -} -.fa-shield:before { - content: "\f132"; -} -.fa-calendar-o:before { - content: "\f133"; -} -.fa-fire-extinguisher:before { - content: "\f134"; -} -.fa-rocket:before { - content: "\f135"; -} -.fa-maxcdn:before { - content: "\f136"; -} -.fa-chevron-circle-left:before { - content: "\f137"; -} -.fa-chevron-circle-right:before { - content: "\f138"; -} -.fa-chevron-circle-up:before { - content: "\f139"; -} -.fa-chevron-circle-down:before { - content: "\f13a"; -} -.fa-html5:before { - content: "\f13b"; -} -.fa-css3:before { - content: "\f13c"; -} -.fa-anchor:before { - content: "\f13d"; -} -.fa-unlock-alt:before { - content: "\f13e"; -} -.fa-bullseye:before { - content: "\f140"; -} -.fa-ellipsis-h:before { - content: "\f141"; -} -.fa-ellipsis-v:before { - content: "\f142"; -} -.fa-rss-square:before { - content: "\f143"; -} -.fa-play-circle:before { - content: "\f144"; -} -.fa-ticket:before { - content: "\f145"; -} -.fa-minus-square:before { - content: "\f146"; -} -.fa-minus-square-o:before { - content: "\f147"; -} -.fa-level-up:before { - content: "\f148"; -} -.fa-level-down:before { - content: "\f149"; -} -.fa-check-square:before { - content: "\f14a"; -} -.fa-pencil-square:before { - content: "\f14b"; -} -.fa-external-link-square:before { - content: "\f14c"; -} -.fa-share-square:before { - content: "\f14d"; -} -.fa-compass:before { - content: "\f14e"; -} -.fa-toggle-down:before, -.fa-caret-square-o-down:before { - content: "\f150"; -} -.fa-toggle-up:before, -.fa-caret-square-o-up:before { - content: "\f151"; -} -.fa-toggle-right:before, -.fa-caret-square-o-right:before { - content: "\f152"; -} -.fa-euro:before, -.fa-eur:before { - content: "\f153"; -} -.fa-gbp:before { - content: "\f154"; -} -.fa-dollar:before, -.fa-usd:before { - content: "\f155"; -} -.fa-rupee:before, -.fa-inr:before { - content: "\f156"; -} -.fa-cny:before, -.fa-rmb:before, -.fa-yen:before, -.fa-jpy:before { - content: "\f157"; -} -.fa-ruble:before, -.fa-rouble:before, -.fa-rub:before { - content: "\f158"; -} -.fa-won:before, -.fa-krw:before { - content: "\f159"; -} -.fa-bitcoin:before, -.fa-btc:before { - content: "\f15a"; -} -.fa-file:before { - content: "\f15b"; -} -.fa-file-text:before { - content: "\f15c"; -} -.fa-sort-alpha-asc:before { - content: "\f15d"; -} -.fa-sort-alpha-desc:before { - content: "\f15e"; -} -.fa-sort-amount-asc:before { - content: "\f160"; -} -.fa-sort-amount-desc:before { - content: "\f161"; -} -.fa-sort-numeric-asc:before { - content: "\f162"; -} -.fa-sort-numeric-desc:before { - content: "\f163"; -} -.fa-thumbs-up:before { - content: "\f164"; -} -.fa-thumbs-down:before { - content: "\f165"; -} -.fa-youtube-square:before { - content: "\f166"; -} -.fa-youtube:before { - content: "\f167"; -} -.fa-xing:before { - content: "\f168"; -} -.fa-xing-square:before { - content: "\f169"; -} -.fa-youtube-play:before { - content: "\f16a"; -} -.fa-dropbox:before { - content: "\f16b"; -} -.fa-stack-overflow:before { - content: "\f16c"; -} -.fa-instagram:before { - content: "\f16d"; -} -.fa-flickr:before { - content: "\f16e"; -} -.fa-adn:before { - content: "\f170"; -} -.fa-bitbucket:before { - content: "\f171"; -} -.fa-bitbucket-square:before { - content: "\f172"; -} -.fa-tumblr:before { - content: "\f173"; -} -.fa-tumblr-square:before { - content: "\f174"; -} -.fa-long-arrow-down:before { - content: "\f175"; -} -.fa-long-arrow-up:before { - content: "\f176"; -} -.fa-long-arrow-left:before { - content: "\f177"; -} -.fa-long-arrow-right:before { - content: "\f178"; -} -.fa-apple:before { - content: "\f179"; -} -.fa-windows:before { - content: "\f17a"; -} -.fa-android:before { - content: "\f17b"; -} -.fa-linux:before { - content: "\f17c"; -} -.fa-dribbble:before { - content: "\f17d"; -} -.fa-skype:before { - content: "\f17e"; -} -.fa-foursquare:before { - content: "\f180"; -} -.fa-trello:before { - content: "\f181"; -} -.fa-female:before { - content: "\f182"; -} -.fa-male:before { - content: "\f183"; -} -.fa-gittip:before { - content: "\f184"; -} -.fa-sun-o:before { - content: "\f185"; -} -.fa-moon-o:before { - content: "\f186"; -} -.fa-archive:before { - content: "\f187"; -} -.fa-bug:before { - content: "\f188"; -} -.fa-vk:before { - content: "\f189"; -} -.fa-weibo:before { - content: "\f18a"; -} -.fa-renren:before { - content: "\f18b"; -} -.fa-pagelines:before { - content: "\f18c"; -} -.fa-stack-exchange:before { - content: "\f18d"; -} -.fa-arrow-circle-o-right:before { - content: "\f18e"; -} -.fa-arrow-circle-o-left:before { - content: "\f190"; -} -.fa-toggle-left:before, -.fa-caret-square-o-left:before { - content: "\f191"; -} -.fa-dot-circle-o:before { - content: "\f192"; -} -.fa-wheelchair:before { - content: "\f193"; -} -.fa-vimeo-square:before { - content: "\f194"; -} -.fa-turkish-lira:before, -.fa-try:before { - content: "\f195"; -} -.fa-plus-square-o:before { - content: "\f196"; -} -.fa-space-shuttle:before { - content: "\f197"; -} -.fa-slack:before { - content: "\f198"; -} -.fa-envelope-square:before { - content: "\f199"; -} -.fa-wordpress:before { - content: "\f19a"; -} -.fa-openid:before { - content: "\f19b"; -} -.fa-institution:before, -.fa-bank:before, -.fa-university:before { - content: "\f19c"; -} -.fa-mortar-board:before, -.fa-graduation-cap:before { - content: "\f19d"; -} -.fa-yahoo:before { - content: "\f19e"; -} -.fa-google:before { - content: "\f1a0"; -} -.fa-reddit:before { - content: "\f1a1"; -} -.fa-reddit-square:before { - content: "\f1a2"; -} -.fa-stumbleupon-circle:before { - content: "\f1a3"; -} -.fa-stumbleupon:before { - content: "\f1a4"; -} -.fa-delicious:before { - content: "\f1a5"; -} -.fa-digg:before { - content: "\f1a6"; -} -.fa-pied-piper:before { - content: "\f1a7"; -} -.fa-pied-piper-alt:before { - content: "\f1a8"; -} -.fa-drupal:before { - content: "\f1a9"; -} -.fa-joomla:before { - content: "\f1aa"; -} -.fa-language:before { - content: "\f1ab"; -} -.fa-fax:before { - content: "\f1ac"; -} -.fa-building:before { - content: "\f1ad"; -} -.fa-child:before { - content: "\f1ae"; -} -.fa-paw:before { - content: "\f1b0"; -} -.fa-spoon:before { - content: "\f1b1"; -} -.fa-cube:before { - content: "\f1b2"; -} -.fa-cubes:before { - content: "\f1b3"; -} -.fa-behance:before { - content: "\f1b4"; -} -.fa-behance-square:before { - content: "\f1b5"; -} -.fa-steam:before { - content: "\f1b6"; -} -.fa-steam-square:before { - content: "\f1b7"; -} -.fa-recycle:before { - content: "\f1b8"; -} -.fa-automobile:before, -.fa-car:before { - content: "\f1b9"; -} -.fa-cab:before, -.fa-taxi:before { - content: "\f1ba"; -} -.fa-tree:before { - content: "\f1bb"; -} -.fa-spotify:before { - content: "\f1bc"; -} -.fa-deviantart:before { - content: "\f1bd"; -} -.fa-soundcloud:before { - content: "\f1be"; -} -.fa-database:before { - content: "\f1c0"; -} -.fa-file-pdf-o:before { - content: "\f1c1"; -} -.fa-file-word-o:before { - content: "\f1c2"; -} -.fa-file-excel-o:before { - content: "\f1c3"; -} -.fa-file-powerpoint-o:before { - content: "\f1c4"; -} -.fa-file-photo-o:before, -.fa-file-picture-o:before, -.fa-file-image-o:before { - content: "\f1c5"; -} -.fa-file-zip-o:before, -.fa-file-archive-o:before { - content: "\f1c6"; -} -.fa-file-sound-o:before, -.fa-file-audio-o:before { - content: "\f1c7"; -} -.fa-file-movie-o:before, -.fa-file-video-o:before { - content: "\f1c8"; -} -.fa-file-code-o:before { - content: "\f1c9"; -} -.fa-vine:before { - content: "\f1ca"; -} -.fa-codepen:before { - content: "\f1cb"; -} -.fa-jsfiddle:before { - content: "\f1cc"; -} -.fa-life-bouy:before, -.fa-life-buoy:before, -.fa-life-saver:before, -.fa-support:before, -.fa-life-ring:before { - content: "\f1cd"; -} -.fa-circle-o-notch:before { - content: "\f1ce"; -} -.fa-ra:before, -.fa-rebel:before { - content: "\f1d0"; -} -.fa-ge:before, -.fa-empire:before { - content: "\f1d1"; -} -.fa-git-square:before { - content: "\f1d2"; -} -.fa-git:before { - content: "\f1d3"; -} -.fa-hacker-news:before { - content: "\f1d4"; -} -.fa-tencent-weibo:before { - content: "\f1d5"; -} -.fa-qq:before { - content: "\f1d6"; -} -.fa-wechat:before, -.fa-weixin:before { - content: "\f1d7"; -} -.fa-send:before, -.fa-paper-plane:before { - content: "\f1d8"; -} -.fa-send-o:before, -.fa-paper-plane-o:before { - content: "\f1d9"; -} -.fa-history:before { - content: "\f1da"; -} -.fa-circle-thin:before { - content: "\f1db"; -} -.fa-header:before { - content: "\f1dc"; -} -.fa-paragraph:before { - content: "\f1dd"; -} -.fa-sliders:before { - content: "\f1de"; -} -.fa-share-alt:before { - content: "\f1e0"; -} -.fa-share-alt-square:before { - content: "\f1e1"; -} -.fa-bomb:before { - content: "\f1e2"; -} -.fa-soccer-ball-o:before, -.fa-futbol-o:before { - content: "\f1e3"; -} -.fa-tty:before { - content: "\f1e4"; -} -.fa-binoculars:before { - content: "\f1e5"; -} -.fa-plug:before { - content: "\f1e6"; -} -.fa-slideshare:before { - content: "\f1e7"; -} -.fa-twitch:before { - content: "\f1e8"; -} -.fa-yelp:before { - content: "\f1e9"; -} -.fa-newspaper-o:before { - content: "\f1ea"; -} -.fa-wifi:before { - content: "\f1eb"; -} -.fa-calculator:before { - content: "\f1ec"; -} -.fa-paypal:before { - content: "\f1ed"; -} -.fa-google-wallet:before { - content: "\f1ee"; -} -.fa-cc-visa:before { - content: "\f1f0"; -} -.fa-cc-mastercard:before { - content: "\f1f1"; -} -.fa-cc-discover:before { - content: "\f1f2"; -} -.fa-cc-amex:before { - content: "\f1f3"; -} -.fa-cc-paypal:before { - content: "\f1f4"; -} -.fa-cc-stripe:before { - content: "\f1f5"; -} -.fa-bell-slash:before { - content: "\f1f6"; -} -.fa-bell-slash-o:before { - content: "\f1f7"; -} -.fa-trash:before { - content: "\f1f8"; -} -.fa-copyright:before { - content: "\f1f9"; -} -.fa-at:before { - content: "\f1fa"; -} -.fa-eyedropper:before { - content: "\f1fb"; -} -.fa-paint-brush:before { - content: "\f1fc"; -} -.fa-birthday-cake:before { - content: "\f1fd"; -} -.fa-area-chart:before { - content: "\f1fe"; -} -.fa-pie-chart:before { - content: "\f200"; -} -.fa-line-chart:before { - content: "\f201"; -} -.fa-lastfm:before { - content: "\f202"; -} -.fa-lastfm-square:before { - content: "\f203"; -} -.fa-toggle-off:before { - content: "\f204"; -} -.fa-toggle-on:before { - content: "\f205"; -} -.fa-bicycle:before { - content: "\f206"; -} -.fa-bus:before { - content: "\f207"; -} -.fa-ioxhost:before { - content: "\f208"; -} -.fa-angellist:before { - content: "\f209"; -} -.fa-cc:before { - content: "\f20a"; -} -.fa-shekel:before, -.fa-sheqel:before, -.fa-ils:before { - content: "\f20b"; -} -.fa-meanpath:before { - content: "\f20c"; -} diff --git a/apps/static/css/font-awesome.min.css b/apps/static/css/font-awesome.min.css old mode 100644 new mode 100755 index ec53d4d6d..540440ce8 --- a/apps/static/css/font-awesome.min.css +++ b/apps/static/css/font-awesome.min.css @@ -1,4 +1,4 @@ /*! - * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.2.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"} \ No newline at end of file + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/apps/static/css/jumpserver.css b/apps/static/css/jumpserver.css index 5e9360545..28fe88b68 100644 --- a/apps/static/css/jumpserver.css +++ b/apps/static/css/jumpserver.css @@ -322,4 +322,20 @@ div.dataTables_wrapper div.dataTables_filter { .welcome-message img { margin: -11px 0; +} + +.nav.nav-tabs li.active { + background-color: #FFF; +} + +.ibox-title { + border-top: none; +} + +.nav.nav-tabs li > a { + max-height: 38px; +} + +.nav.nav-tabs li.active a { + border: none; } \ No newline at end of file diff --git a/apps/static/fonts/FontAwesome.otf b/apps/static/fonts/FontAwesome.otf old mode 100644 new mode 100755 index f7936cc1e..401ec0f36 Binary files a/apps/static/fonts/FontAwesome.otf and b/apps/static/fonts/FontAwesome.otf differ diff --git a/apps/static/fonts/fontawesome-webfont.eot b/apps/static/fonts/fontawesome-webfont.eot old mode 100644 new mode 100755 index 33b2bb800..e9f60ca95 Binary files a/apps/static/fonts/fontawesome-webfont.eot and b/apps/static/fonts/fontawesome-webfont.eot differ diff --git a/apps/static/fonts/fontawesome-webfont.svg b/apps/static/fonts/fontawesome-webfont.svg old mode 100644 new mode 100755 index 1ee89d436..855c845e5 --- a/apps/static/fonts/fontawesome-webfont.svg +++ b/apps/static/fonts/fontawesome-webfont.svg @@ -1,565 +1,2671 @@ - - + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/static/fonts/fontawesome-webfont.ttf b/apps/static/fonts/fontawesome-webfont.ttf old mode 100644 new mode 100755 index ed9372f8e..35acda2fa Binary files a/apps/static/fonts/fontawesome-webfont.ttf and b/apps/static/fonts/fontawesome-webfont.ttf differ diff --git a/apps/static/fonts/fontawesome-webfont.woff b/apps/static/fonts/fontawesome-webfont.woff old mode 100644 new mode 100755 index 8b280b98f..400014a4b Binary files a/apps/static/fonts/fontawesome-webfont.woff and b/apps/static/fonts/fontawesome-webfont.woff differ diff --git a/apps/static/fonts/fontawesome-webfont.woff2 b/apps/static/fonts/fontawesome-webfont.woff2 old mode 100644 new mode 100755 index 3311d5851..4d13fc604 Binary files a/apps/static/fonts/fontawesome-webfont.woff2 and b/apps/static/fonts/fontawesome-webfont.woff2 differ diff --git a/apps/static/js/jumpserver.js b/apps/static/js/jumpserver.js index a76c44187..aca70a5a0 100644 --- a/apps/static/js/jumpserver.js +++ b/apps/static/js/jumpserver.js @@ -157,6 +157,11 @@ function APIUpdateAttr(props) { props = props || {}; var success_message = props.success_message || '更新成功!'; var fail_message = props.fail_message || '更新时发生未知错误.'; + var flash_message = true; + if (props.flash_message === false){ + flash_message = false; + } + $.ajax({ url: props.url, type: props.method || "PATCH", @@ -164,12 +169,16 @@ function APIUpdateAttr(props) { contentType: props.content_type || "application/json; charset=utf-8", dataType: props.data_type || "json" }).done(function(data, textStatue, jqXHR) { - toastr.success(success_message); + if (flash_message) { + toastr.success(success_message); + } if (typeof props.success === 'function') { return props.success(data); } }).fail(function(jqXHR, textStatus, errorThrown) { - toastr.error(fail_message); + if (flash_message) { + toastr.error(fail_message); + } if (typeof props.error === 'function') { return props.error(jqXHR.responseText); } @@ -310,11 +319,130 @@ jumpserver.initDataTable = function (options) { if (!jumpserver.checked) { $(this).closest('table').find('.ipt_check').prop('checked', true); jumpserver.checked = true; - table.rows().select(); + table.rows({search:'applied', page:'current'}).select(); } else { $(this).closest('table').find('.ipt_check').prop('checked', false); jumpserver.checked = false; - table.rows().deselect(); + table.rows({search:'applied', page:'current'}).deselect(); + } + }); + + return table; +}; + +jumpserver.initServerSideDataTable = function (options) { + // options = { + // ele *: $('#dataTable_id'), + // ajax_url *: '{% url 'users:user-list-api' %}', + // columns *: [{data: ''}, ....], + // dom: 'fltip', + // i18n_url: '{% static "js/...../en-us.json" %}', + // order: [[1, 'asc'], [2, 'asc'], ...], + // buttons: ['excel', 'pdf', 'print'], + // columnDefs: [{target: 0, createdCell: ()=>{}}, ...], + // uc_html: 'header button', + // op_html: 'div.btn-group?', + // paging: true + // } + var ele = options.ele || $('.dataTable'); + var columnDefs = [ + { + targets: 0, + orderable: false, + createdCell: function (td, cellData) { + $(td).html(''.replace('99991937', cellData)); + } + }, + {className: 'text-center', targets: '_all'} + ]; + columnDefs = options.columnDefs ? options.columnDefs.concat(columnDefs) : columnDefs; + var select = { + style: 'multi', + selector: 'td:first-child' + }; + var table = ele.DataTable({ + pageLength: options.pageLength || 15, + dom: options.dom || '<"#uc.pull-left">flt<"row m-t"<"col-md-8"<"#op.col-md-6"><"col-md-6 text-center"i>><"col-md-4"p>>', + order: options.order || [], + // select: options.select || 'multi', + buttons: [], + columnDefs: columnDefs, + serverSide: true, + processing: true, + ajax: { + url: options.ajax_url , + data: function (data) { + delete data.columns; + if (data.length !== null ){ + data.limit = data.length; + delete data.length; + } + if (data.start !== null) { + data.offset = data.start; + delete data.start; + } + if (data.search !== null) { + var search_val = data.search.value; + data.search = search_val; + } + if (data.order !== null && data.order.length === 1) { + var col = data.order[0].column; + var order = options.columns[col].data; + if (data.order[0].dir = "desc") { + order = "-" + order; + } + data.order = order; + } + }, + dataFilter: function(data){ + var json = jQuery.parseJSON( data ); + json.recordsTotal = json.count; + json.recordsFiltered = json.count; + return JSON.stringify(json); // return JSON string + }, + dataSrc: "results" + }, + columns: options.columns || [], + select: options.select || select, + language: { + search: "搜索", + lengthMenu: "每页 _MENU_", + info: "显示第 _START_ 至 _END_ 项结果; 总共 _TOTAL_ 项", + infoFiltered: "", + infoEmpty: "", + zeroRecords: "没有匹配项", + emptyTable: "没有记录", + paginate: { + first: "«", + previous: "‹", + next: "›", + last: "»" + } + }, + lengthMenu: [[15, 25, 50], [15, 25, 50]] + }); + table.on('select', function(e, dt, type, indexes) { + var $node = table[ type ]( indexes ).nodes().to$(); + $node.find('input.ipt_check').prop('checked', true); + jumpserver.selected[$node.find('input.ipt_check').prop('id')] = true + }).on('deselect', function(e, dt, type, indexes) { + var $node = table[ type ]( indexes ).nodes().to$(); + $node.find('input.ipt_check').prop('checked', false); + jumpserver.selected[$node.find('input.ipt_check').prop('id')] = false + }). + on('draw', function(){ + $('#op').html(options.op_html || ''); + $('#uc').html(options.uc_html || ''); + }); + $('.ipt_check_all').on('click', function() { + if (!jumpserver.checked) { + $(this).closest('table').find('.ipt_check').prop('checked', true); + jumpserver.checked = true; + table.rows({search:'applied', page:'current'}).select(); + } else { + $(this).closest('table').find('.ipt_check').prop('checked', false); + jumpserver.checked = false; + table.rows({search:'applied', page:'current'}).deselect(); } }); diff --git a/apps/templates/_head_css_js.html b/apps/templates/_head_css_js.html index 917e57f18..93ca99462 100644 --- a/apps/templates/_head_css_js.html +++ b/apps/templates/_head_css_js.html @@ -2,7 +2,7 @@ - + diff --git a/apps/templates/_message.html b/apps/templates/_message.html index 29a25e963..046f48ae6 100644 --- a/apps/templates/_message.html +++ b/apps/templates/_message.html @@ -1,6 +1,6 @@ {% load i18n %} {% block first_login_message %} - {% if user.is_authenticated and user.is_first_login %} + {% if request.user.is_authenticated and request.user.is_first_login %}
    {% url 'users:user-first-login' as first_login_url %} {% blocktrans %} @@ -10,7 +10,7 @@ {% endif %} {% endblock %} {% block update_public_key_message %} - {% if user.is_authenticated and not user.is_public_key_valid %} + {% if request.user.is_authenticated and not request.user.is_public_key_valid %}
    {% url 'users:user-pubkey-update' as user_pubkey_update %} {% blocktrans %} diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index f931cbb6d..39b14add8 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -54,6 +54,7 @@ + {#
  • #} {# #} {# {% trans 'File' %}#} @@ -63,8 +64,8 @@ {#
  • {% trans 'File download' %}
  • #} {# #} {##} -{#
  • #} -{# #} -{# {% trans 'Settings' %}#} -{# #} -{#
  • #} \ No newline at end of file +
  • + + {% trans 'Settings' %} + +
  • \ No newline at end of file diff --git a/apps/templates/_nav_user.html b/apps/templates/_nav_user.html index cccb13fc4..f82ad6776 100644 --- a/apps/templates/_nav_user.html +++ b/apps/templates/_nav_user.html @@ -8,4 +8,9 @@ {% trans 'Profile' %} + +
  • + + {% trans 'Web terminal' %} +
  • \ No newline at end of file diff --git a/apps/terminal/api.py b/apps/terminal/api.py index 810a19496..87311d5b2 100644 --- a/apps/terminal/api.py +++ b/apps/terminal/api.py @@ -141,7 +141,9 @@ class StatusViewSet(viewsets.ModelViewSet): session = serializer.save() return session else: - msg = "session data is not valid {}".format(serializer.errors) + msg = "session data is not valid {}: {}".format( + serializer.errors, str(serializer.data) + ) logger.error(msg) return None diff --git a/apps/terminal/signals_handler.py b/apps/terminal/signals_handler.py index 757a97bc6..3926a5751 100644 --- a/apps/terminal/signals_handler.py +++ b/apps/terminal/signals_handler.py @@ -13,10 +13,6 @@ RUNNING = False logger = get_logger(__file__) -@shared_task -@register_as_period_task(interval=3600) -@after_app_ready_start -@after_app_shutdown_clean def set_session_info_cache(): logger.debug("") from .utils import get_session_asset_list, get_session_user_list, \ diff --git a/apps/terminal/views/command.py b/apps/terminal/views/command.py index 2bd588ff6..8b0479d3e 100644 --- a/apps/terminal/views/command.py +++ b/apps/terminal/views/command.py @@ -19,7 +19,7 @@ class CommandListView(DatetimeSearchMixin, ListView): model = Command template_name = "terminal/command_list.html" context_object_name = 'command_list' - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE command = user = asset = system_user = "" date_from = date_to = None diff --git a/apps/terminal/views/session.py b/apps/terminal/views/session.py index 087e16b68..22d04fbaf 100644 --- a/apps/terminal/views/session.py +++ b/apps/terminal/views/session.py @@ -26,7 +26,7 @@ class SessionListView(AdminUserRequiredMixin, DatetimeSearchMixin, ListView): model = Session template_name = 'terminal/session_list.html' context_object_name = 'session_list' - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE user = asset = system_user = '' date_from = date_to = None diff --git a/apps/users/api.py b/apps/users/api.py index e1d38f572..2dfd89fba 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -13,14 +13,14 @@ from .tasks import write_login_log_async from .models import User, UserGroup from .permissions import IsSuperUser, IsValidUser, IsCurrentUserOrReadOnly from .utils import check_user_valid, generate_token -from common.mixins import IDInFilterMixin +from common.mixins import CustomFilterMixin from common.utils import get_logger logger = get_logger(__name__) -class UserViewSet(IDInFilterMixin, BulkModelViewSet): +class UserViewSet(CustomFilterMixin, BulkModelViewSet): queryset = User.objects.exclude(role="App") # queryset = User.objects.all().exclude(role="App").order_by("date_joined") serializer_class = UserSerializer @@ -72,7 +72,7 @@ class UserUpdatePKApi(generics.UpdateAPIView): user.save() -class UserGroupViewSet(IDInFilterMixin, BulkModelViewSet): +class UserGroupViewSet(CustomFilterMixin, BulkModelViewSet): queryset = UserGroup.objects.all() serializer_class = UserGroupSerializer @@ -128,7 +128,11 @@ class UserAuthApi(APIView): user_agent = request.data.get('HTTP_USER_AGENT', '') if not login_ip: - login_ip = request.META.get('HTTP_X_REAL_IP') or request.META.get("REMOTE_ADDR") + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') + if x_forwarded_for: + login_ip = x_forwarded_for[0] + else: + login_ip = request.META.get("REMOTE_ADDR") user, msg = check_user_valid( username=username, password=password, diff --git a/apps/users/authentication.py b/apps/users/authentication.py index 09321953e..647f9a776 100644 --- a/apps/users/authentication.py +++ b/apps/users/authentication.py @@ -113,7 +113,7 @@ class AccessKeyAuthentication(authentication.BaseAuthentication): class AccessTokenAuthentication(authentication.BaseAuthentication): keyword = 'Bearer' model = User - expiration = settings.CONFIG.TOKEN_EXPIRATION or 3600 + expiration = settings.TOKEN_EXPIRATION or 3600 def authenticate(self, request): auth = authentication.get_authorization_header(request).split() diff --git a/apps/users/forms.py b/apps/users/forms.py index c4f008663..018c39a18 100644 --- a/apps/users/forms.py +++ b/apps/users/forms.py @@ -29,7 +29,7 @@ class UserCreateUpdateForm(forms.ModelForm): model = User fields = [ 'username', 'name', 'email', 'groups', 'wechat', - 'phone', 'role', 'date_expired', 'comment', 'password' + 'phone', 'role', 'date_expired', 'comment', ] help_texts = { 'username': '* required', @@ -38,13 +38,16 @@ class UserCreateUpdateForm(forms.ModelForm): } widgets = { 'groups': forms.SelectMultiple( - attrs={'class': 'select2', - 'data-placeholder': _('Join user groups')}), + attrs={ + 'class': 'select2', + 'data-placeholder': _('Join user groups') + } + ), } def save(self, commit=True): - user = super().save(commit=commit) password = self.cleaned_data.get('password') + user = super().save(commit=commit) if password: user.set_password(password) user.save() @@ -153,7 +156,7 @@ class UserBulkUpdateForm(forms.ModelForm): class Meta: model = User - fields = ['users', 'role', 'groups', 'date_expired', 'is_active'] + fields = ['users', 'role', 'groups', 'date_expired'] widgets = { "groups": forms.SelectMultiple( attrs={ @@ -169,6 +172,7 @@ class UserBulkUpdateForm(forms.ModelForm): if self.data.get(field) is not None: changed_fields.append(field) + print(changed_fields) cleaned_data = {k: v for k, v in self.cleaned_data.items() if k in changed_fields} users = cleaned_data.pop('users', '') @@ -183,7 +187,7 @@ class UserBulkUpdateForm(forms.ModelForm): class UserGroupForm(forms.ModelForm): users = forms.ModelMultipleChoiceField( - queryset=User.objects.all(), + queryset=User.objects.exclude(role=User.ROLE_APP), label=_("User"), widget=forms.SelectMultiple( attrs={ diff --git a/apps/users/models/__init__.py b/apps/users/models/__init__.py index 269f68bd4..f3c9d1941 100644 --- a/apps/users/models/__init__.py +++ b/apps/users/models/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # -from .group import * from .user import * +from .group import * from .authentication import * from .utils import * diff --git a/apps/users/models/group.py b/apps/users/models/group.py index a0c7a8b0f..b4b7aacc1 100644 --- a/apps/users/models/group.py +++ b/apps/users/models/group.py @@ -20,12 +20,6 @@ class UserGroup(NoDeleteModelMixin): def __str__(self): return self.name - def delete(self, using=None, keep_parents=False): - if self.name != 'Default': - self.users.clear() - return super(UserGroup, self).delete() - return True - class Meta: ordering = ['name'] diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 899cec4cb..4ae0e62c4 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -13,7 +13,6 @@ from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.shortcuts import reverse -from .group import UserGroup from common.utils import get_signer, date_expired_default @@ -35,7 +34,7 @@ class User(AbstractUser): username = models.CharField(max_length=128, unique=True, verbose_name=_('Username')) name = models.CharField(max_length=128, verbose_name=_('Name')) email = models.EmailField(max_length=128, unique=True, verbose_name=_('Email')) - groups = models.ManyToManyField(UserGroup, related_name='users', blank=True, verbose_name=_('User group')) + groups = models.ManyToManyField('users.UserGroup', related_name='users', blank=True, verbose_name=_('User group')) role = models.CharField(choices=ROLE_CHOICES, default='User', max_length=10, blank=True, verbose_name=_('Role')) avatar = models.ImageField(upload_to="avatar", null=True, verbose_name=_('Avatar')) wechat = models.CharField(max_length=128, blank=True, verbose_name=_('Wechat')) @@ -149,12 +148,7 @@ class User(AbstractUser): def save(self, *args, **kwargs): if not self.name: self.name = self.username - super().save(*args, **kwargs) - # Add the current user to the default group. - if not self.groups.count(): - group = UserGroup.initial() - self.groups.add(group) @property def private_token(self): @@ -254,6 +248,7 @@ class User(AbstractUser): #: Use this method initial user @classmethod def initial(cls): + from .group import UserGroup user = cls(username='admin', email='admin@jumpserver.org', name=_('Administrator'), @@ -269,6 +264,7 @@ class User(AbstractUser): from random import seed, choice import forgery_py from django.db import IntegrityError + from .group import UserGroup seed() for i in range(count): diff --git a/apps/users/signals.py b/apps/users/signals.py index 537cfb329..39e57f5a5 100644 --- a/apps/users/signals.py +++ b/apps/users/signals.py @@ -2,16 +2,19 @@ # from django.dispatch import Signal, receiver +from django.db.models.signals import post_save from common.utils import get_logger +from .models import User logger = get_logger(__file__) -on_user_created = Signal(providing_args=['user', 'request']) -@receiver(on_user_created) -def send_user_add_mail_to_user(sender, user=None, **kwargs): - from .utils import send_user_created_mail - logger.debug("Receive asset create signal, update asset hardware info") - send_user_created_mail(user) +@receiver(post_save, sender=User) +def on_user_created(sender, instance=None, created=False, **kwargs): + if created: + logger.debug("Receive user `{}` create signal".format(instance.name)) + from .utils import send_user_created_mail + logger.info(" - Sending welcome mail ...".format(instance.name)) + send_user_created_mail(instance) diff --git a/apps/users/templates/users/login.html b/apps/users/templates/users/login.html index e9504d63c..284b6bc43 100644 --- a/apps/users/templates/users/login.html +++ b/apps/users/templates/users/login.html @@ -6,7 +6,7 @@ - JumpServer + Jumpserver {% include '_head_css_js.html' %} diff --git a/apps/users/templates/users/reset_password.html b/apps/users/templates/users/reset_password.html index 6c454ee01..cf8003a86 100644 --- a/apps/users/templates/users/reset_password.html +++ b/apps/users/templates/users/reset_password.html @@ -6,7 +6,7 @@ - JumpServer + Jumpserver {% include '_head_css_js.html' %} diff --git a/apps/users/templates/users/user_group_create_update.html b/apps/users/templates/users/user_group_create_update.html index 5c388e668..6baef2a94 100644 --- a/apps/users/templates/users/user_group_create_update.html +++ b/apps/users/templates/users/user_group_create_update.html @@ -25,20 +25,6 @@ {% csrf_token %} {% bootstrap_field form.name layout="horizontal" %} {% bootstrap_field form.users layout="horizontal" %} -{#
    #} -{# #} -{#
    #} -{# #} -{#
    #} -{#
    #} {% bootstrap_field form.comment layout="horizontal" %}
    @@ -57,7 +43,9 @@ {% block custom_foot_js %} {% endblock %} diff --git a/apps/users/templates/users/user_list.html b/apps/users/templates/users/user_list.html index ab9bcf3c7..24ec5b226 100644 --- a/apps/users/templates/users/user_list.html +++ b/apps/users/templates/users/user_list.html @@ -161,18 +161,30 @@ $(document).ready(function(){ var body = $.each(id_list, function(index, user_object) { user_object['is_active'] = false; }); - console.log(body); - APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(body)}); - $data_table.ajax.reload(); - jumpserver.checked = false; + function success() { + location.reload(); + } + APIUpdateAttr({ + url: the_url, + method: 'PATCH', + body: JSON.stringify(body), + success: success + }); + location.reload(); } function doActive() { var body = $.each(id_list, function(index, user_object) { user_object['is_active'] = true; }); - APIUpdateAttr({url: the_url, method: 'PATCH', body: JSON.stringify(body)}); - $data_table.ajax.reload(); - jumpserver.checked = false; + function success() { + location.reload(); + } + APIUpdateAttr({ + url: the_url, + method: 'PATCH', + body: JSON.stringify(body), + success: success + }); } function doDelete() { swal({ diff --git a/apps/users/templates/users/user_profile.html b/apps/users/templates/users/user_profile.html index 99f5ca6e1..8b26c943d 100644 --- a/apps/users/templates/users/user_profile.html +++ b/apps/users/templates/users/user_profile.html @@ -17,7 +17,7 @@ {% trans 'Profile' %}
  • - {% trans 'Settings' %} + {% trans 'Setting' %}
  • diff --git a/apps/users/utils.py b/apps/users/utils.py index 02ab846f5..fd03ad97d 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -151,12 +151,12 @@ def check_user_valid(**kwargs): return None, _('Password or SSH public key invalid') -def refresh_token(token, user, expiration=settings.CONFIG.TOKEN_EXPIRATION or 3600): +def refresh_token(token, user, expiration=settings.TOKEN_EXPIRATION or 3600): cache.set(token, user.id, expiration) def generate_token(request, user): - expiration = settings.CONFIG.TOKEN_EXPIRATION or 3600 + expiration = settings.TOKEN_EXPIRATION or 3600 remote_addr = request.META.get('REMOTE_ADDR', '') if not isinstance(remote_addr, bytes): remote_addr = remote_addr.encode("utf-8") @@ -180,8 +180,10 @@ def validate_ip(ip): def write_login_log(username, type='', ip='', user_agent=''): if not (ip and validate_ip(ip)): - ip = '0.0.0.0' - city = get_ip_city(ip) + ip = ip[:15] + city = "Unknown" + else: + city = get_ip_city(ip) LoginLog.objects.create( username=username, type=type, ip=ip, city=city, user_agent=user_agent diff --git a/apps/users/views/group.py b/apps/users/views/group.py index a26cfcaec..4b11e7901 100644 --- a/apps/users/views/group.py +++ b/apps/users/views/group.py @@ -2,17 +2,15 @@ from __future__ import unicode_literals from django import forms -from django.shortcuts import reverse, redirect from django.utils.translation import ugettext as _ from django.urls import reverse_lazy -from django.views.generic import ListView from django.views.generic.base import TemplateView -from django.views.generic.edit import CreateView, UpdateView, FormMixin -from django.views.generic.detail import DetailView, SingleObjectMixin +from django.views.generic.edit import CreateView, UpdateView +from django.views.generic.detail import DetailView from django.contrib.messages.views import SuccessMessageMixin from common.utils import get_logger -from perms.models import AssetPermission +from common.const import create_success_msg, update_success_msg from ..models import User, UserGroup from ..utils import AdminUserRequiredMixin from .. import forms @@ -39,9 +37,7 @@ class UserGroupCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateVie form_class = forms.UserGroupForm template_name = 'users/user_group_create_update.html' success_url = reverse_lazy('users:user-group-list') - success_message = _( - 'User group {name} was created successfully' - ) + success_message = create_success_msg def get_context_data(self, **kwargs): context = { @@ -51,21 +47,13 @@ class UserGroupCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateVie kwargs.update(context) return super().get_context_data(**kwargs) - def get_success_message(self, cleaned_data): - url = reverse_lazy( - 'users:user-group-detail', - kwargs={'pk': self.object.id} - ) - return self.success_message.format( - url=url, name=self.object.name - ) - -class UserGroupUpdateView(AdminUserRequiredMixin, UpdateView): +class UserGroupUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = UserGroup form_class = forms.UserGroupForm template_name = 'users/user_group_create_update.html' success_url = reverse_lazy('users:user-group-list') + success_message = update_success_msg def get_context_data(self, **kwargs): users = User.objects.all() diff --git a/apps/users/views/login.py b/apps/users/views/login.py index fabc39b7d..614f321db 100644 --- a/apps/users/views/login.py +++ b/apps/users/views/login.py @@ -53,7 +53,11 @@ class UserLoginView(FormView): if not self.request.session.test_cookie_worked(): return HttpResponse(_("Please enable cookies and try again.")) auth_login(self.request, form.get_user()) - login_ip = self.request.META.get('REMOTE_ADDR', '') + x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR', '').split(',') + if x_forwarded_for: + login_ip = x_forwarded_for[0] + else: + login_ip = self.request.META.get('REMOTE_ADDR', '') user_agent = self.request.META.get('HTTP_USER_AGENT', '') write_login_log_async.delay( self.request.user.username, type='W', @@ -184,7 +188,7 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): user.is_public_key_valid = True user.save() context = { - 'user_guide_url': settings.CONFIG.USER_GUIDE_URL + 'user_guide_url': settings.USER_GUIDE_URL } return render(self.request, 'users/first_login_done.html', context) @@ -215,7 +219,7 @@ class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): class LoginLogListView(DatetimeSearchMixin, ListView): template_name = 'users/login_log_list.html' model = LoginLog - paginate_by = settings.CONFIG.DISPLAY_PER_PAGE + paginate_by = settings.DISPLAY_PER_PAGE user = keyword = "" date_to = date_from = None diff --git a/apps/users/views/user.py b/apps/users/views/user.py index ec54e1d0c..9110e2887 100644 --- a/apps/users/views/user.py +++ b/apps/users/views/user.py @@ -27,12 +27,14 @@ from django.views.generic.detail import DetailView, SingleObjectMixin from django.views.decorators.csrf import csrf_exempt from django.contrib.auth import logout as auth_logout +from common.const import create_success_msg, update_success_msg +from common.mixins import JSONResponseMixin +from common.utils import get_logger, get_object_or_none, is_uuid from .. import forms from ..models import User, UserGroup from ..utils import AdminUserRequiredMixin from ..signals import on_user_created -from common.mixins import JSONResponseMixin -from common.utils import get_logger, get_object_or_none, is_uuid + __all__ = [ 'UserListView', 'UserCreateView', 'UserDetailView', @@ -63,7 +65,7 @@ class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): form_class = forms.UserCreateUpdateForm template_name = 'users/user_create.html' success_url = reverse_lazy('users:user-list') - success_message = _('Create user {name} successfully.') + success_message = create_success_msg def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) @@ -74,22 +76,16 @@ class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): user = form.save(commit=False) user.created_by = self.request.user.username or 'System' user.save() - on_user_created.send(self.__class__, user=user) return super().form_valid(form) - def get_success_message(self, cleaned_data): - url = reverse_lazy('users:user-detail', kwargs={'pk': self.object.pk}) - return self.success_message.format( - url=url, name=self.object.name - ) - -class UserUpdateView(AdminUserRequiredMixin, UpdateView): +class UserUpdateView(AdminUserRequiredMixin, SuccessMessageMixin, UpdateView): model = User form_class = forms.UserCreateUpdateForm template_name = 'users/user_update.html' context_object_name = 'user_object' success_url = reverse_lazy('users:user-list') + success_message = update_success_msg def get_context_data(self, **kwargs): context = {'app': _('Users'), 'action': _('Update user')} @@ -332,17 +328,10 @@ class UserProfileUpdateView(LoginRequiredMixin, UpdateView): model = User form_class = forms.UserProfileForm success_url = reverse_lazy('users:user-profile') - success_message = _('Create user {name} successfully.') def get_object(self, queryset=None): return self.request.user - def get_success_message(self, cleaned_data): - url = reverse_lazy('users:user-detail', kwargs={'pk': self.object.pk}) - return self.success_message.format( - url=url, name=self.object.name - ) - def get_context_data(self, **kwargs): context = { 'app': _('User'), diff --git a/config_example.py b/config_example.py index 26fe13a4a..45d4b00f3 100644 --- a/config_example.py +++ b/config_example.py @@ -17,14 +17,6 @@ class Config: # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = os.environ.get('SECRET_KEY') or '2vym+ky!997d5kkcc64mnz06y1mmui3lut#(^wd=%s_qj$1%x' - # How many line display every page if using django pager, default 25 - DISPLAY_PER_PAGE = 25 - - # It's used to identify your site, When we send a create mail to user, we only know login url is /login/ - # But we should know the absolute url like: http://jms.jumpserver.org/login/, so SITE_URL is - # HTTP_PROTOCOL://HOST[:PORT] - SITE_URL = 'http://localhost' - # Django security setting, if your disable debug model, you should setting that ALLOWED_HOSTS = ['*'] @@ -65,40 +57,6 @@ class Config: 'port': REDIS_PORT, } - # Api token expiration when create, Jumpserver refresh time when request arrive - TOKEN_EXPIRATION = 3600 - - # Session and csrf domain settings - SESSION_COOKIE_AGE = 3600*24 - - # Email SMTP setting, we only support smtp send mail - EMAIL_HOST = 'smtp.163.com' - EMAIL_PORT = 25 - EMAIL_HOST_USER = '' - EMAIL_HOST_PASSWORD = '' # Caution: Some SMTP server using `Authorization Code` except password - EMAIL_USE_SSL = True if EMAIL_PORT == 465 else False - EMAIL_USE_TLS = True if EMAIL_PORT == 587 else False - EMAIL_SUBJECT_PREFIX = '[Jumpserver] ' - - CAPTCHA_TEST_MODE = False - - # You can set jumpserver usage url here, that when user submit wizard redirect to - USER_GUIDE_URL = '' - - # LDAP Auth settings - AUTH_LDAP = False - AUTH_LDAP_SERVER_URI = 'ldap://localhost:389' - AUTH_LDAP_BIND_DN = 'cn=admin,dc=jumpserver,dc=org' - AUTH_LDAP_BIND_PASSWORD = '' - AUTH_LDAP_SEARCH_OU = 'ou=tech,dc=jumpserver,dc=org' - AUTH_LDAP_SEARCH_FILTER = '(cn=%(user)s)' - AUTH_LDAP_USER_ATTR_MAP = { - "username": "cn", - "name": "sn", - "email": "mail" - } - AUTH_LDAP_START_TLS = False - def __init__(self): pass
    {% trans 'Hardware' %} {% trans 'Active' %} {% trans 'Connective' %}{% trans 'Action' %}