18
18
# Author: Matt Giuca, Will Grant
21
import formencode.validators
22
from genshi.filters import HTMLFormFiller
24
from ivle.database import User
26
from ivle.pulldown_subj import enrol_user
27
from ivle.webapp import ApplicationRoot
28
from ivle.webapp.base.forms import BaseFormView, URLNameValidator
20
from ivle.webapp.base.rest import JSONRESTView
29
21
from ivle.webapp.base.xhtml import XHTMLView
30
from ivle.webapp.base.plugins import ViewPlugin, MediaPlugin
31
from ivle.webapp.admin.publishing import root_to_user, user_url
34
class UsersView(XHTMLView):
35
"""A list of all IVLE users."""
36
template = 'templates/users.html'
38
breadcrumb_text = 'Users'
40
def authorize(self, req):
41
return req.user and req.user.admin
43
def populate(self, req, ctx):
45
ctx['users'] = req.store.find(User).order_by(User.login)
48
class UserEditSchema(formencode.Schema):
49
nick = formencode.validators.UnicodeString(not_empty=True)
50
email = formencode.validators.Email(not_empty=False,
53
class UserEditView(BaseFormView):
54
"""A form to change a user's details."""
55
template = 'templates/user-edit.html'
61
return UserEditSchema()
63
def get_default_data(self, req):
64
return {'nick': self.context.nick,
65
'email': self.context.email
68
def save_object(self, req, data):
69
self.context.nick = data['nick']
70
self.context.email = unicode(data['email']) if data['email'] \
74
def populate(self, req, ctx):
75
super(UserEditView, self).populate(req, ctx)
76
ctx['format_datetime'] = ivle.date.make_date_nice
77
ctx['format_datetime_short'] = ivle.date.format_datetime_for_paragraph
78
ctx['svn_url'] = req.user.get_svn_url(req.config)
79
ctx['svn_pass'] = req.user.svn_pass
82
class UserAdminSchema(formencode.Schema):
83
admin = formencode.validators.StringBoolean(if_missing=False)
84
disabled = formencode.validators.StringBoolean(if_missing=False)
85
fullname = formencode.validators.UnicodeString(not_empty=True)
86
studentid = formencode.validators.UnicodeString(not_empty=False,
90
class UserAdminView(BaseFormView):
91
"""A form for admins to change more of a user's details."""
92
template = 'templates/user-admin.html'
95
def authorize(self, req):
96
"""Only allow access if the requesting user is an admin."""
97
return req.user and req.user.admin
101
return UserAdminSchema()
103
def get_default_data(self, req):
104
return {'admin': self.context.admin,
105
'disabled': self.context.state == u'disabled',
106
'fullname': self.context.fullname,
107
'studentid': self.context.studentid,
110
def save_object(self, req, data):
111
if self.context is req.user:
112
# Admin checkbox is disabled -- assume unchanged
113
data['admin'] = self.context.admin
114
data['disabled'] = self.context.state == u'disabled'
116
self.context.admin = data['admin']
117
if self.context.state in (u'enabled', u'disabled'):
118
self.context.state = (u'disabled' if data['disabled']
120
self.context.fullname = data['fullname'] \
121
if data['fullname'] else None
122
self.context.studentid = data['studentid'] \
123
if data['studentid'] else None
126
def populate(self, req, ctx):
127
super(UserAdminView, self).populate(req, ctx)
129
# Disable the admin checkbox if editing oneself
130
ctx['disable_admin'] = self.context is req.user
132
class PasswordChangeView(XHTMLView):
133
"""A form to change a user's password, with knowledge of the old one."""
134
template = 'templates/user-password-change.html'
138
def authorize(self, req):
139
"""Only allow access if the requesting user holds the permission,
140
and the target user has a password set. Otherwise we might be
141
clobbering external authn.
143
return super(PasswordChangeView, self).authorize(req) and \
144
self.context.passhash is not None
146
def populate(self, req, ctx):
148
if req.method == 'POST':
149
data = dict(req.get_fieldstorage())
150
if data.get('old_password') is None or \
151
not self.context.authenticate(data.get('old_password')):
152
error = 'Incorrect password.'
153
elif data.get('new_password') != data.get('new_password_again'):
154
error = 'New passwords do not match.'
155
elif not data.get('new_password'):
156
error = 'New password cannot be empty.'
158
self.context.password = data['new_password']
160
req.throw_redirect(req.uri)
163
ctx['user'] = self.context
166
class PasswordResetView(XHTMLView):
167
"""A form to reset a user's password, without knowledge of the old one."""
168
template = 'templates/user-password-reset.html'
171
def authorize(self, req):
172
"""Only allow access if the requesting user is an admin."""
173
return req.user and req.user.admin
175
def populate(self, req, ctx):
177
if req.method == 'POST':
178
data = dict(req.get_fieldstorage())
179
if data.get('new_password') != data.get('new_password_again'):
180
error = 'New passwords do not match.'
181
elif not data.get('new_password'):
182
error = 'New password cannot be empty.'
184
self.context.password = data['new_password']
186
req.throw_redirect(req.uri)
188
ctx['user'] = self.context
192
class UserNewSchema(formencode.Schema):
193
login = URLNameValidator() # XXX: Validate uniqueness.
194
admin = formencode.validators.StringBoolean(if_missing=False)
195
fullname = formencode.validators.UnicodeString(not_empty=True)
196
studentid = formencode.validators.UnicodeString(not_empty=False,
199
email = formencode.validators.Email(not_empty=False,
203
class UserNewView(BaseFormView):
204
"""A form for admins to create new users."""
205
template = 'templates/user-new.html'
208
def authorize(self, req):
209
"""Only allow access if the requesting user is an admin."""
210
return req.user and req.user.admin
214
return UserNewSchema()
216
def get_default_data(self, req):
219
def save_object(self, req, data):
220
data['nick'] = data['fullname']
221
data['email'] = unicode(data['email']) if data['email'] else None
222
userobj = User(**data)
223
req.store.add(userobj)
224
enrol_user(req.config, req.store, userobj)
229
class Plugin(ViewPlugin, MediaPlugin):
22
from ivle.webapp.base.plugins import ViewPlugin
23
from ivle.webapp.errors import NotFound, Unauthorized
27
# List of fields returned as part of the user JSON dictionary
28
# (as returned by the get_user action)
30
"login", "state", "unixid", "email", "nick", "fullname",
31
"rolenm", "studentid", "acct_exp", "pass_exp", "last_login",
35
class UserRESTView(JSONRESTView):
37
A REST interface to the user object.
39
def __init__(self, req, login):
40
super(UserRESTView, self).__init__(self, req, login)
41
self.context = ivle.database.User.get_by_login(req.store, login)
42
if self.context is None:
47
user = ivle.util.object_to_dict(user_fields_list, self.context)
48
# Convert time stamps to nice strings
49
for k in 'pass_exp', 'acct_exp', 'last_login':
50
if user[k] is not None:
51
user[k] = unicode(user[k])
53
user['local_password'] = self.context.passhash is not None
56
def PATCH(self, req, data):
58
# XXX Admins can set extra fields
59
# Note: Cannot change password here (use change_password named op)
61
for f in user_fields_list:
64
if isinstance(field, str):
65
field = unicode(field)
66
setattr(self.context, f, field)
70
class UserSettingsView(XHTMLView):
71
template = 'user-settings.html'
74
def __init__(self, req, login):
75
self.context = ivle.database.User.get_by_login(req.store, login)
76
if self.context is None:
79
if req.user is None or (req.user is not self.context and
80
req.user.rolenm != 'admin'):
83
def populate(self, req, ctx):
84
self.plugin_scripts[Plugin] = ['settings.js']
85
req.scripts_init = ['revert_settings']
87
ctx['login'] = self.context.login
89
class Plugin(ViewPlugin):
231
91
The Plugin class for the user plugin.
234
forward_routes = (root_to_user,)
235
reverse_routes = (user_url,)
236
views = [(ApplicationRoot, ('users', '+index'), UsersView),
237
(ApplicationRoot, ('users', '+new'), UserNewView),
238
(User, '+index', UserEditView),
239
(User, '+admin', UserAdminView),
240
(User, '+changepassword', PasswordChangeView),
241
(User, '+resetpassword', PasswordResetView),
245
('users', 'Users', 'Display and edit all users',
246
'users.png', 'users', 90, True)
93
# Magic attribute: urls
94
# Sequence of pairs/triples of
95
# (regex str, handler class, kwargs dict)
96
# The kwargs dict is passed to the __init__ of the view object
98
('~:login/+settings', UserSettingsView),
99
('api/~:login', UserRESTView),
249
public_forward_routes = forward_routes
250
public_reverse_routes = reverse_routes
252
102
media = 'user-media'