Merge branch 'master' of code.simcu.com:jumpserver/jumpserver

pull/530/head
liuzheng712 2016-08-23 22:35:00 +08:00
commit 7cebc4efe8
30 changed files with 608 additions and 2022 deletions

7
apps/__init__.py Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
if __name__ == '__main__':
pass

Binary file not shown.

Before

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

View File

@ -1,429 +0,0 @@
/*!
Chosen, a Select Box Enhancer for jQuery and Prototype
by Patrick Filler for Harvest, http://getharvest.com
Version 1.1.0
Full source at https://github.com/harvesthq/chosen
Copyright (c) 2011 Harvest http://getharvest.com
MIT License, https://github.com/harvesthq/chosen/blob/master/LICENSE.md
This file is generated by `grunt build`, do not edit it by hand.
*/
/* @group Base */
.chosen-container {
position: relative;
display: inline-block;
vertical-align: middle;
font-size: 13px;
zoom: 1;
*display: inline;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.chosen-container .chosen-drop {
position: absolute;
top: 100%;
left: -9999px;
z-index: 1010;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
width: 100%;
border: 1px solid #aaa;
border-top: 0;
background: #fff;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);
}
.chosen-container.chosen-with-drop .chosen-drop {
left: 0;
}
.chosen-container a {
cursor: pointer;
}
/* @end */
/* @group Single Chosen */
.chosen-container-single .chosen-single {
position: relative;
display: block;
overflow: hidden;
padding: 0 0 0 8px;
height: 23px;
border: 1px solid #aaa;
border-radius: 5px;
background-color: #fff;
background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4));
background: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background: linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background-clip: padding-box;
box-shadow: 0 0 3px white inset, 0 1px 1px rgba(0, 0, 0, 0.1);
color: #444;
text-decoration: none;
white-space: nowrap;
line-height: 24px;
}
.chosen-container-single .chosen-default {
color: #999;
}
.chosen-container-single .chosen-single span {
display: block;
overflow: hidden;
margin-right: 26px;
text-overflow: ellipsis;
white-space: nowrap;
}
.chosen-container-single .chosen-single-with-deselect span {
margin-right: 38px;
}
.chosen-container-single .chosen-single abbr {
position: absolute;
top: 6px;
right: 26px;
display: block;
width: 12px;
height: 12px;
background: url('chosen-sprite.png') -42px 1px no-repeat;
font-size: 1px;
}
.chosen-container-single .chosen-single abbr:hover {
background-position: -42px -10px;
}
.chosen-container-single.chosen-disabled .chosen-single abbr:hover {
background-position: -42px -10px;
}
.chosen-container-single .chosen-single div {
position: absolute;
top: 0;
right: 0;
display: block;
width: 18px;
height: 100%;
}
.chosen-container-single .chosen-single div b {
display: block;
width: 100%;
height: 100%;
background: url('chosen-sprite.png') no-repeat 0px 2px;
}
.chosen-container-single .chosen-search {
position: relative;
z-index: 1010;
margin: 0;
padding: 3px 4px;
white-space: nowrap;
}
.chosen-container-single .chosen-search input[type="text"] {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 1px 0;
padding: 4px 20px 4px 5px;
width: 100%;
height: auto;
outline: 0;
border: 1px solid #aaa;
background: white url('chosen-sprite.png') no-repeat 100% -20px;
background: url('chosen-sprite.png') no-repeat 100% -20px;
font-size: 1em;
font-family: sans-serif;
line-height: normal;
border-radius: 0;
}
.chosen-container-single .chosen-drop {
margin-top: -1px;
border-radius: 0 0 4px 4px;
background-clip: padding-box;
}
.chosen-container-single.chosen-container-single-nosearch .chosen-search {
position: absolute;
left: -9999px;
}
/* @end */
/* @group Results */
.chosen-container .chosen-results {
position: relative;
overflow-x: hidden;
overflow-y: auto;
margin: 0 4px 4px 0;
padding: 0 0 0 4px;
max-height: 240px;
-webkit-overflow-scrolling: touch;
}
.chosen-container .chosen-results li {
display: none;
margin: 0;
padding: 5px 6px;
list-style: none;
line-height: 15px;
-webkit-touch-callout: none;
}
.chosen-container .chosen-results li.active-result {
display: list-item;
cursor: pointer;
}
.chosen-container .chosen-results li.disabled-result {
display: list-item;
color: #ccc;
cursor: default;
}
.chosen-container .chosen-results li.highlighted {
background-color: #3875d7;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc));
background-image: -webkit-linear-gradient(#3875d7 20%, #2a62bc 90%);
background-image: -moz-linear-gradient(#3875d7 20%, #2a62bc 90%);
background-image: -o-linear-gradient(#3875d7 20%, #2a62bc 90%);
background-image: linear-gradient(#3875d7 20%, #2a62bc 90%);
color: #fff;
}
.chosen-container .chosen-results li.no-results {
display: list-item;
background: #f4f4f4;
}
.chosen-container .chosen-results li.group-result {
display: list-item;
font-weight: bold;
cursor: default;
}
.chosen-container .chosen-results li.group-option {
padding-left: 15px;
}
.chosen-container .chosen-results li em {
font-style: normal;
text-decoration: underline;
}
/* @end */
/* @group Multi Chosen */
.chosen-container-multi .chosen-choices {
-moz-box-sizing: border-box;
background-color: #FFFFFF;
border: 1px solid #CBD5DD;
border-radius: 2px;
cursor: text;
height: auto !important;
margin: 0;
min-height: 30px;
overflow: hidden;
padding: 2px;
position: relative;
width: 100%;
}
.chosen-container-multi .chosen-choices li {
float: left;
list-style: none;
}
.chosen-container-multi .chosen-choices li.search-field {
margin: 0;
padding: 0;
white-space: nowrap;
}
.chosen-container-multi .chosen-choices li.search-field input[type="text"] {
margin: 1px 0;
padding: 5px;
height: 25px;
outline: 0;
border: 0 !important;
background: transparent !important;
box-shadow: none;
color: #666;
font-size: 100%;
font-family: sans-serif;
line-height: normal;
border-radius: 0;
}
.chosen-container-multi .chosen-choices li.search-field .default {
color: #999;
}
.chosen-container-multi .chosen-choices li.search-choice {
position: relative;
margin: 3px 0 3px 5px;
padding: 3px 20px 3px 5px;
border: 1px solid #aaa;
border-radius: 3px;
background-color: #e4e4e4;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
background-image: -webkit-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -moz-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -o-linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-clip: padding-box;
box-shadow: 0 0 2px white inset, 0 1px 0 rgba(0, 0, 0, 0.05);
color: #333;
line-height: 13px;
cursor: default;
}
.chosen-container-multi .chosen-choices li.search-choice .search-choice-close {
position: absolute;
top: 4px;
right: 3px;
display: block;
width: 12px;
height: 12px;
background: url('chosen-sprite.png') -42px 1px no-repeat;
font-size: 1px;
}
.chosen-container-multi .chosen-choices li.search-choice .search-choice-close:hover {
background-position: -42px -10px;
}
.chosen-container-multi .chosen-choices li.search-choice-disabled {
padding-right: 5px;
border: 1px solid #ccc;
background-color: #e4e4e4;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
color: #666;
}
.chosen-container-multi .chosen-choices li.search-choice-focus {
background: #d4d4d4;
}
.chosen-container-multi .chosen-choices li.search-choice-focus .search-choice-close {
background-position: -42px -10px;
}
.chosen-container-multi .chosen-results {
margin: 0;
padding: 0;
}
.chosen-container-multi .chosen-drop .result-selected {
display: list-item;
color: #ccc;
cursor: default;
}
/* @end */
/* @group Active */
.chosen-container-active .chosen-single {
border: 1px solid #5897fb;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
.chosen-container-active.chosen-with-drop .chosen-single {
border: 1px solid #aaa;
-moz-border-radius-bottomright: 0;
border-bottom-right-radius: 0;
-moz-border-radius-bottomleft: 0;
border-bottom-left-radius: 0;
background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff));
background-image: -webkit-linear-gradient(#eeeeee 20%, #ffffff 80%);
background-image: -moz-linear-gradient(#eeeeee 20%, #ffffff 80%);
background-image: -o-linear-gradient(#eeeeee 20%, #ffffff 80%);
background-image: linear-gradient(#eeeeee 20%, #ffffff 80%);
box-shadow: 0 1px 0 #fff inset;
}
.chosen-container-active.chosen-with-drop .chosen-single div {
border-left: none;
background: transparent;
}
.chosen-container-active.chosen-with-drop .chosen-single div b {
background-position: -18px 2px;
}
.chosen-container-active .chosen-choices {
border: 1px solid #5897fb;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
.chosen-container-active .chosen-choices li.search-field input[type="text"] {
color: #111 !important;
}
/* @end */
/* @group Disabled Support */
.chosen-disabled {
opacity: 0.5 !important;
cursor: default;
}
.chosen-disabled .chosen-single {
cursor: default;
}
.chosen-disabled .chosen-choices .search-choice .search-choice-close {
cursor: default;
}
/* @end */
/* @group Right to Left */
.chosen-rtl {
text-align: right;
}
.chosen-rtl .chosen-single {
overflow: visible;
padding: 0 8px 0 0;
}
.chosen-rtl .chosen-single span {
margin-right: 0;
margin-left: 26px;
direction: rtl;
}
.chosen-rtl .chosen-single-with-deselect span {
margin-left: 38px;
}
.chosen-rtl .chosen-single div {
right: auto;
left: 3px;
}
.chosen-rtl .chosen-single abbr {
right: auto;
left: 26px;
}
.chosen-rtl .chosen-choices li {
float: right;
}
.chosen-rtl .chosen-choices li.search-field input[type="text"] {
direction: rtl;
}
.chosen-rtl .chosen-choices li.search-choice {
margin: 3px 5px 3px 0;
padding: 3px 5px 3px 19px;
}
.chosen-rtl .chosen-choices li.search-choice .search-choice-close {
right: auto;
left: 4px;
}
.chosen-rtl.chosen-container-single-nosearch .chosen-search,
.chosen-rtl .chosen-drop {
left: 9999px;
}
.chosen-rtl.chosen-container-single .chosen-results {
margin: 0 0 4px 4px;
padding: 0 4px 0 0;
}
.chosen-rtl .chosen-results li.group-option {
padding-right: 15px;
padding-left: 0;
}
.chosen-rtl.chosen-container-active.chosen-with-drop .chosen-single div {
border-right: none;
}
.chosen-rtl .chosen-search input[type="text"] {
padding: 4px 5px 4px 20px;
background: white url('chosen-sprite.png') no-repeat -30px -20px;
background: url('chosen-sprite.png') no-repeat -30px -20px;
direction: rtl;
}
.chosen-rtl.chosen-container-single .chosen-single div b {
background-position: 6px 2px;
}
.chosen-rtl.chosen-container-single.chosen-with-drop .chosen-single div b {
background-position: -12px 2px;
}
/* @end */
/* @group Retina compatibility */
@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-resolution: 144dpi) {
.chosen-rtl .chosen-search input[type="text"],
.chosen-container-single .chosen-single abbr,
.chosen-container-single .chosen-single div b,
.chosen-container-single .chosen-search input[type="text"],
.chosen-container-multi .chosen-choices .search-choice .search-choice-close,
.chosen-container .chosen-results-scroll-down span,
.chosen-container .chosen-results-scroll-up span {
background-image: url('chosen-sprite@2x.png') !important;
background-size: 52px 37px !important;
background-repeat: no-repeat !important;
}
}
/* @end */

File diff suppressed because one or more lines are too long

View File

@ -969,17 +969,43 @@ button.dim:active:before {
position: relative;
width: 100%;
}
.select2-container--default .select2-results__option--highlighted[aria-selected] {
background-color: #1ab394;
color: white;
}
.select2-selection--multiple {
border: 1px solid #e5e6e7 !important;
cursor: text !important;
}
/*.select2-container--classic .select2-selection--multiple:focus {*/
/*border: 1px solid #1ab394;*/
/*}*/
.select2-container--forcus {
border: 1px solid #1AB394 !important;
}
.select2-selection__choice,
.chosen-container-multi .chosen-choices li.search-choice {
background: #f1f1f1;
border: 1px solid #ededed;
border-radius: 2px;
box-shadow: none;
color: #333333;
cursor: default;
line-height: 13px;
margin: 3px 0 3px 5px;
padding: 3px 20px 3px 5px;
position: relative;
background: #f1f1f1 !important;
border: 1px solid #e5e6e7 !important;
/*border: 1px solid #ededed;*/
border-radius: 2px !important;
box-shadow: none !important;
color: #333333 !important;
cursor: default !important;
line-height: 13px !important;
/*margin: 3px 0 3px 5px !important;*/
padding: 3px 20px 3px 5px !important;
position: relative !important;
}
.select2-container--default.select2-container--focus .select2-selection--multiple {
border: 1px solid #1ab394 !important;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3) !important;
}
.chosen-container .chosen-results li.highlighted {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,29 +1,11 @@
{% load static %}
<!-- Mainly scripts -->
<script src="{% static "js/plugins/metisMenu/jquery.metisMenu.js" %}"></script>
{#<script src="{% static "js/plugins/slimscroll/jquery.slimscroll.min.js" %}"></script>#}
{#<script src="{% static "js/bootstrap-dialog.js" %}"></script>#}
{#<script src="{% static "js/mindmup-editabletable.js" %}"></script>#}
{#<script src="{% static "js/plugins/fullcalendar/moment.min.js" %}"></script>#}
{#<script src="{% static "js/plugins/fullcalendar/fullcalendar.min.js" %}"></script>#}
<!-- Custom and plugin javascript -->
<script src="{% static "js/inspinia.js" %}"></script>
{#<script src="{% static "js/plugins/pace/pace.min.js" %}"></script>#}
<!-- Peity -->
{#<script src="{% static "js/plugins/peity/jquery.peity.min.js" %}"></script>#}
{#<script src="{% static "js/demo/peity-demo.js" %}"></script>#}
<script src="{% static "js/base.js" %}"></script>
<!-- pop windows layer-->
{#<script src="{% static "js/layer/layer.js" %}"></script>#}
<!-- highcharts -->
{#<script src="{% static "js/highcharts/highcharts.js" %}"></script>#}
{#<script src="{% static "js/dropzone/dropzone.js" %}"></script>#}
<!-- active menu -->
<script>
var url_array = document.location.pathname.split("/");

View File

@ -1,5 +1,5 @@
<div class="row border-bottom">
<nav class="navbar navbar-static-top" role="navigation" style="margin-bottom: 0">
<nav class="navbar navbar-static-top white-bg" role="navigation" style="margin-bottom: 0">
<div class="navbar-header">
<a class="navbar-minimalize minimalize-styl-2 btn btn-primary " href="#"><i class="fa fa-bars"></i> </a>
<form role="search" class="navbar-form-custom" method="get" action="">

View File

@ -0,0 +1,62 @@
{% extends 'base.html' %}
{% load common_tags %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5> {{ path2 }} </h5>
<div class="ibox-tools">
<a class="collapise-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<div class="">
{# left button add #}
{% block content_left_head %} {% endblock %}
<form id="search_form" method="get" action="" class="pull-right mail-search">
<div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="Search" value="{{ keyword }}">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
搜索
</button>
</div>
</div>
</form>
</div>
<table class="table table-striped table-bordered table-hover " id="editable" >
<thead>
<tr>
{% block table_head %} {% endblock %}
</tr>
</thead>
<tbody>
{% block table_body %} {% endblock %}
</tbody>
</table>
<div class="row">
<div class="col-sm-4">
{# Update batch #}
{% block content_bottom_left %} {% endblock %}
</div>
{% include '_pagination.html' %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}" style="margin: 20px auto 0px">
{{ message|safe }}
</div>
{% endfor %}
{% endif %}

View File

@ -1,15 +1,15 @@
<li id="">
<li id="index">
<a href="">
<i class="fa fa-dashboard"></i> <span class="nav-label">仪表盘</span><span class="label label-info pull-right"></span>
</a>
</li>
<li id="">
<li id="users">
<a href="#">
<i class="fa fa-group"></i> <span class="nav-label">用户管理</span><span class="fa arrow"></span>
</a>
<ul class="nav nav-second-level">
<li class="group"><a href="{% url 'users:user-list' %}">用户列表</a></li>
<li class="user"><a href="{% url 'users:usergroup-list' %}">用户组列表</a></li>
<ul class="nav nav-second-level active">
<li class="user"><a href="{% url 'users:user-list' %}">用户列表</a></li>
<li class="usergroup"><a href="{% url 'users:usergroup-list' %}">用户组列表</a></li>
</ul>
</li>
<li id="">

View File

@ -1,40 +1,38 @@
{% load common_tags %}
{% if is_paginated %}
<div class="row">
<div class="col-sm-6">
<div class="dataTables_info" id="editable_info" role="status" aria-live="polite">
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ paginator.count }} entries
</div>
</div>
<div class="col-sm-6">
<div class="dataTables_paginate paging_simple_numbers" id="editable_paginate">
<ul class="pagination" style="margin-top: 0; float: right">
{% if page_obj.has_previous %}
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="previous">
<a data-page="next" href="?page={{ page_obj.previous_page_number}}"></a>
</li>
{% endif %}
{% if is_paginated %}
<div class="col-sm-4">
<div class="dataTables_info text-center" id="editable_info" role="status" aria-live="polite">
Showing {{ page_obj.start_index }} to {{ page_obj.end_index }} of {{ paginator.count }} entries
</div>
</div>
<div class="col-sm-4">
<div class="dataTables_paginate paging_simple_numbers" id="editable_paginate">
<ul class="pagination" style="margin-top: 0; float: right">
{% if page_obj.has_previous %}
<li class="paginate_button previous" aria-controls="editable" tabindex="0" id="previous">
<a data-page="next" href="?page={{ page_obj.previous_page_number}}"></a>
</li>
{% endif %}
{% for page in paginator.num_pages|pagination_range:page_obj.number %}
{% if page == page_obj.number %}
<li class="paginate_button active" aria-controls="editable" tabindex="0">
{% else %}
<li class="paginate_button" aria-controls="editable" tabindex="0">
{% endif %}
<a class="page" href="?page={{ page }}" title="第{{ page }}页">{{ page }}</a>
</li>
{% endfor %}
{% for page in paginator.num_pages|pagination_range:page_obj.number %}
{% if page == page_obj.number %}
<li class="paginate_button active" aria-controls="editable" tabindex="0">
{% else %}
<li class="paginate_button" aria-controls="editable" tabindex="0">
{% endif %}
<a class="page" href="?page={{ page }}" title="第{{ page }}页">{{ page }}</a>
</li>
{% endfor %}
{% if page_obj.has_next %}
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="next">
<a data-page="next" href="?page={{ page_obj.next_page_number }}"></a>
</li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endif %}
{% if page_obj.has_next %}
<li class="paginate_button next" aria-controls="editable" tabindex="0" id="next">
<a data-page="next" href="?page={{ page_obj.next_page_number }}"></a>
</li>
{% endif %}
</ul>
</div>
</div>
{% endif %}
<script>
function sleep(n) { //n表示的毫秒数
var start = new Date().getTime();

View File

@ -20,6 +20,7 @@
{% include '_left_side_bar.html' %}
<div id="page-wrapper" class="gray-bg">
{% include '_header_bar.html' %}
{% include '_message.html' %}
{% block content %}{% endblock %}
{% include '_footer.html' %}
</div>

View File

@ -21,7 +21,7 @@ class UserAddForm(ModelForm):
}
widgets = {
'groups': forms.SelectMultiple(attrs={'class': 'chosen-select', 'data-placeholder': '请选择用户组'}),
'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': '请选择用户组'}),
}
@ -29,19 +29,18 @@ class UserUpdateForm(ModelForm):
class Meta:
model = User
fields = [
'name', 'email', 'groups', 'wechat', 'avatar',
'name', 'email', 'groups', 'wechat',
'phone', 'enable_otp', 'role', 'date_expired', 'comment',
]
help_texts = {
'username': '* required',
'name': '* required',
'email': '* required',
'groups': '* required'
}
widgets = {
'groups': forms.SelectMultiple(attrs={'class': 'chosen-select', 'data-placeholder': '请选择用户组'}),
'groups': forms.SelectMultiple(attrs={'class': 'select2', 'data-placeholder': '请选择用户组'}),
}

View File

@ -7,6 +7,7 @@ from django.contrib.auth.hashers import make_password
from django.utils import timezone
from django.db import models
from django.contrib.auth.models import AbstractUser, Permission
from django.db import OperationalError
class Role(models.Model):
@ -23,6 +24,12 @@ class Role(models.Model):
def __unicode__(self):
return self.name
def delete(self, using=None, keep_parents=False):
if self.user_set.all().count() > 0:
raise OperationalError('Role %s has some member, should not be delete.' % self.name)
else:
return super(Role, self).delete(using=using, keep_parents=keep_parents)
class Meta:
db_table = 'role'
@ -56,7 +63,7 @@ class UserGroup(models.Model):
@classmethod
def initial(cls):
group_or_create = cls.objects.get_or_create(name='All', comment='Default user group for all user',
group_or_create = cls.objects.get_or_create(name='Default', comment='Default user group for all user',
created_by='System')
return group_or_create[0]
@ -93,7 +100,7 @@ class User(AbstractUser):
phone = models.CharField(max_length=20, blank=True, verbose_name='手机号')
enable_otp = models.BooleanField(default=False, verbose_name='启用二次验证')
secret_key_otp = models.CharField(max_length=16, blank=True)
role = models.ForeignKey(Role, on_delete=models.PROTECT, verbose_name='角色')
role = models.ForeignKey(Role, on_delete=models.SET('None'), verbose_name='角色')
private_key = models.CharField(max_length=5000, blank=True, verbose_name='ssh私钥') # ssh key max length 4096 bit
public_key = models.CharField(max_length=1000, blank=True, verbose_name='公钥')
comment = models.TextField(max_length=200, blank=True, verbose_name='描述')
@ -123,11 +130,13 @@ class User(AbstractUser):
# If user not set name, it's default equal username
if not self.name:
self.name = self.username
super(User, self).save(args, **kwargs)
super(User, self).save(*args, **kwargs)
# Set user default group 'All'
# Todo: It's have bug
group = UserGroup.initial()
self.groups.add(group)
if group not in self.groups.all():
self.groups.add(group)
# super(User, self).save(*args, **kwargs)
class Meta:
db_table = 'user'

View File

@ -2,9 +2,9 @@
{% load static %}
{% load bootstrap %}
{% block custom_head_css_js %}
<link href="{% static "css/plugins/chosen/chosen.css" %}" rel="stylesheet">
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
<link href="{% static "css/plugins/datepicker/datepicker3.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/chosen/chosen.jquery.min.js" %}"></script>
{% endblock %}
{% block content %}
@ -41,13 +41,14 @@
<div class="hr-line-dashed"></div>
<h3>角色安全</h3>
{{ form.role|bootstrap_horizontal }}
<div class="form-group" id="date_5">
<div class="form-group {% if form.date_expired.errors %} has-error {% endif %}" id="date_5">
<label for="{{ form.date_expired.id_for_label }}" class="col-sm-2 control-label">{{ form.date_expired.label }}</label>
<div class="col-sm-9">
<div class="input-group date">
<span class="input-group-addon"><i class="fa fa-calendar"></i></span>
<input id="{{ form.date_expired.id_for_label }}" name="{{ form.date_expired.html_name }}" type="text" class="form-control" value="{{ form.date_expired.value|date:'m/d/Y' }}">
</div>
<span class="help-block ">{{ form.date_expired.errors }}</span>
</div>
</div>
<div class="form-group">
@ -79,16 +80,7 @@
<script src="{% static 'js/plugins/datapicker/bootstrap-datepicker.js' %}"></script>
<script>
$(document).ready(function () {
var config = {
'.chosen-select' : {},
'.chosen-select-deselect' : {allow_single_deselect:true},
'.chosen-select-no-single' : {disable_search_threshold:10},
'.chosen-select-no-results': {no_results_text:'Oops, nothing found!'},
'.chosen-select-width' : {width:"95%"}
};
for (var selector in config) {
$(selector).chosen(config[selector]);
}
$('.select2').select2();
$('.input-group.date').datepicker({
todayBtn: "linked",

View File

@ -4,8 +4,8 @@
{% load static %}
{% block custom_head_css_js %}
<link href="{% static "css/plugins/chosen/chosen.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/chosen/chosen.jquery.min.js" %}"></script>
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
{% endblock %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
@ -192,7 +192,7 @@
<form>
<tr>
<td colspan="2" class="no-borders">
<select data-placeholder="选择用户组" class="chosen-select" style="width: 100%" multiple="" tabindex="4">
<select data-placeholder="选择用户组" class="select2" style="width: 100%" multiple="" tabindex="4">
{% for group in groups %}
<option value="{{ group.id }}">{{ group.name }}</option>
{% endfor %}
@ -208,7 +208,7 @@
{% for group in user.groups.all %}
<tr>
<td width="40%"><b style="font-size: medium">{{ group.name }}</b></td>
<td ><b>{{ group.name }}</b></td>
<td>
<button class="btn btn-danger btn-sm" type="button" style="float: right;"><i class="fa fa-minus"></i></button>
</td>
@ -230,16 +230,7 @@
{% block custom_foot_js %}
<script>
$(document).ready(function () {
var config = {
'.chosen-select' : {},
'.chosen-select-deselect' : {allow_single_deselect:true},
'.chosen-select-no-single' : {disable_search_threshold:10},
'.chosen-select-no-results': {no_results_text:'Oops, nothing found!'},
'.chosen-select-width' : {width:"95%"}
};
for (var selector in config) {
$(selector).chosen(config[selector]);
}
$('.select2').select2();
})
</script>
{% endblock %}

View File

@ -0,0 +1,92 @@
{% extends '_list_base.html' %}
{% load common_tags %}
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5> 查看用户 </h5>
<div class="ibox-tools">
<a class="collapise-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<div class="">
<a href="{% url 'users:user-add' %}" class="btn btn-sm btn-primary "> 添加用户 </a>
<a id="del_btn" class="btn btn-sm btn-danger "> 删除所选 </a>
<form id="search_form" method="get" action="" class="pull-right mail-search">
<div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="用户名或姓名" value="{{ keyword }}">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
搜索
</button>
</div>
</div>
</form>
</div>
<table class="table table-striped table-bordered table-hover " id="editable" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" onclick="checkAll('check_all', 'checked')">
</th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=name">姓名</a></th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=username">用户名</a></th>
<th class="text-center">角色</th>
<th class="text-center">用户组</th>
<th class="text-center">资产数量</th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=date_expired">有效</a></th>
<th class="text-center"></th>
</tr>
</thead>
<tbody>
{% for user in user_list %}
<tr class="gradeX">
<td class="text-center">
<input type="checkbox" name="checked" value="{{ user.id }}">
</td>
<td class="text-center">
<a href="{% url 'users:user-detail' pk=user.id %}">
{{ user.name }}
</a>
</td>
<td class="text-center">{{ user.username }}</td>
<td class="text-center">{{ user.role.name }}</td>
<td class="text-center" title="{% for user_group in user.group.all %} {{ user_group.name }} {% endfor %}"> {{ user.groups.all|join_queryset_attr:"name" }} </td>
<th class="text-center">{{ user.name }}</th>
<td class="text-center">
{% if user.is_expired %}
<i class="fa fa-times text-danger"></i>
{% else %}
<i class="fa fa-check text-navy"></i>
{% endif %}
</td>
<td class="text-center">
<a href="{% url 'users:user-edit' pk=user.id %}" class="btn btn-xs btn-info">编辑</a>
<a href="{% url 'users:user-delete' pk=user.id %}" class="btn btn-xs btn-danger del {% if user.username == 'admin' %} disabled {% endif %}">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="row">
<div class="col-sm-6">
</div>
{% include '_pagination.html' %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -1,91 +1,71 @@
{% extends 'base.html' %}
{% extends '_list_base.html' %}
{% load common_tags %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5> 查看用户 </h5>
<div class="ibox-tools">
<a class="collapise-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
<div class="ibox-content">
<div class="">
<a href="{% url 'users:user-add' %}" class="btn btn-sm btn-primary "> 添加用户 </a>
<a id="del_btn" class="btn btn-sm btn-danger "> 删除所选 </a>
<form id="search_form" method="get" action="" class="pull-right mail-search">
<div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="用户名或姓名" value="{{ keyword }}">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
搜索
</button>
</div>
</div>
</form>
</div>
<table class="table table-striped table-bordered table-hover " id="editable" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" onclick="checkAll('check_all', 'checked')">
</th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=name">姓名</a></th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=username">用户名</a></th>
<th class="text-center">角色</th>
<th class="text-center">用户组</th>
<th class="text-center">资产数量</th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=date_expired">有效</a></th>
<th class="text-center"></th>
</tr>
</thead>
<tbody>
{% for user in user_list %}
<tr class="gradeX">
<td class="text-center">
<input type="checkbox" name="checked" value="{{ user.id }}">
</td>
<td class="text-center">
<a href="{% url 'users:user-detail' pk=user.id %}">
{{ user.name }}
</a>
</td>
<td class="text-center">{{ user.username }}</td>
<td class="text-center">{{ user.role.name }}</td>
<td class="text-center" title="{% for user_group in user.group.all %} {{ user_group.name }} {% endfor %}"> {{ user.groups.all|join_queryset_attr:"name" }} </td>
<th class="text-center">{{ user.name }}</th>
<td class="text-center">
{% if user.is_expired %}
<i class="fa fa-times text-danger"></i>
{% else %}
<i class="fa fa-check text-navy"></i>
{% endif %}
</td>
<td class="text-center">
<a href="{% url 'users:user-edit' pk=user.id %}" class="btn btn-xs btn-info">编辑</a>
<a href="{% url 'users:user-delete' pk=user.id %}" class="btn btn-xs btn-danger del {% if user.username == 'admin' %} disabled {% endif %}">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include '_pagination.html' %}
</div>
</div>
</div>
</div>
</div>
{% block content_left_head %}
<a href="{% url 'users:user-add' %}" class="btn btn-sm btn-primary "> 添加用户 </a>
{# <a id="del_btn" class="btn btn-sm btn-danger "> 删除所选 </a>#}
{% endblock %}
{% block table_head %}
<th class="text-center">
<input type="checkbox" id="check_all" onclick="checkAll('check_all', 'checked')">
</th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=name">姓名</a></th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=username">用户名</a></th>
<th class="text-center">角色</th>
<th class="text-center">用户组</th>
<th class="text-center">资产数量</th>
<th class="text-center"><a href="{% url 'users:user-list' %}?sort=date_expired">有效</a></th>
<th class="text-center"></th>
{% endblock %}
{% block table_body %}
{% for user in user_list %}
<tr class="gradeX">
<td class="text-center">
<input type="checkbox" name="checked" value="{{ user.id }}">
</td>
<td class="text-center">
<a href="{% url 'users:user-detail' pk=user.id %}">
{{ user.name }}
</a>
</td>
<td class="text-center">{{ user.username }}</td>
<td class="text-center">{{ user.role.name }}</td>
<td class="text-center" title="{% for user_group in user.group.all %} {{ user_group.name }} {% endfor %}"> {{ user.groups.all|join_queryset_attr:"name" }} </td>
<th class="text-center">{{ user.name }}</th>
<td class="text-center">
{% if user.is_expired %}
<i class="fa fa-times text-danger"></i>
{% else %}
<i class="fa fa-check text-navy"></i>
{% endif %}
</td>
<td class="text-center">
<a href="{% url 'users:user-edit' pk=user.id %}" class="btn btn-xs btn-info">编辑</a>
<a href="{% url 'users:user-delete' pk=user.id %}" class="btn btn-xs btn-danger del {% if user.username == 'admin' %} disabled {% endif %}">删除</a>
</td>
</tr>
{% endfor %}
{% endblock %}
{% block content_bottom_left %}
<form id="" method="get" action="" class=" mail-search">
<div class="input-group">
<select class="form-control m-b" style="width: auto">
<option>批量删除</option>
<option>批量更新</option>
<option>批量禁用</option>
<option>批量导出</option>
</select>
<div class="input-group-btn pull-left" style="padding-left: 5px;">
<button id='search_btn' type="submit" style="height: 32px;" class="btn btn-sm btn-primary">
确认
</button>
</div>
</div>
</form>
{% endblock %}

View File

@ -2,8 +2,8 @@
{% load static %}
{% load bootstrap %}
{% block custom_head_css_js %}
<link href="{% static "css/plugins/chosen/chosen.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/chosen/chosen.jquery.min.js" %}"></script>
<link href="{% static "css/plugins/select2/select2.min.css" %}" rel="stylesheet">
<script src="{% static "js/plugins/select2/select2.full.min.js" %}"></script>
{% endblock %}
{% block content %}
@ -32,8 +32,8 @@
<div class="form-group">
<label for="users" class="col-sm-2 control-label">用户</label>
<div class="col-sm-8">
<select name="users" id="users" data-placeholder="选择用户" class="chosen-select form-control m-b" multiple tabindex="2">
<div class="col-sm-9">
<select name="users" id="users" data-placeholder="选择用户" class="select2 form-control m-b" multiple tabindex="2">
{% for user in users %}
<option value="{{ user.id }}">{{ user.name }}</option>
{% endfor %}
@ -59,16 +59,7 @@
{% block custom_foot_js %}
<script>
$(document).ready(function () {
var config = {
'.chosen-select' : {},
'.chosen-select-deselect' : {allow_single_deselect:true},
'.chosen-select-no-single' : {disable_search_threshold:10},
'.chosen-select-no-results': {no_results_text:'Oops, nothing found!'},
'.chosen-select-width' : {width:"95%"}
};
for (var selector in config) {
$(selector).chosen(config[selector]);
}
$('.select2').select2();
})
</script>
{% endblock %}

View File

@ -1,81 +1,58 @@
{% extends 'base.html' %}
{% extends '_list_base.html' %}
{% load common_tags %}
{% block content %}
<div class="wrapper wrapper-content animated fadeInRight">
<div class="row">
<div class="col-sm-12">
<div class="ibox float-e-margins">
<div class="ibox-title">
<h5> 查看用户组 </h5>
<div class="ibox-tools">
<a class="collapise-link">
<i class="fa fa-chevron-up"></i>
</a>
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
<i class="fa fa-wrench"></i>
</a>
<a class="close-link">
<i class="fa fa-times"></i>
</a>
</div>
</div>
{% block content_left_head %}
<a href="{% url 'users:usergroup-add' %}" class="btn btn-sm btn-primary "> 添加用户组 </a>
{% endblock %}
<div class="ibox-content">
<div class="">
<a href="{% url 'users:usergroup-add' %}" class="btn btn-sm btn-primary "> 添加用户组 </a>
<a id="del_btn" class="btn btn-sm btn-danger "> 删除所选 </a>
<form id="search_form" method="get" action="" class="pull-right mail-search">
<div class="input-group">
<input type="text" class="form-control input-sm" name="keyword" placeholder="名称" value="{{ keyword }}">
<div class="input-group-btn">
<button id='search_btn' type="submit" class="btn btn-sm btn-primary">
搜索
</button>
</div>
</div>
</form>
</div>
{% block table_head %}
<th class="text-center">
<input type="checkbox" id="check_all" onclick="checkAll('check_all', 'checked')">
</th>
<th class="text-center"><a href="{% url 'users:usergroup-list' %}?sort=name">名称</a></th>
<th class="text-center">用户数量</th>
<th class="text-center">资产数量</th>
<th class="text-center">描述</th>
<th class="text-center"></th>
{% endblock %}
<table class="table table-striped table-bordered table-hover " id="editable" >
<thead>
<tr>
<th class="text-center">
<input type="checkbox" id="check_all" onclick="checkAll('check_all', 'checked')">
</th>
<th class="text-center"><a href="{% url 'users:usergroup-list' %}?sort=name">名称</a></th>
<th class="text-center">用户数量</th>
<th class="text-center">资产数量</th>
<th class="text-center">描述</th>
<th class="text-center"></th>
</tr>
</thead>
<tbody>
{% for usergroup in usergroup_list %}
<tr class="gradeX">
<td class="text-center">
<input type="checkbox" name="checked" value="{{ usergroup.id }}">
</td>
<td class="text-center">
<a href="{% url 'users:usergroup-detail' pk=usergroup.id %}">
{{ usergroup.name }}
</a>
</td>
<td class="text-center">{{ usergroup.user_set.all|length }}</td>
<td class="text-center">数量</td>
<th class="text-center">{{ usergroup.comment|truncatewords:8 }}</th>
<td class="text-center">
<a href="{% url 'users:usergroup-edit' pk=usergroup.id %}" class="btn btn-xs btn-info">编辑</a>
<a href="{% url 'users:usergroup-delete' pk=usergroup.id %}" class="btn btn-xs btn-danger del">删除</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% include '_pagination.html' %}
</div>
{% block table_body %}
{% for usergroup in usergroup_list %}
<tr class="gradeX">
<td class="text-center">
<input type="checkbox" name="checked" value="{{ usergroup.id }}">
</td>
<td class="text-center">
<a href="{% url 'users:usergroup-detail' pk=usergroup.id %}">
{{ usergroup.name }}
</a>
</td>
<td class="text-center">{{ usergroup.user_set.all|length }}</td>
<td class="text-center">数量</td>
<th class="text-center">{{ usergroup.comment|truncatewords:8 }}</th>
<td class="text-center">
<a href="{% url 'users:usergroup-edit' pk=usergroup.id %}" class="btn btn-xs btn-info">编辑</a>
<a href="{% url 'users:usergroup-delete' pk=usergroup.id %}" class="btn btn-xs btn-danger del">删除</a>
</td>
</tr>
{% endfor %}
{% endblock %}
{% block content_bottom_left %}
<form id="" method="get" action="" class=" mail-search">
<div class="input-group">
<select class="form-control m-b" style="width: auto">
<option>批量删除</option>
<option>批量更新</option>
<option>批量禁用</option>
<option>批量导出</option>
</select>
<div class="input-group-btn pull-left" style="padding-left: 5px;">
<button id='search_btn' type="submit" style="height: 32px;" class="btn btn-sm btn-primary">
确认
</button>
</div>
</div>
</div>
</div>
</form>
{% endblock %}

View File

@ -1,95 +0,0 @@
# ~*~ coding: utf-8 ~*~
from random import choice
import forgery_py
from django.utils import timezone
from django.test import TestCase, Client, TransactionTestCase
from django.test.utils import setup_test_environment
from django.db import IntegrityError, transaction
from .models import User, UserGroup, Role, init_all_models
setup_test_environment()
client = Client()
def create_usergroup(name):
pass
def get_random_usergroup():
pass
def create_user(username, name, email, groups):
pass
def gen_username():
return forgery_py.internet.user_name(True)
def gen_email():
return forgery_py.internet.email_address()
def gen_name():
return forgery_py.name.full_name()
class UserModelTest(TransactionTestCase):
def setUp(self):
init_all_models()
def test_user_duplicate(self):
# 创建一个基准测试用户
role = choice(Role.objects.all())
user = User(name='test', username='test', email='test@email.org', role=role)
user.save()
# 创建一个姓名一致的用户, 应该创建成功
user1 = User(name='test', username=gen_username(), password_raw=gen_username(),
email=gen_email(), role=role)
try:
user1.save()
user1.delete()
except IntegrityError:
self.assertTrue(0, 'Duplicate <name> not allowed.')
# 创建一个用户名一致的用户, 应该创建不成功
user2 = User(username='test', email=gen_email(), role=role)
try:
user2.save()
self.assertTrue(0, 'Duplicate <username> allowed.')
except IntegrityError:
pass
# 创建一个Email一致的用户,应该创建不成功
user3 = User(username=gen_username(), email='test@email.org', role=role)
try:
user3.save()
self.assertTrue(0, 'Duplicate <email> allowed.')
except IntegrityError:
pass
def test_user_was_expired(self):
role = choice(Role.objects.all())
date = timezone.now() - timezone.timedelta(days=1)
user = User(name=gen_name(), username=gen_username(),
email=gen_email(), role=role, date_expired=date)
self.assertTrue(user.is_expired())
def test_user_with_default_group(self):
role = choice(Role.objects.all())
user = User(username=gen_username(), email=gen_email(), role=role)
user.save()
self.assertEqual(user.groups.count(), 1)
self.assertEqual(user.groups.first().name, 'All')
class UserListViewTests(TestCase):
pass

View File

24
apps/users/tests/base.py Normal file
View File

@ -0,0 +1,24 @@
# ~*~ coding: utf-8 ~*~
from random import choice
import forgery_py
from users.models import User, UserGroup, Role, init_all_models
def gen_username():
return forgery_py.internet.user_name(True)
def gen_email():
return forgery_py.internet.email_address()
def gen_name():
return forgery_py.name.full_name()
def get_role():
role = choice(Role.objects.all())
return role

View File

@ -0,0 +1,109 @@
# ~*~ coding: utf-8 ~*~
from django.utils import timezone
from django.shortcuts import reverse
from django.test import TestCase, Client, TransactionTestCase
from django.db import IntegrityError
from users.models import User, UserGroup, Role, init_all_models
from django.contrib.auth.models import Permission
from .base import gen_name, gen_username, gen_email, get_role
class UserModelTest(TransactionTestCase):
def setUp(self):
init_all_models()
# 创建一个用户用于测试
role = get_role()
user = User(name='test', username='test', email='test@email.org', role=role)
user.save()
def test_initial(self):
self.assertEqual(User.objects.all().count(), 2)
self.assertEqual(Role.objects.all().count(), 3)
self.assertEqual(UserGroup.objects.all().count(), 1)
@property
def role(self):
return get_role()
# 创建一个姓名一致的用户, 应该创建成功
def test_user_name_duplicate(self):
user1 = User(name='test', username=gen_username(), password_raw=gen_username(),
email=gen_email(), role=self.role)
try:
user1.save()
user1.delete()
except IntegrityError:
self.assertTrue(0, 'Duplicate <name> not allowed.')
# 创建一个用户名一致的用户, 应该创建不成功
def test_user_username_duplicate(self):
user2 = User(username='test', email=gen_email(), role=self.role)
with self.assertRaises(IntegrityError):
user2.save()
# 创建一个Email一致的用户,应该创建不成功
def test_user_email_duplicate(self):
user3 = User(username=gen_username(), email='test@email.org', role=self.role)
with self.assertRaises(IntegrityError):
user3.save()
# 用户过期测试
def test_user_was_expired(self):
date = timezone.now() - timezone.timedelta(days=1)
user = User(name=gen_name(), username=gen_username(),
email=gen_email(), role=self.role, date_expired=date)
self.assertTrue(user.is_expired())
# 测试用户默认会输入All用户组
def test_user_with_default_group(self):
role = get_role()
user = User(username=gen_username(), email=gen_email(), role=role)
user.save()
self.assertEqual(user.groups.count(), 1)
self.assertEqual(user.groups.first().name, 'Default')
def test_user_password_authenticated(self):
password = gen_username() * 3
user = User(username=gen_username(), password_raw=password, role=self.role)
user.save()
self.assertTrue(user.check_password(password))
self.assertFalse(user.check_password(password*2))
def tearDown(self):
User.objects.all().delete()
UserGroup.objects.all().delete()
Role.objects.all().delete()
class RoleModelTestCase(TransactionTestCase):
def setUp(self):
Role.objects.all().delete()
Role.initial()
def test_role_initial(self):
self.assertEqual(Role.objects.all().count(), 3)
def test_create_new_role(self):
role = Role(name=gen_name(), comment=gen_name()*3)
role.save()
role.permissions = Permission.objects.all()
role.save()
self.assertEqual(Role.objects.count(), 4)
role = Role.objects.last()
self.assertEqual(role.permissions.all().count(), Permission.objects.all().count())
class UserGroupModelTestCase(TransactionTestCase):
pass

View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
from users.models import User, UserGroup, Role, init_all_models
from django.shortcuts import reverse
from django.test import TestCase, Client, TransactionTestCase
from .base import gen_username, gen_name, gen_email, get_role
class UserListViewTests(TransactionTestCase):
def setUp(self):
init_all_models()
def test_a_new_user_in_list(self):
username = gen_username()
user = User(username=username, email=gen_email(), role=get_role())
user.save()
response = self.client.get(reverse('users:user-list'))
self.assertContains(response, username)
def test_list_view_with_admin_user(self):
response = self.client.get(reverse('users:user-list'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Admin')
self.assertEqual(response.context['user_list'].count(), User.objects.all().count())
def test_pagination(self):
User.generate_fake(count=20)
response = self.client.get(reverse('users:user-list'))
self.assertEqual(response.context['is_paginated'], True)
class UserAddTests(TestCase):
def setUp(self):
init_all_models()
def test_add_a_new_user(self):
username = gen_username()
data = {
'username': username,
'comment': '',
'name': gen_name(),
'email': gen_email(),
'groups': [UserGroup.objects.first().id, ],
'role': get_role().id,
'date_expired': '2086-08-06 19:12:22',
}
response = self.client.post(reverse('users:user-add'), data)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['location'], reverse('users:user-list'))
response = self.client.get(reverse('users:user-list'))
self.assertContains(response, username)

View File

@ -1,11 +1,14 @@
# ~*~ coding: utf-8 ~*~
from django.shortcuts import get_object_or_404
from __future__ import unicode_literals
from django.shortcuts import get_object_or_404, reverse
from django.urls import reverse_lazy
from django.db.models import Q
from django.views.generic.list import ListView
from django.views.generic.edit import CreateView, DeleteView, UpdateView
from django.views.generic.detail import DetailView
from django.contrib.messages.views import SuccessMessageMixin
from django.conf import settings
from .models import User, UserGroup, Role
@ -37,11 +40,12 @@ class UserListView(ListView):
return context
class UserAddView(CreateView):
class UserAddView(SuccessMessageMixin, CreateView):
model = User
form_class = UserAddForm
template_name = 'users/user_add.html'
success_url = reverse_lazy('users:user-list')
success_message = '添加用户 <a href="%s">%s</a> 成功 .'
def get_context_data(self, **kwargs):
context = super(UserAddView, self).get_context_data(**kwargs)
@ -54,6 +58,12 @@ class UserAddView(CreateView):
user.save()
return super(UserAddView, self).form_valid(form)
def get_success_message(self, cleaned_data):
return self.success_message % (
reverse_lazy('users:user-detail', kwargs={'pk': self.object.pk}),
self.object.name,
)
class UserUpdateView(UpdateView):
model = User
@ -72,6 +82,10 @@ class UserUpdateView(UpdateView):
user.set_password(password)
return super(UserUpdateView, self).form_valid(form)
def form_invalid(self, form):
print(form.errors)
return super(UserUpdateView, self).form_invalid(form)
class UserDeleteView(DeleteView):
model = User
@ -93,7 +107,7 @@ class UserDetailView(DetailView):
class UserGroupListView(ListView):
model = UserGroup
paginate_by = 20
paginate_by = settings.CONFIG.DISPLAY_PER_PAGE
context_object_name = 'usergroup_list'
template_name = 'users/usergroup_list.html'
ordering = '-date_added'