~azzar1/unity/add-show-desktop-key

465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
1
# IVLE
2
# Copyright (C) 2007-2008 The University of Melbourne
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
18
# App: userservice
19
# Author: Matt Giuca
20
# Date: 14/2/2008
21
478 by mattgiuca
scripts/usrmgr-server: Renamed actions from dashes to underscores.
22
# Provides an Ajax service for handling user management requests.
23
# This includes when a user logs in for the first time.
24
486 by mattgiuca
caps: Added a few new capabilities.
25
### Actions ###
26
27
# The one-and-only path segment to userservice determines the action being
28
# undertaken.
29
# All actions require that you are logged in.
30
# All actions require method = POST, unless otherwise stated.
31
32
# userservice/activate_me
33
# Required cap: None
34
# declaration = "I accept the IVLE Terms of Service"
35
# Activate the currently-logged-in user's account. Requires that "declaration"
36
# is as above, and that the user's state is "no_agreement".
37
38
# userservice/create_user
39
# Required cap: CAP_CREATEUSER
40
# Arguments are the same as the database columns for the "login" table.
41
# Required:
42
#   login, fullname, rolenm
43
# Optional:
44
#   password, nick, email, studentid
45
46
# userservice/get_user
47
# method: May be GET
48
# Required cap: None to see yourself.
49
#   CAP_GETUSER to see another user.
50
# Gets the login details of a user. Returns as a JSON object.
51
# login = Optional login name of user to get. If omitted, get yourself.
52
53
# userservice/update_user
54
# Required cap: None to update yourself.
55
#   CAP_UPDATEUSER to update another user (and also more fields).
56
#   (This is all-powerful so should be only for admins)
57
# login = Optional login name of user to update. If omitted, update yourself.
58
# Other fields are optional, and will set the given field of the user.
59
# Without CAP_UPDATEUSER, you may change the following fields of yourself:
60
#   password, nick, email
61
# With CAP_UPDATEUSER, you may also change the following fields of any user:
62
#   password, nick, email, login, rolenm, unixid, fullname, studentid
63
# (You can't change "state", but see userservice/[en|dis]able_user).
64
65
# TODO
66
# userservice/enable_user
67
# Required cap: CAP_UPDATEUSER
68
# Enable a user whose account has been disabled. Does not work for
69
# no_agreement or pending users.
70
# login = Login name of user to enable.
71
72
# TODO
73
# userservice/disable_user
74
# Required cap: CAP_UPDATEUSER
75
# Disable a user's account. Does not work for no_agreement or pending users.
76
# login = Login name of user to disable.
77
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
78
# userservice/get_enrolments
79
# Required cap: None (for yourself)
80
# Returns a JSON encoded listing of a students is enrollments
81
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
82
# userservice/get_active_offerings(req, fields):
83
# Required cap: None
84
# Returns all the active offerings for a particular subject
85
# Required:
86
#   subjectid
87
981 by dcoles
Groups: Added in support for creating groups in the database through
88
# PROJECTS AND GROUPS
89
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
90
# userservice/get_project_groups
91
# Required cap: None
92
# Returns all the project groups in an offering grouped by project set
93
# Required:
94
#   offeringid
95
981 by dcoles
Groups: Added in support for creating groups in the database through
96
# userservice/create_project_set
97
# Required cap: CAP_MANAGEPROJECTS
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
98
# Creates a project set for a offering
981 by dcoles
Groups: Added in support for creating groups in the database through
99
# Required:
100
#   offeringid, max_students_per_group
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
101
# Returns:
102
#   projectsetid
981 by dcoles
Groups: Added in support for creating groups in the database through
103
104
# userservice/create_project
105
# Required cap: CAP_MANAGEPROJECTS
106
# Creates a project in a specific project set
107
# Required:
108
#   projectsetid
109
# Optional:
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
110
#   synopsis, url, deadline
111
# Returns:
112
#   projectid
981 by dcoles
Groups: Added in support for creating groups in the database through
113
114
# userservice/create_group
115
# Required cap: CAP_MANAGEGROUPS
116
# Creates a project group in a specific project set
117
# Required:
118
#   projectsetid, groupnm
119
# Optional:
120
#   nick
121
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
122
# userservice/get_group_membership
123
# Required cap: None
124
# Returns two lists. One of people in the group and one of people not in the 
125
# group (but enroled in the offering)
126
# Required:
127
#   groupid
128
981 by dcoles
Groups: Added in support for creating groups in the database through
129
# userservice/assign_to_group
130
# Required cap: CAP_MANAGEGROUPS
131
# Assigns a user to a project group
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
132
# Required: login, groupid
981 by dcoles
Groups: Added in support for creating groups in the database through
133
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
134
import os
135
import sys
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
136
import datetime
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
137
138
import cjson
139
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
140
import ivle.database
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
141
from ivle import (util, chat)
1099.1.161 by William Grant
Move ivle.dispatch.login.get_user_details() to ivle.webapp.security.
142
from ivle.webapp.security import get_user_details
1080.1.73 by William Grant
www/apps/userservice: create_user now creates and enrols the User itself, not
143
import ivle.pulldown_subj
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
144
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
145
from ivle.rpc.decorators import require_method, require_role_anywhere, \
146
                                require_admin
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
147
1080.1.12 by me at id
ivle.auth.autherror: Remove, moving AuthError into ivle.auth itself.
148
from ivle.auth import AuthError, authenticate
1078 by chadnickbok
Updated the settings page to require the old password
149
import urllib
150
1099.1.132 by William Grant
Direct port of userservice to the new framework.
151
from ivle.webapp.base.views import BaseView
152
from ivle.webapp.base.plugins import ViewPlugin
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
153
from ivle.webapp.errors import NotFound, BadRequest, Unauthorized
1099.1.132 by William Grant
Direct port of userservice to the new framework.
154
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
155
# The user must send this declaration message to ensure they acknowledge the
156
# TOS
157
USER_DECLARATION = "I accept the IVLE Terms of Service"
158
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
159
# List of fields returned as part of the user JSON dictionary
160
# (as returned by the get_user action)
161
user_fields_list = (
162
    "login", "state", "unixid", "email", "nick", "fullname",
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
163
    "admin", "studentid", "acct_exp", "pass_exp", "last_login",
1122 by William Grant
Display a message in UserSettingsView if the user is an admin, rather than
164
    "svn_pass", "admin",
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
165
)
166
1099.1.132 by William Grant
Direct port of userservice to the new framework.
167
class UserServiceView(BaseView):
168
    def __init__(self, req, path):
