1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
|
# IVLE - Informatics Virtual Learning Environment
# Copyright (C) 2007-2009 The University of Melbourne
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# Author: Matt Giuca, Will Grant
import formencode
import formencode.validators
from genshi.filters import HTMLFormFiller
from ivle.webapp import ApplicationRoot
from ivle.webapp.base.rest import JSONRESTView, require_permission
from ivle.webapp.base.xhtml import XHTMLView
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
from ivle.webapp.admin.publishing import root_to_user, user_url
from ivle.database import User
import ivle.database
import ivle.date
import ivle.util
class UsersView(XHTMLView):
"""A list of all IVLE users."""
template = 'templates/users.html'
tab = 'users'
breadcrumb_text = 'Users'
def authorize(self, req):
return req.user and req.user.admin
def populate(self, req, ctx):
ctx['req'] = req
ctx['users'] = req.store.find(User).order_by(User.login)
# List of fields returned as part of the user JSON dictionary
# (as returned by the get_user action)
user_fields_list = (
"login", "state", "unixid", "email", "nick", "fullname",
"admin", "studentid", "acct_exp", "pass_exp", "last_login",
"svn_pass"
)
class UserRESTView(JSONRESTView):
"""
A REST interface to the user object.
"""
@require_permission('view')
def GET(self, req):
# XXX Check Caps
user = ivle.util.object_to_dict(user_fields_list, self.context)
# Convert time stamps to nice strings
for k in 'pass_exp', 'acct_exp', 'last_login':
if user[k] is not None:
user[k] = unicode(user[k])
user['local_password'] = self.context.passhash is not None
return user
class UserEditSchema(formencode.Schema):
nick = formencode.validators.UnicodeString(not_empty=True)
email = formencode.validators.Email(not_empty=False,
if_missing=None)
class UserEditView(XHTMLView):
"""A form to change a user's details."""
template = 'templates/user-edit.html'
tab = 'users'
permission = 'edit'
def filter(self, stream, ctx):
return stream | HTMLFormFiller(data=ctx['data'])
def populate(self, req, ctx):
if req.method == 'POST':
data = dict(req.get_fieldstorage())
try:
validator = UserEditSchema()
data = validator.to_python(data, state=req)
self.context.nick = data['nick']
self.context.email = unicode(data['email']) if data['email'] \
else None
req.store.commit()
req.throw_redirect(req.uri)
except formencode.Invalid, e:
errors = e.unpack_errors()
else:
data = {'nick': self.context.nick,
'email': self.context.email
}
errors = {}
ctx['format_datetime'] = ivle.date.make_date_nice
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
ctx['req'] = req
ctx['user'] = self.context
ctx['data'] = data
ctx['errors'] = errors
class UserAdminSchema(formencode.Schema):
admin = formencode.validators.StringBoolean(if_missing=False)
disabled = formencode.validators.StringBoolean(if_missing=False)
fullname = formencode.validators.UnicodeString(not_empty=True)
studentid = formencode.validators.UnicodeString(not_empty=False,
if_missing=None
)
class UserAdminView(XHTMLView):
"""A form for admins to change more of a user's details."""
template = 'templates/user-admin.html'
tab = 'users'
def authorize(self, req):
"""Only allow access if the requesting user is an admin."""
return req.user and req.user.admin
def filter(self, stream, ctx):
return stream | HTMLFormFiller(data=ctx['data'])
def populate(self, req, ctx):
if req.method == 'POST':
data = dict(req.get_fieldstorage())
try:
validator = UserAdminSchema()
data = validator.to_python(data, state=req)
if self.context is req.user:
# Admin checkbox is disabled -- assume unchanged
data['admin'] = self.context.admin
data['disabled'] = self.context.state == u'disabled'
else:
self.context.admin = data['admin']
if self.context.state in (u'enabled', u'disabled'):
self.context.state = (u'disabled' if data['disabled']
else u'enabled')
self.context.fullname = data['fullname'] \
if data['fullname'] else None
self.context.studentid = data['studentid'] \
if data['studentid'] else None
req.store.commit()
req.throw_redirect(req.uri)
except formencode.Invalid, e:
errors = e.unpack_errors()
else:
data = {'admin': self.context.admin,
'disabled': self.context.state == u'disabled',
'fullname': self.context.fullname,
'studentid': self.context.studentid,
}
errors = {}
ctx['req'] = req
ctx['user'] = self.context
# Disable the Admin checkbox if editing oneself
ctx['disable_admin'] = self.context is req.user
ctx['data'] = data
ctx['errors'] = errors
class PasswordChangeView(XHTMLView):
"""A form to change a user's password, with knowledge of the old one."""
template = 'templates/user-password-change.html'
tab = 'users'
permission = 'edit'
def authorize(self, req):
"""Only allow access if the requesting user holds the permission,
and the target user has a password set. Otherwise we might be
clobbering external authn.
"""
return super(PasswordChangeView, self).authorize(req) and \
self.context.passhash is not None
def populate(self, req, ctx):
error = None
if req.method == 'POST':
data = dict(req.get_fieldstorage())
if data.get('old_password') is None or \
not self.context.authenticate(data.get('old_password')):
error = 'Incorrect password.'
elif data.get('new_password') != data.get('new_password_again'):
error = 'New passwords do not match.'
elif not data.get('new_password'):
error = 'New password cannot be empty.'
else:
self.context.password = data['new_password']
req.store.commit()
req.throw_redirect(req.uri)
ctx['req'] = req
ctx['user'] = self.context
ctx['error'] = error
class PasswordResetView(XHTMLView):
"""A form to reset a user's password, without knowledge of the old one."""
template = 'templates/user-password-reset.html'
tab = 'users'
def authorize(self, req):
"""Only allow access if the requesting user is an admin."""
return req.user and req.user.admin
def populate(self, req, ctx):
error = None
if req.method == 'POST':
data = dict(req.get_fieldstorage())
if data.get('new_password') != data.get('new_password_again'):
error = 'New passwords do not match.'
elif not data.get('new_password'):
error = 'New password cannot be empty.'
else:
self.context.password = data['new_password']
req.store.commit()
req.throw_redirect(req.uri)
ctx['user'] = self.context
ctx['error'] = error
class Plugin(ViewPlugin, MediaPlugin):
"""
The Plugin class for the user plugin.
"""
forward_routes = (root_to_user,)
reverse_routes = (user_url,)
views = [(ApplicationRoot, 'users', UsersView),
(ivle.database.User, '+index', UserEditView),
(ivle.database.User, '+admin', UserAdminView),
(ivle.database.User, '+changepassword', PasswordChangeView),
(ivle.database.User, '+resetpassword', PasswordResetView),
(ivle.database.User, '+index', UserRESTView, 'api'),
]
tabs = [
('users', 'Users', 'Display and edit all users',
'users.png', 'users', 90, True)
]
public_forward_routes = forward_routes
public_reverse_routes = reverse_routes
media = 'user-media'
|