Skip to content

Commit 301993c

Browse files
committedJan 22, 2015
add “must change password”; prepare #33
Signed-off-by: Chris Warrick <kwpolska@gmail.com>
1 parent e437ec2 commit 301993c

12 files changed

+309
-183
lines changed
 

‎coil/data/coil_assets/css/coil.css

+4
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ table.users .uid {
9090
text-align: center;
9191
}
9292

93+
.perm-descr {
94+
font-size: x-small;
95+
}
96+
9397
/* login (copied from Bootstrap example) */
9498

9599
.form-signin {

‎coil/data/templates/jinja/coil_users.tmpl

+43-41
Original file line numberDiff line numberDiff line change
@@ -41,57 +41,59 @@ $('#deleteModal').on('show.bs.modal', function (event) {
4141

4242
<table class="table table-hover users" style="table-layout: fixed;">
4343
<thead><tr>
44-
<th class="uid">#</th>
45-
<th class="username">Username</th>
46-
<th class="realname">Real name</th>
47-
<th class="email">E-mail address</th>
48-
<th class="is_admin">Admin</th>
49-
<th class="actions">Actions</th>
44+
<th class="uid">#</th>
45+
<th class="username">Username</th>
46+
<th class="realname">Real name</th>
47+
<th class="email">E-mail address</th>
48+
<th class="is_admin">Admin</th>
49+
<th class="actions">Actions</th>
5050
</tr></thead>
5151
{% for uid, user in USERS %}
5252
{% if user.active %}
5353
<tr>
5454
{% else %}
5555
<tr class="danger inactive-user">
5656
{% endif %}
57-
<td class="uid">{{ uid }}</td>
58-
<td class="username">{{ user.username }}</td>
59-
<td class="realname">{{ user.realname }}</td>
60-
<td class="email">{{ user.email }}</td>
61-
<td class="actions">
62-
{% if user.is_admin %}
63-
<i class="fa fa-check"></i>
64-
{% else %}
65-
<i class="fa fa-times"></i>
66-
{% endif %}
67-
</td>
68-
<td class="actions">
69-
<form action="{{ url_for('acp_users_edit') }}" method="POST">{{ editform.csrf_token }}
70-
<input type="hidden" name="uid" value="{{ uid }}">
71-
<input type="hidden" name="action" value="edit">
72-
<div class="btn-group" role="group">
73-
{% if user.active %}
74-
<button type="submit" class="btn btn-sm btn-info" title="Edit"><i class="fa fa-pencil fa-fw"></i> Edit</button>
75-
{% if user.uid == current_user.uid %}
76-
<button type="button" class="btn btn-sm btn-danger" title="Delete" disabled><i class="fa fa-trash fa-fw"></i> Delete</button>
77-
{% else %}
78-
<button type="button" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#deleteModal" data-username="{{ user.username }}" data-uid="{{ uid }}" data-direction="del" title="Delete"><i class="fa fa-trash fa-fw"></i> Delete</button>
79-
{% endif %}
80-
{% else %}
81-
<button type="button" class="btn btn-sm btn-info" title="Edit" disabled><i class="fa fa-pencil fa-fw"></i> Edit</button>
82-
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#deleteModal" data-username="{{ user.username }}" data-uid="{{ uid }}" data-direction="undel" title="Undelete"><i class="fa fa-trash-o fa-fw"></i> Undelete</button>
83-
{% endif %}
84-
</div>
85-
</form>
57+
<td class="uid">{{ uid }}</td>
58+
<td class="username">{{ user.username }}</td>
59+
<td class="realname">{{ user.realname }}</td>
60+
<td class="email">{{ user.email }}</td>
61+
<td class="actions">
62+
{% if user.is_admin %}
63+
<i class="fa fa-check"></i>
64+
{% else %}
65+
<i class="fa fa-times"></i>
66+
{% endif %}
67+
</td>
68+
<td class="actions">
69+
<form action="{{ url_for('acp_users_edit') }}" method="POST">{{ editform.csrf_token }}
70+
<input type="hidden" name="uid" value="{{ uid }}">
71+
<input type="hidden" name="action" value="edit">
72+
<div class="btn-group" role="group">
73+
{% if user.active %}
74+
<button type="submit" class="btn btn-sm btn-info" title="Edit"><i class="fa fa-pencil fa-fw"></i> Edit</button>
75+
{% if user.uid == current_user.uid %}
76+
<button type="button" class="btn btn-sm btn-danger" title="Delete" disabled><i class="fa fa-trash fa-fw"></i> Delete</button>
77+
{% else %}
78+
<button type="button" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#deleteModal" data-username="{{ user.username }}" data-uid="{{ uid }}" data-direction="del" title="Delete"><i class="fa fa-trash fa-fw"></i> Delete</button>
79+
{% endif %}
80+
{% else %}
81+
<button type="button" class="btn btn-sm btn-info" title="Edit" disabled><i class="fa fa-pencil fa-fw"></i> Edit</button>
82+
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#deleteModal" data-username="{{ user.username }}" data-uid="{{ uid }}" data-direction="undel" title="Undelete"><i class="fa fa-trash-o fa-fw"></i> Undelete</button>
83+
{% endif %}
84+
</div>
85+
</form>
8686
</tr>
8787
{% endfor %}
8888
<tr><form action="{{ url_for('acp_users_edit') }}" method="POST">{{ editform.csrf_token }}
89-
<td></td>
90-
<td><input name="username" placeholder="New user" class="form-control"></td>
91-
<td><input name="action" value="new" type="hidden"></td>
92-
<td></td>
93-
<td></td>
94-
<td><button type="submit" class="btn btn-sm btn-primary"><i class="fa fa-plus-square fa-fw"></i> Create</button></td>
89+
<td class="uid"><strong>Create:</strong></td>
90+
<td colspan="4"><input name="username" placeholder="New user" class="form-control input-sm"></td>
91+
<td><button type="submit" class="btn btn-sm btn-primary"><i class="fa fa-plus-square fa-fw"></i> Create</button></td>
92+
</form></tr>
93+
<tr><form action="{{ url_for('acp_users_import') }}" method="POST" enctype="multipart/form-data">{{ importform.csrf_token }}
94+
<td class="uid"><strong>Import:</strong></td>
95+
<td colspan="4"><input name="tsv" type="file" required></td>
96+
<td><button type="submit" class="btn btn-sm btn-primary"><i class="fa fa-file-text-o fa-fw"></i> Import TSV</button></td>
9597
</form></tr>
9698
</table>
9799

‎coil/data/templates/jinja/coil_users_edit.tmpl

+9
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@
9595
{% endif %}
9696
> User is an administrator
9797
</label></div>
98+
<div class="checkbox"><label><input type="checkbox" name="must_change_password"
99+
{% if user.must_change_password %}
100+
checked
101+
{% endif %}
102+
{% if user.uid == current_user.uid %}
103+
disabled
104+
{% endif %}
105+
> User must change password on next login
106+
</label></div>
98107
<div class="checkbox"><label><input type="checkbox" name="can_edit_all_posts"
99108
{% if user.can_edit_all_posts %}
100109
checked

‎coil/data/templates/jinja/coil_users_permissions.tmpl

+15-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{% extends 'base.tmpl' %}
33
{% block extra_js %}
44
<script>
5-
var PERMISSIONS = {{ json.dumps(PERMISSIONS) }};
5+
var PERMISSIONS = {{ json.dumps(PERMISSIONS_E) }};
66
var UIDS = {{ json.dumps(UIDS) }};
77
var current_uid = {{ current_user.uid }};
88
$(document).ready(function() {
@@ -54,35 +54,39 @@ $(document).ready(function() {
5454
{% endif %}
5555

5656
<form action="{{ url_for('acp_users_permissions') }}" method="POST">
57-
<table class="table table-hover users" style="table-layout: fixed;">
57+
<table class="table table-hover users">
5858
<thead><tr>
5959
<th class="uid">#</th>
6060
<th class="username">Username</th>
61-
<th class="perm is_active">Active<br>
61+
<th class="perm is_active"><div class="perm-descr">Active</div>
6262
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="is_active"><i class="fa fa-check-square-o fa-fw"></i></button>
6363
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="is_active"><i class="fa fa-square-o fa-fw"></i></button>
6464
</th>
65-
<th class="perm is_admin">Admin<br>
65+
<th class="perm is_admin"><div class="perm-descr">Admin</div>
6666
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="is_admin"><i class="fa fa-check-square-o fa-fw"></i></button>
6767
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="is_admin"><i class="fa fa-square-o fa-fw"></i></button>
6868
</th>
69-
<th class="perm can_edit_all_posts">Can all posts<br>
69+
<th class="perm must_change_password"><div class="perm-descr">Must change password</div>
70+
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="must_change_password"><i class="fa fa-check-square-o fa-fw"></i></button>
71+
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="must_change_password"><i class="fa fa-square-o fa-fw"></i></button>
72+
</th>
73+
<th class="perm can_edit_all_posts"><div class="perm-descr">Can all posts</div>
7074
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="can_edit_all_posts"><i class="fa fa-check-square-o fa-fw"></i></button>
7175
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="can_edit_all_posts"><i class="fa fa-square-o fa-fw"></i></button>
7276
</th>
73-
<th class="perm wants_all_posts">Wants all posts<br>
77+
<th class="perm wants_all_posts"><div class="perm-descr">Wants all posts</div>
7478
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="wants_all_posts"><i class="fa fa-check-square-o fa-fw"></i></button>
7579
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="wants_all_posts"><i class="fa fa-square-o fa-fw"></i></button>
7680
</th>
77-
<th class="perm can_upload_attachments">Attachments<br>
81+
<th class="perm can_upload_attachments"><div class="perm-descr">Attachments</div>
7882
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="can_upload_attachments"><i class="fa fa-check-square-o fa-fw"></i></button>
7983
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="can_upload_attachments"><i class="fa fa-square-o fa-fw"></i></button>
8084
</th>
81-
<th class="perm can_rebuild_site">Rebuild<br>
85+
<th class="perm can_rebuild_site"><div class="perm-descr">Rebuild</div>
8286
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="can_rebuild_site"><i class="fa fa-check-square-o fa-fw"></i></button>
8387
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="can_rebuild_site"><i class="fa fa-square-o fa-fw"></i></button>
8488
</th>
85-
<th class="perm can_transfer_post_authorship">Transfer authorship<br>
89+
<th class="perm can_transfer_post_authorship"><div class="perm-descr">Transfer authorship</div>
8690
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="can_transfer_post_authorship"><i class="fa fa-check-square-o fa-fw"></i></button>
8791
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="can_transfer_post_authorship"><i class="fa fa-square-o fa-fw"></i></button>
8892
</th>
@@ -93,7 +97,7 @@ $(document).ready(function() {
9397
<tr class="u{{ uid }}">
9498
<td class="uid">{{ uid }}</td>
9599
<td class="username">{{ user.username }}</td>
96-
{% for p in PERMISSIONS %}
100+
{% for p in PERMISSIONS_E %}
97101
<td class="perm
98102
{% if p == 'active' %}
99103
is_active
@@ -102,7 +106,7 @@ is_active
102106
{% endif %}
103107
">{{ display_permission(user, p) }}</td>
104108
{% endfor %}
105-
<td class="select_all"><button type="button" class="btn btn-info select_all-user" data-uid="{{ uid }}"><i class="fa fa-check-square-o fa-fw"></i></button> <button type="button" class="btn btn-info select_none-user" data-uid="{{ uid }}"><i class="fa fa-square-o fa-fw"></i></button></td>
109+
<td class="select_all"><button type="button" class="btn btn-sm btn-info select_all-user" data-uid="{{ uid }}"><i class="fa fa-check-square-o fa-fw"></i></button> <button type="button" class="btn btn-sm btn-info select_none-user" data-uid="{{ uid }}"><i class="fa fa-square-o fa-fw"></i></button></td>
106110
</tr>
107111
{% endif %}
108112
{% endfor %}

‎coil/data/templates/mako/coil_users.tmpl

+43-41
Original file line numberDiff line numberDiff line change
@@ -41,57 +41,59 @@ $('#deleteModal').on('show.bs.modal', function (event) {
4141

4242
<table class="table table-hover users" style="table-layout: fixed;">
4343
<thead><tr>
44-
<th class="uid">#</th>
45-
<th class="username">Username</th>
46-
<th class="realname">Real name</th>
47-
<th class="email">E-mail address</th>
48-
<th class="is_admin">Admin</th>
49-
<th class="actions">Actions</th>
44+
<th class="uid">#</th>
45+
<th class="username">Username</th>
46+
<th class="realname">Real name</th>
47+
<th class="email">E-mail address</th>
48+
<th class="is_admin">Admin</th>
49+
<th class="actions">Actions</th>
5050
</tr></thead>
5151
% for uid, user in USERS:
5252
% if user.active:
5353
<tr>
5454
% else:
5555
<tr class="danger inactive-user">
5656
% endif
57-
<td class="uid">${uid}</td>
58-
<td class="username">${user.username}</td>
59-
<td class="realname">${user.realname}</td>
60-
<td class="email">${user.email}</td>
61-
<td class="actions">
62-
% if user.is_admin:
63-
<i class="fa fa-check"></i>
64-
% else:
65-
<i class="fa fa-times"></i>
66-
% endif
67-
</td>
68-
<td class="actions">
69-
<form action="${url_for('acp_users_edit')}" method="POST">${editform.csrf_token}
70-
<input type="hidden" name="uid" value="${uid}">
71-
<input type="hidden" name="action" value="edit">
72-
<div class="btn-group" role="group">
73-
% if user.active:
74-
<button type="submit" class="btn btn-sm btn-info" title="Edit"><i class="fa fa-pencil fa-fw"></i> Edit</button>
75-
% if user.uid == current_user.uid:
76-
<button type="button" class="btn btn-sm btn-danger" title="Delete" disabled><i class="fa fa-trash fa-fw"></i> Delete</button>
77-
% else:
78-
<button type="button" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#deleteModal" data-username="${user.username}" data-uid="${uid}" data-direction="del" title="Delete"><i class="fa fa-trash fa-fw"></i> Delete</button>
79-
% endif
80-
% else:
81-
<button type="button" class="btn btn-sm btn-info" title="Edit" disabled><i class="fa fa-pencil fa-fw"></i> Edit</button>
82-
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#deleteModal" data-username="${user.username}" data-uid="${uid}" data-direction="undel" title="Undelete"><i class="fa fa-trash-o fa-fw"></i> Undelete</button>
83-
% endif
84-
</div>
85-
</form>
57+
<td class="uid">${uid}</td>
58+
<td class="username">${user.username}</td>
59+
<td class="realname">${user.realname}</td>
60+
<td class="email">${user.email}</td>
61+
<td class="actions">
62+
% if user.is_admin:
63+
<i class="fa fa-check"></i>
64+
% else:
65+
<i class="fa fa-times"></i>
66+
% endif
67+
</td>
68+
<td class="actions">
69+
<form action="${url_for('acp_users_edit')}" method="POST">${editform.csrf_token}
70+
<input type="hidden" name="uid" value="${uid}">
71+
<input type="hidden" name="action" value="edit">
72+
<div class="btn-group" role="group">
73+
% if user.active:
74+
<button type="submit" class="btn btn-sm btn-info" title="Edit"><i class="fa fa-pencil fa-fw"></i> Edit</button>
75+
% if user.uid == current_user.uid:
76+
<button type="button" class="btn btn-sm btn-danger" title="Delete" disabled><i class="fa fa-trash fa-fw"></i> Delete</button>
77+
% else:
78+
<button type="button" class="btn btn-sm btn-danger" data-toggle="modal" data-target="#deleteModal" data-username="${user.username}" data-uid="${uid}" data-direction="del" title="Delete"><i class="fa fa-trash fa-fw"></i> Delete</button>
79+
% endif
80+
% else:
81+
<button type="button" class="btn btn-sm btn-info" title="Edit" disabled><i class="fa fa-pencil fa-fw"></i> Edit</button>
82+
<button type="button" class="btn btn-sm btn-success" data-toggle="modal" data-target="#deleteModal" data-username="${user.username}" data-uid="${uid}" data-direction="undel" title="Undelete"><i class="fa fa-trash-o fa-fw"></i> Undelete</button>
83+
% endif
84+
</div>
85+
</form>
8686
</tr>
8787
% endfor
8888
<tr><form action="${url_for('acp_users_edit')}" method="POST">${editform.csrf_token}
89-
<td></td>
90-
<td><input name="username" placeholder="New user" class="form-control"></td>
91-
<td><input name="action" value="new" type="hidden"></td>
92-
<td></td>
93-
<td></td>
94-
<td><button type="submit" class="btn btn-sm btn-primary"><i class="fa fa-plus-square fa-fw"></i> Create</button></td>
89+
<td class="uid"><strong>Create:</strong></td>
90+
<td colspan="4"><input name="username" placeholder="New user" class="form-control input-sm"></td>
91+
<td><button type="submit" class="btn btn-sm btn-primary"><i class="fa fa-plus-square fa-fw"></i> Create</button></td>
92+
</form></tr>
93+
<tr><form action="${url_for('acp_users_import')}" method="POST" enctype="multipart/form-data">${importform.csrf_token}
94+
<td class="uid"><strong>Import:</strong></td>
95+
<td colspan="4"><input name="tsv" type="file" required></td>
96+
<td><button type="submit" class="btn btn-sm btn-primary"><i class="fa fa-file-text-o fa-fw"></i> Import TSV</button></td>
9597
</form></tr>
9698
</table>
9799

‎coil/data/templates/mako/coil_users_edit.tmpl

+9
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@
9595
% endif
9696
> User is an administrator
9797
</label></div>
98+
<div class="checkbox"><label><input type="checkbox" name="must_change_password"
99+
% if user.must_change_password:
100+
checked
101+
% endif
102+
% if user.uid == current_user.uid:
103+
disabled
104+
% endif
105+
> User must change password on next login
106+
</label></div>
98107
<div class="checkbox"><label><input type="checkbox" name="can_edit_all_posts"
99108
% if user.can_edit_all_posts:
100109
checked

‎coil/data/templates/mako/coil_users_permissions.tmpl

+15-11
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<%inherit file="base.tmpl"/>
33
<%block name="extra_js">
44
<script>
5-
var PERMISSIONS = ${json.dumps(PERMISSIONS)};
5+
var PERMISSIONS = ${json.dumps(PERMISSIONS_E)};
66
var UIDS = ${json.dumps(UIDS)};
77
var current_uid = ${current_user.uid};
88
$(document).ready(function() {
@@ -54,35 +54,39 @@ $(document).ready(function() {
5454
% endif
5555

5656
<form action="${url_for('acp_users_permissions')}" method="POST">
57-
<table class="table table-hover users" style="table-layout: fixed;">
57+
<table class="table table-hover users">
5858
<thead><tr>
5959
<th class="uid">#</th>
6060
<th class="username">Username</th>
61-
<th class="perm is_active">Active<br>
61+
<th class="perm is_active"><div class="perm-descr">Active</div>
6262
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="is_active"><i class="fa fa-check-square-o fa-fw"></i></button>
6363
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="is_active"><i class="fa fa-square-o fa-fw"></i></button>
6464
</th>
65-
<th class="perm is_admin">Admin<br>
65+
<th class="perm is_admin"><div class="perm-descr">Admin</div>
6666
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="is_admin"><i class="fa fa-check-square-o fa-fw"></i></button>
6767
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="is_admin"><i class="fa fa-square-o fa-fw"></i></button>
6868
</th>
69-
<th class="perm can_edit_all_posts">Can all posts<br>
69+
<th class="perm must_change_password"><div class="perm-descr">Must change password</div>
70+
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="must_change_password"><i class="fa fa-check-square-o fa-fw"></i></button>
71+
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="must_change_password"><i class="fa fa-square-o fa-fw"></i></button>
72+
</th>
73+
<th class="perm can_edit_all_posts"><div class="perm-descr">Can all posts</div>
7074
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="can_edit_all_posts"><i class="fa fa-check-square-o fa-fw"></i></button>
7175
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="can_edit_all_posts"><i class="fa fa-square-o fa-fw"></i></button>
7276
</th>
73-
<th class="perm wants_all_posts">Wants all posts<br>
77+
<th class="perm wants_all_posts"><div class="perm-descr">Wants all posts</div>
7478
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="wants_all_posts"><i class="fa fa-check-square-o fa-fw"></i></button>
7579
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="wants_all_posts"><i class="fa fa-square-o fa-fw"></i></button>
7680
</th>
77-
<th class="perm can_upload_attachments">Attachments<br>
81+
<th class="perm can_upload_attachments"><div class="perm-descr">Attachments</div>
7882
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="can_upload_attachments"><i class="fa fa-check-square-o fa-fw"></i></button>
7983
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="can_upload_attachments"><i class="fa fa-square-o fa-fw"></i></button>
8084
</th>
81-
<th class="perm can_rebuild_site">Rebuild<br>
85+
<th class="perm can_rebuild_site"><div class="perm-descr">Rebuild</div>
8286
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="can_rebuild_site"><i class="fa fa-check-square-o fa-fw"></i></button>
8387
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="can_rebuild_site"><i class="fa fa-square-o fa-fw"></i></button>
8488
</th>
85-
<th class="perm can_transfer_post_authorship">Transfer authorship<br>
89+
<th class="perm can_transfer_post_authorship"><div class="perm-descr">Transfer authorship</div>
8690
<button type="button" class="btn btn-info btn-xs select_all-perm" data-perm="can_transfer_post_authorship"><i class="fa fa-check-square-o fa-fw"></i></button>
8791
<button type="button" class="btn btn-info btn-xs select_none-perm" data-perm="can_transfer_post_authorship"><i class="fa fa-square-o fa-fw"></i></button>
8892
</th>
@@ -93,7 +97,7 @@ $(document).ready(function() {
9397
<tr class="u${uid}">
9498
<td class="uid">${uid}</td>
9599
<td class="username">${user.username}</td>
96-
% for p in PERMISSIONS:
100+
% for p in PERMISSIONS_E:
97101
<td class="perm
98102
% if p == 'active':
99103
is_active
@@ -102,7 +106,7 @@ ${p}
102106
%endif
103107
">${display_permission(user, p)}</td>
104108
% endfor
105-
<td class="select_all"><button type="button" class="btn btn-info select_all-user" data-uid="${uid}"><i class="fa fa-check-square-o fa-fw"></i></button> <button type="button" class="btn btn-info select_none-user" data-uid="${uid}"><i class="fa fa-square-o fa-fw"></i></button></td>
109+
<td class="select_all"><button type="button" class="btn btn-sm btn-info select_all-user" data-uid="${uid}"><i class="fa fa-check-square-o fa-fw"></i></button> <button type="button" class="btn btn-sm btn-info select_none-user" data-uid="${uid}"><i class="fa fa-square-o fa-fw"></i></button></td>
106110
</tr>
107111
% endif
108112
% endfor

‎coil/forms.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from __future__ import print_function, unicode_literals
2929

3030
from flask.ext.wtf import Form
31-
from wtforms.fields import TextField, PasswordField, BooleanField
31+
from wtforms.fields import TextField, PasswordField, FileField, BooleanField
3232
from wtforms.validators import Required, ValidationError
3333

3434

@@ -69,6 +69,10 @@ class AccountForm(Form):
6969
pass
7070

7171

72+
class UserImportForm(Form):
73+
"""A user import form."""
74+
tsv = FileField("TSV File")
75+
7276
class UserEditForm(Form):
7377
"""A user editor form, used for CSRF protection only."""
7478
pass

‎coil/utils.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@
3636
'SiteProxy']
3737

3838
USER_FIELDS = ['username', 'realname', 'password', 'email']
39+
# internal order
3940
PERMISSIONS = ['active', 'is_admin', 'can_edit_all_posts', 'wants_all_posts',
4041
'can_upload_attachments', 'can_rebuild_site',
41-
'can_transfer_post_authorship']
42+
'can_transfer_post_authorship', 'must_change_password']
43+
# special display order
44+
PERMISSIONS_E = ['active', 'is_admin', 'must_change_password',
45+
'can_edit_all_posts', 'wants_all_posts',
46+
'can_upload_attachments', 'can_rebuild_site',
47+
'can_transfer_post_authorship']
4248
USER_ALL = USER_FIELDS + PERMISSIONS
4349

4450

‎coil/web.py

+116-62
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,10 @@
4444
from flask.ext.login import (LoginManager, login_required, login_user,
4545
logout_user, current_user, make_secure_token)
4646
from flask.ext.bcrypt import Bcrypt
47-
from coil.utils import USER_FIELDS, PERMISSIONS, SiteProxy
47+
from coil.utils import USER_FIELDS, PERMISSIONS, PERMISSIONS_E, SiteProxy
4848
from coil.forms import (LoginForm, NewPostForm, NewPageForm, DeleteForm,
4949
UserDeleteForm, UserEditForm, AccountForm,
50-
PermissionsForm)
50+
PermissionsForm, UserImportForm)
5151

5252
_site = None
5353
site = None
@@ -366,7 +366,7 @@ class User(object):
366366
def __init__(self, uid, username, realname, password, email, active,
367367
is_admin, can_edit_all_posts, wants_all_posts,
368368
can_upload_attachments, can_rebuild_site,
369-
can_transfer_post_authorship):
369+
can_transfer_post_authorship, must_change_password):
370370
"""Initialize an user with specified settings."""
371371
self.uid = int(uid)
372372
self.username = username
@@ -380,6 +380,7 @@ def __init__(self, uid, username, realname, password, email, active,
380380
self.can_upload_attachments = can_upload_attachments
381381
self.can_rebuild_site = can_rebuild_site
382382
self.can_transfer_post_authorship = can_transfer_post_authorship
383+
self.must_change_password = must_change_password
383384

384385
def get_id(self):
385386
"""Get user ID."""
@@ -419,7 +420,7 @@ def get_user(uid):
419420
d = db.hgetall('user:{0}'.format(uid))
420421
if d:
421422
for p in PERMISSIONS:
422-
d[p] = d[p] == '1'
423+
d[p] = d.get(p) == '1'
423424
return User(uid=uid, **d)
424425
else:
425426
return None
@@ -511,6 +512,9 @@ def index():
511512
512513
:param int all: Whether or not should show all posts
513514
"""
515+
if current_user.must_change_password:
516+
return redirect(url_for('acp_account') + '?status=pwdchange')
517+
514518
context = {'postform': NewPostForm(),
515519
'pageform': NewPageForm(),
516520
'delform': DeleteForm()}
@@ -559,6 +563,9 @@ def edit(path):
559563
560564
:param path: Path to post to edit.
561565
"""
566+
if current_user.must_change_password:
567+
return redirect(url_for('acp_account') + '?status=pwdchange')
568+
562569
context = {'path': path, 'site': site}
563570
post = find_post(path)
564571
if post is None:
@@ -628,6 +635,9 @@ def edit(path):
628635
@login_required
629636
def delete():
630637
"""Delete a post."""
638+
if current_user.must_change_password:
639+
return redirect(url_for('acp_account') + '?status=pwdchange')
640+
631641
form = DeleteForm()
632642
path = request.form['path']
633643
post = find_post(path)
@@ -687,6 +697,9 @@ def api_rebuild():
687697
@login_required
688698
def rebuild(mode=''):
689699
"""Rebuild the site with a nice UI."""
700+
if current_user.must_change_password:
701+
return redirect(url_for('acp_account') + '?status=pwdchange')
702+
690703
scan_site() # for good measure
691704
if not current_user.can_rebuild_site:
692705
return error('You are not permitted to rebuild the site.</p>'
@@ -705,55 +718,16 @@ def rebuild(mode=''):
705718
return render('coil_rebuild.tmpl', {'title': 'Rebuild'})
706719

707720

708-
@app.route('/bower_components/<path:path>')
709-
def serve_bower_components(path):
710-
"""Serve bower components.
711-
712-
This is meant to be used ONLY by the internal dev server.
713-
Please configure your web server to handle requests to this URL::
714-
715-
/bower_components/ => coil/data/bower_components
716-
"""
717-
res = pkg_resources.resource_filename(
718-
'coil', os.path.join('data', 'bower_components'))
719-
return send_from_directory(res, path)
720-
721-
722-
@app.route('/coil_assets/<path:path>')
723-
def serve_coil_assets(path):
724-
"""Serve Coil assets.
725-
726-
This is meant to be used ONLY by the internal dev server.
727-
Please configure your web server to handle requests to this URL::
728-
729-
/coil_assets/ => coil/data/coil_assets
730-
"""
731-
res = pkg_resources.resource_filename(
732-
'coil', os.path.join('data', 'coil_assets'))
733-
return send_from_directory(res, path)
734-
735-
736-
@app.route('/assets/<path:path>')
737-
def serve_assets(path):
738-
"""Serve Nikola assets.
739-
740-
This is meant to be used ONLY by the internal dev server.
741-
Please configure your web server to handle requests to this URL::
742-
743-
/assets/ => output/assets
744-
"""
745-
res = os.path.join(app.config['NIKOLA_ROOT'],
746-
_site.config["OUTPUT_FOLDER"], 'assets')
747-
return send_from_directory(res, path)
748-
749-
750721
@app.route('/new/<obj>/', methods=['POST'])
751722
@login_required
752723
def new(obj):
753724
"""Create a new post or page.
754725
755726
:param str obj: Object to create (post or page)
756727
"""
728+
if current_user.must_change_password:
729+
return redirect(url_for('acp_account') + '?status=pwdchange')
730+
757731
title = request.form['title']
758732
_site.config['ADDITIONAL_METADATA']['author.uid'] = current_user.uid
759733
try:
@@ -789,15 +763,61 @@ def new(obj):
789763
return redirect(url_for('index'))
790764

791765

766+
@app.route('/bower_components/<path:path>')
767+
def serve_bower_components(path):
768+
"""Serve bower components.
769+
770+
This is meant to be used ONLY by the internal dev server.
771+
Please configure your web server to handle requests to this URL::
772+
773+
/bower_components/ => coil/data/bower_components
774+
"""
775+
res = pkg_resources.resource_filename(
776+
'coil', os.path.join('data', 'bower_components'))
777+
return send_from_directory(res, path)
778+
779+
780+
@app.route('/coil_assets/<path:path>')
781+
def serve_coil_assets(path):
782+
"""Serve Coil assets.
783+
784+
This is meant to be used ONLY by the internal dev server.
785+
Please configure your web server to handle requests to this URL::
786+
787+
/coil_assets/ => coil/data/coil_assets
788+
"""
789+
res = pkg_resources.resource_filename(
790+
'coil', os.path.join('data', 'coil_assets'))
791+
return send_from_directory(res, path)
792+
793+
794+
@app.route('/assets/<path:path>')
795+
def serve_assets(path):
796+
"""Serve Nikola assets.
797+
798+
This is meant to be used ONLY by the internal dev server.
799+
Please configure your web server to handle requests to this URL::
800+
801+
/assets/ => output/assets
802+
"""
803+
res = os.path.join(app.config['NIKOLA_ROOT'],
804+
_site.config["OUTPUT_FOLDER"], 'assets')
805+
return send_from_directory(res, path)
806+
807+
792808
@app.route('/account/', methods=['POST', 'GET'])
793809
@login_required
794810
def acp_account():
795811
"""Manage the user account of currently-logged-in users.
796812
797813
This does NOT accept admin-specific options.
798814
"""
799-
alert = ''
800-
alert_status = ''
815+
if request.args.get('status') == 'pwdchange':
816+
alert = 'You must change your password before proceeding.'
817+
alert_status = 'danger'
818+
else:
819+
alert = ''
820+
alert_status = ''
801821
action = 'edit'
802822
form = AccountForm()
803823
if request.method == 'POST':
@@ -809,6 +829,7 @@ def acp_account():
809829
if data['newpwd1'] == data['newpwd2'] and check_password(
810830
current_user.password, data['oldpwd']):
811831
current_user.password = password_hash(data['newpwd1'])
832+
current_user.must_change_password = False
812833
else:
813834
alert = 'Passwords don’t match.'
814835
alert_status = 'danger'
@@ -830,6 +851,11 @@ def acp_account():
830851
@login_required
831852
def acp_users():
832853
"""List all users."""
854+
if current_user.must_change_password:
855+
return redirect(url_for('acp_account') + '?status=pwdchange')
856+
elif not current_user.is_admin:
857+
return error("Not authorized to edit users.", 401)
858+
833859
alert = ''
834860
alert_status = ''
835861
if request.args.get('status') == 'deleted':
@@ -838,8 +864,6 @@ def acp_users():
838864
if request.args.get('status') == 'undeleted':
839865
alert = 'User undeleted.'
840866
alert_status = 'success'
841-
if not current_user.is_admin:
842-
return error("Not authorized to edit users.", 401)
843867
else:
844868
uids = db.hgetall('users').values()
845869
USERS = sorted([(i, get_user(i)) for i in uids])
@@ -849,16 +873,18 @@ def acp_users():
849873
'alert': alert,
850874
'alert_status': alert_status,
851875
'delform': UserDeleteForm(),
852-
'editform': UserEditForm()})
876+
'editform': UserEditForm(),
877+
'importform': UserImportForm()})
853878

854879

855880
@app.route('/users/edit/', methods=['POST'])
856881
@login_required
857882
def acp_users_edit():
858883
"""Edit an user account."""
859884
global current_user
860-
861-
if not current_user.is_admin:
885+
if current_user.must_change_password:
886+
return redirect(url_for('acp_account') + '?status=pwdchange')
887+
elif not current_user.is_admin:
862888
return error("Not authorized to edit users.", 401)
863889
data = request.form
864890

@@ -873,6 +899,7 @@ def acp_users_edit():
873899
uid = max(db.hgetall('users').values()) + 1
874900
pf = [False for p in PERMISSIONS]
875901
pf[0] = True # active
902+
pf[7] = True # must_change_password
876903
user = User(uid, data['username'], '', '', '', *pf)
877904
write_user(user)
878905
db.hset('users', user.username, user.uid)
@@ -911,6 +938,7 @@ def acp_users_edit():
911938
user.active = True
912939
if user.uid == current_user.uid:
913940
user.is_admin = True
941+
user.must_change_password = False
914942
current_user = user
915943
write_user(user)
916944

@@ -924,12 +952,33 @@ def acp_users_edit():
924952
'form': form})
925953

926954

955+
@app.route('/users/import/', methods=['POST'])
956+
@login_required
957+
def acp_users_import():
958+
"""Import users from a TSV file."""
959+
if current_user.must_change_password:
960+
return redirect(url_for('acp_account') + '?status=pwdchange')
961+
elif not current_user.is_admin:
962+
return error("Not authorized to edit users.", 401)
963+
964+
form = UserImportForm()
965+
if not form.validate():
966+
return error("Bad Request", 400)
967+
fh = request.files['tsv'].stream
968+
tsv = fh.read()
969+
return tsv
970+
# TODO
971+
972+
927973
@app.route('/users/delete/', methods=['POST'])
928974
@login_required
929975
def acp_users_delete():
930976
"""Delete or undelete an user account."""
931-
if not current_user.is_admin:
977+
if current_user.must_change_password:
978+
return redirect(url_for('acp_account') + '?status=pwdchange')
979+
elif not current_user.is_admin:
932980
return error("Not authorized to edit users.", 401)
981+
933982
form = UserDeleteForm()
934983
if not form.validate():
935984
return error("Bad Request", 400)
@@ -950,7 +999,9 @@ def acp_users_delete():
950999
@login_required
9511000
def acp_users_permissions():
9521001
"""Change user permissions."""
953-
if not current_user.is_admin:
1002+
if current_user.must_change_password:
1003+
return redirect(url_for('acp_account') + '?status=pwdchange')
1004+
elif not current_user.is_admin:
9541005
return error("Not authorized to edit users.", 401)
9551006

9561007
form = PermissionsForm()
@@ -966,9 +1017,11 @@ def acp_users_permissions():
9661017
setattr(user, perm, True)
9671018
else:
9681019
setattr(user, perm, False)
969-
if uid == current_user.uid:
970-
user.is_admin = True # cannot deadmin oneself
971-
user.active = True # cannot deactivate oneself
1020+
if int(uid) == current_user.uid:
1021+
# Some permissions cannot apply to the current user.
1022+
user.is_admin = True
1023+
user.active = True
1024+
user.must_change_password = False
9721025
write_user(user)
9731026
users.append((uid, user))
9741027
action = 'save'
@@ -981,8 +1034,8 @@ def display_permission(user, permission):
9811034
if permission == 'wants_all_posts' and not user.can_edit_all_posts:
9821035
# If this happens, permissions are damaged.
9831036
checked = ''
984-
if user.uid == current_user.uid and permission in ['active',
985-
'is_admin']:
1037+
if (user.uid == current_user.uid and permission in [
1038+
'active', 'is_admin', 'must_change_password']):
9861039
disabled = 'disabled'
9871040
else:
9881041
disabled = ''
@@ -993,13 +1046,14 @@ def display_permission(user, permission):
9931046
'data-perm="{4}" class="u{0}" {2} {3}>')
9941047
return d.format(user.uid, permission, checked, disabled, permission_a)
9951048

996-
users = [(i, get_user(i)) for i in uids]
1049+
if not users:
1050+
users = [(i, get_user(i)) for i in uids]
9971051

9981052
return render('coil_users_permissions.tmpl',
9991053
context={'title': 'Permissions',
10001054
'USERS': sorted(users),
10011055
'UIDS': sorted(uids),
1002-
'PERMISSIONS': PERMISSIONS,
1056+
'PERMISSIONS_E': PERMISSIONS_E,
10031057
'action': action,
10041058
'json': json,
10051059
'form': form,

‎docs/admin/users.rst

+39-10
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,17 @@ Permissions
4040
Coil uses a very granular permission system. Each user can have a different
4141
set of permissions, depending on the needs of the organization.
4242

43-
================================ ============================= ===================
44-
Name In Account In Permissions
45-
================================ ============================= ===================
46-
``active`` n/a Active
47-
``is_admin`` User is an administrator Admin
48-
``can_edit_all_posts`` Can edit posts of other users Can all posts
49-
``can_upload_attachments`` Can upload attachments Attachments
50-
``can_rebuild_site`` Can rebuild the site Rebuild
51-
``can_transfer_post_authorship`` Can transfer post authorship Transfer authorship
52-
================================ ============================= ===================
43+
================================ ================================== ====================
44+
Name In Account In Permissions
45+
================================ ================================== ====================
46+
``active`` n/a Active
47+
``must_change_password`` Must change password on next login Must change password
48+
``is_admin`` User is an administrator Admin
49+
``can_edit_all_posts`` Can edit posts of other users Can all posts
50+
``can_upload_attachments`` Can upload attachments Attachments
51+
``can_rebuild_site`` Can rebuild the site Rebuild
52+
``can_transfer_post_authorship`` Can transfer post authorship Transfer authorship
53+
================================ ================================== ====================
5354

5455
Managing users and permissions
5556
==============================
@@ -63,6 +64,33 @@ Manage users
6364
This is a table of all users. You can add new users at the bottom by typing in
6465
a name and clicking *Create*. You can also *Edit*, *Delete* or *Undelete*.
6566

67+
Bulk import
68+
```````````
69+
70+
The last row lets you import a file with user data. `TSV (Tab-Separated Values)`__
71+
files are accepted.
72+
73+
__ http://en.wikipedia.org/wiki/Tab-separated_values
74+
75+
The first row MUST contain all the column names. They are:
76+
77+
1. ``username``
78+
2. ``realname``
79+
3. ``email``
80+
4. ``password``
81+
5. ``active``
82+
6. ``is_admin``
83+
7. ``must_change_password``
84+
8. ``can_edit_all_posts``
85+
9. ``want_all_posts``
86+
10 ``can_upload_attachments``
87+
11. ``can_rebuild_site``
88+
12. ``can_transfer_post_authorship``
89+
90+
The following rows should contain data for the users. Passwords should be in
91+
plain-text. All the boolean fields (``active`` and everything after it) accept
92+
``0`` or ``1`` as their value.
93+
6694
Deleting and undeleting users
6795
`````````````````````````````
6896

@@ -77,3 +105,4 @@ Permissions
77105
This is a table of all permissions in the system. It can be used to quickly
78106
modify the permission list for groups of users. The teal buttons can be used
79107
to select the permission for all users.
108+

‎docs/internals.rst

+4-5
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,12 @@ Database schema
1010
User storage
1111
------------
1212

13-
============ ====== ===================================================
13+
============ ====== ===============================================================================
1414
Name Type Contents
15-
============ ====== ===================================================
16-
``last_uid`` string UID of newest user
15+
============ ====== ===============================================================================
1716
``users`` hash Hash mapping usernames to UIDs
18-
``user:uid`` Hash All the data we have on the user (see :doc:`users`)
19-
============ ====== ===================================================
17+
``user:uid`` hash All the data we have on the user (see :doc:`Users documentation <admin/users>`)
18+
============ ====== ===============================================================================
2019

2120
Caching site
2221
------------

0 commit comments

Comments
 (0)
Please sign in to comment.