169
        if len(path) > 0 and path[-1] == os.sep:
170
            self.path = path[:-1]
171
        else:
172
            self.path = path
173
174
    def authorize(self, req):
1099.1.127 by William Grant
Implement ToS acceptance in the new login machinery. Now implemented through
175
        # XXX: activate_me isn't called by a valid user, so is special for now.
1099.1.132 by William Grant
Direct port of userservice to the new framework.
176
        if req.path == 'activate_me' and get_user_details(req) is not None:
177
            return True
178
        return req.user is not None
179
180
    def render(self, req):
181
        # The path determines which "command" we are receiving
182
        fields = req.get_fieldstorage()
183
        try:
184
            func = actions_map[self.path]
185
        except KeyError:
186
            raise NotFound()
187
        func(req, fields)
188
189
class Plugin(ViewPlugin):
190
    urls = [
191
        ('userservice/*path', UserServiceView)
192
    ]
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
193
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
194
@require_method('POST')
486 by mattgiuca
caps: Added a few new capabilities.
195
def handle_activate_me(req, fields):
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
196
    """Create the jail, svn, etc, for the currently logged in user (this is
197
    put in the queue for usermgt to do).
198
    This will block until usermgt returns, which could take seconds to minutes
199
    in the extreme. Therefore, it is designed to be called by Ajax, with a
200
    nice "Please wait" message on the frontend.
201
202
    This will signal that the user has accepted the terms of the license
203
    agreement, and will result in the user's database status being set to
204
    "enabled". (Note that it will be set to "pending" for the duration of the
205
    handling).
206
207
    As such, it takes a single POST field, "declaration", which
208
    must have the value, "I accept the IVLE Terms of Service".
209
    (Otherwise users could navigate to /userservice/createme without
210
    "accepting" the terms - at least this way requires them to acknowledge
211
    their acceptance). It must only be called through a POST request.
212
    """
1099.1.127 by William Grant
Implement ToS acceptance in the new login machinery. Now implemented through
213
1099.1.137 by William Grant
Actually set user in userservice/activate_me.
214
    user = get_user_details(req)
215
486 by mattgiuca
caps: Added a few new capabilities.
216
    try:
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
217
        declaration = fields.getfirst('declaration')
218
    except AttributeError:
219
        declaration = None      # Will fail next test
220
    if declaration != USER_DECLARATION:
221
        raise BadRequest()
222
223
    # Make sure the user's status is "no_agreement", and set status to
224
    # pending, within the one transaction. This ensures we only do this
225
    # one time.
226
    try:
227
        # Check that the user's status is "no_agreement".
228
        # (Both to avoid redundant calls, and to stop disabled users from
229
        # re-enabling their accounts).
230
        if user.state != "no_agreement":
231
            raise BadRequest("You have already agreed to the terms.")
232
        # Write state "pending" to ensure we don't try this again
233
        user.state = u"pending"
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
234
    except:
235
        req.store.rollback()
236
        raise
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
237
    req.store.commit()
238
239
    # Get the arguments for usermgt.activate_user from the session
240
    # (The user must have already logged in to use this app)
241
    args = {
242
        "login": user.login,
243
    }
244
    msg = {'activate_user': args}
245
246
    # Release our lock on the db so usrmgt can write
247
    req.store.rollback()
248
249
    # Try and contact the usrmgt server
250
    try:
1204 by William Grant
Use the config from the request, rather than ivle.conf, in userservice.
251
        response = chat.chat(req.config['usrmgt']['host'],
252
                             req.config['usrmgt']['port'],
253
                             msg,
254
                             req.config['usrmgt']['magic'],
255
                            )
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
256
    except cjson.DecodeError:
257
        # Gave back rubbish - set the response to failure
258
        response = {'response': 'usrmgt-failure'}
259
260
    # Get the staus of the users request
261
    try:
262
        status = response['response']
263
    except KeyError:
264
        status = 'failure'
265
266
    if status == 'okay':
267
        user.state = u"enabled"
268
    else:
269
        # Reset the user back to no agreement
270
        user.state = u"no_agreement"
271
272
    # Write the response
273
    req.content_type = "text/plain"
274
    req.write(cjson.encode(response))
486 by mattgiuca
caps: Added a few new capabilities.
275
276
create_user_fields_required = [
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
277
    'login', 'fullname',
486 by mattgiuca
caps: Added a few new capabilities.
278
]
279
create_user_fields_optional = [
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
280
    'admin', 'password', 'nick', 'email', 'studentid'
486 by mattgiuca
caps: Added a few new capabilities.
281
]
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
282
283
@require_method('POST')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
284
@require_admin
486 by mattgiuca
caps: Added a few new capabilities.
285
def handle_create_user(req, fields):
286
    """Create a new user, whose state is no_agreement.
287
    This does not create the user's jail, just an entry in the database which
288
    allows the user to accept an agreement.
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
289
       Expected fields:
290
        login       - used as a unix login name and svn repository name.
291
                      STRING REQUIRED 
292
        password    - the clear-text password for the user. If this property is
293
                      absent or None, this is an indication that external
294
                      authentication should be used (i.e. LDAP).
295
                      STRING OPTIONAL
296
        email       - the user's email address.
297
                      STRING OPTIONAL
298
        nick        - the display name to use.
299
                      STRING REQUIRED
300
        fullname    - The name of the user for results and/or other official
301
                      purposes.
302
                      STRING REQUIRED
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
303
        admin       - Whether the user is an admin.
304
                      BOOLEAN REQUIRED
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
305
        studentid   - If supplied and not None, the student id of the user for
306
                      results and/or other official purposes.
307
                      STRING OPTIONAL
308
       Return Value: the uid associated with the user. INT
486 by mattgiuca
caps: Added a few new capabilities.
309
    """
310
    # Make a dict of fields to create
311
    create = {}
312
    for f in create_user_fields_required:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
313
        val = fields.getfirst(f)
314
        if val is not None:
315
            create[f] = val
316
        else:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
317
            raise BadRequest("Required field %s missing." % repr(f))
486 by mattgiuca
caps: Added a few new capabilities.
318
    for f in create_user_fields_optional:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
319
        val = fields.getfirst(f)
320
        if val is not None:
321
            create[f] = val
322
        else:
486 by mattgiuca
caps: Added a few new capabilities.
323
            pass
324
1080.1.73 by William Grant
www/apps/userservice: create_user now creates and enrols the User itself, not
325
    user = ivle.database.User(**create)
326
    req.store.add(user)
1279 by William Grant
Drop ivle.conf usage from ivle.pulldown_subj.
327
    ivle.pulldown_subj.enrol_user(req.config, req.store, user)
1080.1.21 by me at id
userservice/create_user: Use storm rather than ivle.db.get_user.
328
486 by mattgiuca
caps: Added a few new capabilities.
329
    req.content_type = "text/plain"
1080.1.73 by William Grant
www/apps/userservice: create_user now creates and enrols the User itself, not
330
    req.write(str(user.unixid))
486 by mattgiuca
caps: Added a few new capabilities.
331
332
update_user_fields_anyone = [
333
    'password', 'nick', 'email'
334
]
335
update_user_fields_admin = [
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
336
    'password', 'nick', 'email', 'admin', 'unixid', 'fullname',
486 by mattgiuca
caps: Added a few new capabilities.
337
    'studentid'
338
]
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
339
340
@require_method('POST')
486 by mattgiuca
caps: Added a few new capabilities.
341
def handle_update_user(req, fields):
342
    """Update a user's account details.
343
    This can be done in a limited form by any user, on their own account,
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
344
    or with full powers by an admin user on any account.
486 by mattgiuca
caps: Added a few new capabilities.
345
    """
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
346
    # Only give full powers if this user is an admin.
347
    fullpowers = req.user.admin
486 by mattgiuca
caps: Added a few new capabilities.
348
    # List of fields that may be changed
349
    fieldlist = (update_user_fields_admin if fullpowers
350
        else update_user_fields_anyone)
351
352
    try:
353
        login = fields.getfirst('login')
552 by mattgiuca
userservice: Small fixes to update_user.
354
        if login is None:
355
            raise AttributeError()
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
356
        if not fullpowers and login != req.user.login:
486 by mattgiuca
caps: Added a few new capabilities.
357
            # Not allowed to edit other users
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
358
            raise Unauthorized()
486 by mattgiuca
caps: Added a few new capabilities.
359
    except AttributeError:
360
        # If login not specified, update yourself
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
361
        login = req.user.login
486 by mattgiuca
caps: Added a few new capabilities.
362
1080.1.20 by me at id
userservice/update_user: Only get the user once, not for each attr.
363
    user = ivle.database.User.get_by_login(req.store, login)
364
1078 by chadnickbok
Updated the settings page to require the old password
365
    oldpassword = fields.getfirst('oldpass')
1094 by me at id
www/apps/userservice#get_user: Fix fallout from the Storm migration.
366
    if oldpassword is not None: # It was specified.
367
        oldpassword = oldpassword.value
1078 by chadnickbok
Updated the settings page to require the old password
368
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
369
    # If the user is trying to set a new password, check that they have
370
    # entered old password and it authenticates.
371
    if fields.getfirst('password') is not None:
1078 by chadnickbok
Updated the settings page to require the old password
372
        try:
1278 by William Grant
No more ivle.conf in ivle.auth.
373
            authenticate.authenticate(req.config, req.store, login,
374
                                      oldpassword)
1078 by chadnickbok
Updated the settings page to require the old password
375
        except AuthError:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
376
            # XXX: Duplicated!
1078 by chadnickbok
Updated the settings page to require the old password
377
            req.headers_out['X-IVLE-Action-Error'] = \
378
                urllib.quote("Old password incorrect.")
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
379
            raise BadRequest("Old password incorrect.")
1078 by chadnickbok
Updated the settings page to require the old password
380
1094 by me at id
www/apps/userservice#get_user: Fix fallout from the Storm migration.
381
    # Make a dict of fields to update
382
    for f in fieldlist:
383
        val = fields.getfirst(f)
384
        if val is not None:
385
            # Note: May be rolled back if auth check below fails
386
            setattr(user, f, val.value.decode('utf-8'))
387
        else:
388
            pass
389
486 by mattgiuca
caps: Added a few new capabilities.
390
    req.content_type = "text/plain"
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
391
    req.write('')
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
392
552 by mattgiuca
userservice: Small fixes to update_user.
393
def handle_get_user(req, fields):
394
    """
395
    Retrieve a user's account details. This returns all details which the db
554 by mattgiuca
userservice: Does not return svn_pass to the user.
396
    module is willing to give up, EXCEPT the following fields:
397
        svn_pass
552 by mattgiuca
userservice: Small fixes to update_user.
398
    """
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
399
    # Only give full powers if this user is an admin
400
    fullpowers = req.user.admin
552 by mattgiuca
userservice: Small fixes to update_user.
401
402
    try:
403
        login = fields.getfirst('login')
404
        if login is None:
405
            raise AttributeError()
406
        if not fullpowers and login != req.user.login:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
407
            raise Unauthorized()
552 by mattgiuca
userservice: Small fixes to update_user.
408
    except AttributeError:
409
        # If login not specified, update yourself
410
        login = req.user.login
411
412
    # Just talk direct to the DB
1092 by me at id
www/apps/userservice#get_user: Set local_password in the output dict to True
413
    userobj = ivle.database.User.get_by_login(req.store, login)
414
    user = ivle.util.object_to_dict(user_fields_list, userobj)
674 by mattgiuca
userservice: Fixed encoding of User objects into JSON
415
    # Convert time stamps to nice strings
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
416
    for k in 'pass_exp', 'acct_exp', 'last_login':
417
        if user[k] is not None:
418
            user[k] = unicode(user[k])
419
1092 by me at id
www/apps/userservice#get_user: Set local_password in the output dict to True
420
    user['local_password'] = userobj.passhash is not None
421
554 by mattgiuca
userservice: Does not return svn_pass to the user.
422
    response = cjson.encode(user)
552 by mattgiuca
userservice: Small fixes to update_user.
423
    req.content_type = "text/plain"
424
    req.write(response)
425
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
426
def handle_get_enrolments(req, fields):
427
    """
995 by wagrant
common.db: Add get_enrolment_groups. Will return group information for
428
    Retrieve a user's enrolment details. Each enrolment includes any group
429
    memberships for that offering.
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
430
    """
431
    # For the moment we're only able to query ourselves
432
    fullpowers = False
433
434
    try:
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
435
        user = ivle.database.User.get_by_login(req.store,
436
                    fields.getfirst('login'))
437
        if user is None:
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
438
            raise AttributeError()
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
439
        if not fullpowers and user != req.user:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
440
            raise Unauthorized()
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
441
    except AttributeError:
442
        # If login not specified, update yourself
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
443
        user = req.user
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
444
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
445
    dict_enrolments = []
446
    for e in user.active_enrolments:
447
        dict_enrolments.append({
448
            'offeringid':      e.offering.id,
449
            'subj_code':       e.offering.subject.code,
450
            'subj_name':       e.offering.subject.name,
451
            'subj_short_name': e.offering.subject.short_name,
452
            'url':             e.offering.subject.url,
453
            'year':            e.offering.semester.year,
454
            'semester':        e.offering.semester.semester,
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
455
            'groups':          [{'name': group.name,
456
                                 'nick': group.nick} for group in e.groups]
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
457
        })
458
    response = cjson.encode(dict_enrolments)
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
459
    req.content_type = "text/plain"
460
    req.write(response)
461
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
462
def handle_get_active_offerings(req, fields):
463
    """Required cap: None
464
    Returns all the active offerings for a particular subject
465
    Required:
466
        subjectid
467
    """
468
469
    subjectid = fields.getfirst('subjectid')
470
    if subjectid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
471
        raise BadRequest("Required: subjectid")
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
472
    try:
473
        subjectid = int(subjectid)
474
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
475
        raise BadRequest("subjectid must be an integer")
1080.1.82 by William Grant
www/apps/userservice: Use Storm rather than get_offering_semesters.
476
477
    subject = req.store.get(ivle.database.Subject, subjectid)
478
479
    response = cjson.encode([{'offeringid': offering.id,
480
                              'subj_name': offering.subject.name,
481
                              'year': offering.semester.year,
482
                              'semester': offering.semester.semester,
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
483
                              'active': True # XXX: Eliminate from protocol.
1080.1.82 by William Grant
www/apps/userservice: Use Storm rather than get_offering_semesters.
484
                             } for offering in subject.offerings
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
485
                                    if offering.semester.state == 'current'])
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
486
    req.content_type = "text/plain"
487
    req.write(response)
488
489
def handle_get_project_groups(req, fields):
490
    """Required cap: None
491
    Returns all the project groups in an offering grouped by project set
492
    Required:
493
        offeringid
494
    """
495
496
    offeringid = fields.getfirst('offeringid')
497
    if offeringid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
498
        raise BadRequest("Required: offeringid")
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
499
    try:
500
        offeringid = int(offeringid)
501
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
502
        raise BadRequest("offeringid must be an integer")
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
503
504
    offering = req.store.get(ivle.database.Offering, offeringid)
505
506
    dict_projectsets = []
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
507
    for p in offering.project_sets:
508
        dict_projectsets.append({
509
            'projectsetid': p.id,
510
            'max_students_per_group': p.max_students_per_group,
511
            'groups': [{'groupid': g.id,
512
                        'groupnm': g.name,
513
                        'nick': g.nick} for g in p.project_groups]
514
        })
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
515
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
516
    response = cjson.encode(dict_projectsets)
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
517
    req.write(response)
518
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
519
@require_method('POST')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
520
@require_role_anywhere('tutor', 'lecturer')
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
521
def handle_create_group(req, fields):
522
    """Required cap: CAP_MANAGEGROUPS
523
    Creates a project group in a specific project set
524
    Required:
525
        projectsetid, groupnm
526
    Optional:
527
        nick
528
    Returns:
529
        groupid
530
    """
531
    # Get required fields
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
532
    projectsetid = fields.getfirst('projectsetid').value
533
    groupnm = fields.getfirst('groupnm').value
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
534
    if projectsetid is None or groupnm is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
535
        raise BadRequest("Required: projectsetid, groupnm")
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
536
    groupnm = unicode(groupnm)
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
537
    try:
538
        projectsetid = int(projectsetid)
539
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
540
        raise BadRequest("projectsetid must be an integer")
541
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
542
    # Get optional fields
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
543
    nick = fields.getfirst('nick').value
544
    if nick is not None:
545
        nick = unicode(nick)
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
546
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
547
    group = ivle.database.ProjectGroup(name=groupnm,
548
                                       project_set_id=projectsetid,
549
                                       nick=nick,
550
                                       created_by=req.user,
551
                                       epoch=datetime.datetime.now())
552
    req.store.add(group)
553
554
    # Create the group repository
555
    # Yes, this is ugly, and it would be nice to just pass in the groupid,
556
    # but the object isn't visible to the extra transaction in
557
    # usrmgt-server until we commit, which we only do once the repo is
558
    # created.
559
    offering = group.project_set.offering
560
561
    args = {
562
        "subj_short_name": offering.subject.short_name,
563
        "year": offering.semester.year,
564
        "semester": offering.semester.semester,
565
        "groupnm": group.name,
566
    }
567
    msg = {'create_group_repository': args}
568
569
    # Contact the usrmgt server
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
570
    try:
1204 by William Grant
Use the config from the request, rather than ivle.conf, in userservice.
571
        usrmgt = chat.chat(req.config['usrmgt']['host'],
572
                           req.config['usrmgt']['port'],
573
                           msg,
574
                           req.config['usrmgt']['magic'],
575
                          )
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
576
    except cjson.DecodeError, e:
577
        raise Exception("Could not understand usrmgt server response:" +
578
                        e.message)
579
580
    if 'response' not in usrmgt or usrmgt['response']=='failure':
581
        raise Exception("Failure creating repository: " + str(usrmgt))
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
582
583
    req.content_type = "text/plain"
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
584
    req.write('')
981 by dcoles
Groups: Added in support for creating groups in the database through
585
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
586
def handle_get_group_membership(req, fields):
587
    """ Required cap: None
588
    Returns two lists. One of people in the group and one of people not in the 
589
    group (but enroled in the offering)
590
    Required:
591
        groupid, offeringid
592
    """
593
    # Get required fields
594
    groupid = fields.getfirst('groupid')
595
    offeringid = fields.getfirst('offeringid')
596
    if groupid is None or offeringid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
597
        raise BadRequest("Required: groupid, offeringid")
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
598
    try:
599
        groupid = int(groupid)
600
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
601
        raise BadRequest("groupid must be an integer")
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
602
    group = req.store.get(ivle.database.ProjectGroup, groupid)
603
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
604
    try:
605
        offeringid = int(offeringid)
606
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
607
        raise BadRequest("offeringid must be an integer")
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
608
    offering = req.store.get(ivle.database.Offering, offeringid)
609
610
611
    offeringmembers = [{'login': user.login,
612
                        'fullname': user.fullname
613
                       } for user in offering.members.order_by(
614
                            ivle.database.User.login)
615
                      ]
616
    groupmembers = [{'login': user.login,
617
                        'fullname': user.fullname
618
                       } for user in group.members.order_by(
619
                            ivle.database.User.login)
620
                      ]
621
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
622
    # Make sure we don't include members in both lists
623
    for member in groupmembers:
624
        if member in offeringmembers:
625
            offeringmembers.remove(member)
626
627
    response = cjson.encode(
628
        {'groupmembers': groupmembers, 'available': offeringmembers})
629
630
    req.content_type = "text/plain"
631
    req.write(response)
632
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
633
@require_method('POST')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
634
@require_role_anywhere('tutor', 'lecturer')
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
635
def handle_assign_group(req, fields):
636
    """ Required cap: CAP_MANAGEGROUPS
637
    Assigns a user to a project group
638
    Required:
639
        login, groupid
640
    """
641
    # Get required fields
642
    login = fields.getfirst('login')
643
    groupid = fields.getfirst('groupid')
644
    if login is None or groupid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
645
        raise BadRequest("Required: login, groupid")
1080.1.84 by William Grant
wwww/apps/userservice: Create group memberships using Storm, not ivle.db.
646
647
    group = req.store.get(ivle.database.ProjectGroup, int(groupid))
648
    user = ivle.database.User.get_by_login(req.store, login)
649
650
    # Add membership to database
651
    # We can't keep a transaction open until the end here, as usrmgt-server
652
    # needs to see the changes!
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
653
    group.members.add(user)
654
    req.store.commit()
655
656
    # Rebuild the svn config file
657
    # Contact the usrmgt server
658
    msg = {'rebuild_svn_group_config': {}}
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
659
    try:
1204 by William Grant
Use the config from the request, rather than ivle.conf, in userservice.
660
        usrmgt = chat.chat(req.config['usrmgt']['host'],
661
                           req.config['usrmgt']['port'],
662
                           msg,
663
                           req.config['usrmgt']['magic'],
664
                          )
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
665
    except cjson.DecodeError, e:
666
        raise Exception("Could not understand usrmgt server response: %s" +
667
                        e.message)
668
669
        if 'response' not in usrmgt or usrmgt['response']=='failure':
670
            raise Exception("Failure creating repository: " + str(usrmgt))
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
671
672
    return(cjson.encode({'response': 'okay'}))
981 by dcoles
Groups: Added in support for creating groups in the database through
673
1181 by William Grant
Allow revocation of group memberships by tutors and lecturers.
674
@require_method('POST')
675
@require_role_anywhere('tutor', 'lecturer')
676
def handle_unassign_group(req, fields):
677
    """Remove a user from a project group.
678
679
    The user is removed from the group in the database, and access to the
680
    group Subversion repository is revoked.
681
682
    Note that any checkouts will remain, although they will be unusable.
683
    """
684
685
    # Get required fields
686
    login = fields.getfirst('login')
687
    groupid = fields.getfirst('groupid')
688
    if login is None or groupid is None:
689
        raise BadRequest("Required: login, groupid")
690
691
    group = req.store.get(ivle.database.ProjectGroup, int(groupid))
692
    user = ivle.database.User.get_by_login(req.store, login)
693
694
    # Remove membership from the database
695
    # We can't keep a transaction open until the end here, as usrmgt-server
696
    # needs to see the changes!
697
    group.members.remove(user)
698
    req.store.commit()
699
700
    # Rebuild the svn config file
701
    # Contact the usrmgt server
702
    msg = {'rebuild_svn_group_config': {}}
703
    try:
1204 by William Grant
Use the config from the request, rather than ivle.conf, in userservice.
704
        usrmgt = chat.chat(req.config['usrmgt']['host'],
705
                           req.config['usrmgt']['port'],
706
                           msg,
707
                           req.config['usrmgt']['magic'],
708
                          )
1181 by William Grant
Allow revocation of group memberships by tutors and lecturers.
709
    except cjson.DecodeError, e:
710
        raise Exception("Could not understand usrmgt server response: %s" +
711
                        e.message)
712
713
        if 'response' not in usrmgt or usrmgt['response']=='failure':
714
            raise Exception("Failure creating repository: " + str(usrmgt))
715
716
    return(cjson.encode({'response': 'okay'}))
717
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
718
# Map action names (from the path)
719
# to actual function objects
720
actions_map = {
721
    "activate_me": handle_activate_me,
722
    "create_user": handle_create_user,
723
    "update_user": handle_update_user,
552 by mattgiuca
userservice: Small fixes to update_user.
724
    "get_user": handle_get_user,
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
725
    "get_enrolments": handle_get_enrolments,
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
726
    "get_active_offerings": handle_get_active_offerings,
727
    "get_project_groups": handle_get_project_groups,
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
728
    "get_group_membership": handle_get_group_membership,
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
729
    "create_group": handle_create_group,
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
730
    "assign_group": handle_assign_group,
1181 by William Grant
Allow revocation of group memberships by tutors and lecturers.
731
    "unassign_group": handle_unassign_group,
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
732
}