~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)
327
    ivle.pulldown_subj.enrol_user(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:
1094 by me at id
www/apps/userservice#get_user: Fix fallout from the Storm migration.
373
            authenticate.authenticate(req.store, login, oldpassword)
1078 by chadnickbok
Updated the settings page to require the old password
374
        except AuthError:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
375
            # XXX: Duplicated!
1078 by chadnickbok
Updated the settings page to require the old password
376
            req.headers_out['X-IVLE-Action-Error'] = \
377
                urllib.quote("Old password incorrect.")
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
378
            raise BadRequest("Old password incorrect.")
1078 by chadnickbok
Updated the settings page to require the old password
379
1094 by me at id
www/apps/userservice#get_user: Fix fallout from the Storm migration.
380
    # Make a dict of fields to update
381
    for f in fieldlist:
382
        val = fields.getfirst(f)
383
        if val is not None:
384
            # Note: May be rolled back if auth check below fails
385
            setattr(user, f, val.value.decode('utf-8'))
386
        else:
387
            pass
388
486 by mattgiuca
caps: Added a few new capabilities.
389
    req.content_type = "text/plain"
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
390
    req.write('')
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
391
552 by mattgiuca
userservice: Small fixes to update_user.
392
def handle_get_user(req, fields):
393
    """
394
    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.
395
    module is willing to give up, EXCEPT the following fields:
396
        svn_pass
552 by mattgiuca
userservice: Small fixes to update_user.
397
    """
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
398
    # Only give full powers if this user is an admin
399
    fullpowers = req.user.admin
552 by mattgiuca
userservice: Small fixes to update_user.
400
401
    try:
402
        login = fields.getfirst('login')
403
        if login is None:
404
            raise AttributeError()
405
        if not fullpowers and login != req.user.login:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
406
            raise Unauthorized()
552 by mattgiuca
userservice: Small fixes to update_user.
407
    except AttributeError:
408
        # If login not specified, update yourself
409
        login = req.user.login
410
411
    # 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
412
    userobj = ivle.database.User.get_by_login(req.store, login)
413
    user = ivle.util.object_to_dict(user_fields_list, userobj)
674 by mattgiuca
userservice: Fixed encoding of User objects into JSON
414
    # 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
415
    for k in 'pass_exp', 'acct_exp', 'last_login':
416
        if user[k] is not None:
417
            user[k] = unicode(user[k])
418
1092 by me at id
www/apps/userservice#get_user: Set local_password in the output dict to True
419
    user['local_password'] = userobj.passhash is not None
420
554 by mattgiuca
userservice: Does not return svn_pass to the user.
421
    response = cjson.encode(user)
552 by mattgiuca
userservice: Small fixes to update_user.
422
    req.content_type = "text/plain"
423
    req.write(response)
424
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
425
def handle_get_enrolments(req, fields):
426
    """
995 by wagrant
common.db: Add get_enrolment_groups. Will return group information for
427
    Retrieve a user's enrolment details. Each enrolment includes any group
428
    memberships for that offering.
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
429
    """
430
    # For the moment we're only able to query ourselves
431
    fullpowers = False
432
433
    try:
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
434
        user = ivle.database.User.get_by_login(req.store,
435
                    fields.getfirst('login'))
436
        if user is None:
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
437
            raise AttributeError()
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
438
        if not fullpowers and user != req.user:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
439
            raise Unauthorized()
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
440
    except AttributeError:
441
        # 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.
442
        user = req.user
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
443
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
444
    dict_enrolments = []
445
    for e in user.active_enrolments:
446
        dict_enrolments.append({
447
            'offeringid':      e.offering.id,
448
            'subj_code':       e.offering.subject.code,
449
            'subj_name':       e.offering.subject.name,
450
            'subj_short_name': e.offering.subject.short_name,
451
            'url':             e.offering.subject.url,
452
            'year':            e.offering.semester.year,
453
            'semester':        e.offering.semester.semester,
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
454
            'groups':          [{'name': group.name,
455
                                 '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.
456
        })
457
    response = cjson.encode(dict_enrolments)
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
458
    req.content_type = "text/plain"
459
    req.write(response)
460
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
461
def handle_get_active_offerings(req, fields):
462
    """Required cap: None
463
    Returns all the active offerings for a particular subject
464
    Required:
465
        subjectid
466
    """
467
468
    subjectid = fields.getfirst('subjectid')
469
    if subjectid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
470
        raise BadRequest("Required: subjectid")
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
471
    try:
472
        subjectid = int(subjectid)
473
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
474
        raise BadRequest("subjectid must be an integer")
1080.1.82 by William Grant
www/apps/userservice: Use Storm rather than get_offering_semesters.
475
476
    subject = req.store.get(ivle.database.Subject, subjectid)
477
478
    response = cjson.encode([{'offeringid': offering.id,
479
                              'subj_name': offering.subject.name,
480
                              'year': offering.semester.year,
481
                              'semester': offering.semester.semester,
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
482
                              'active': True # XXX: Eliminate from protocol.
1080.1.82 by William Grant
www/apps/userservice: Use Storm rather than get_offering_semesters.
483
                             } for offering in subject.offerings
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
484
                                    if offering.semester.state == 'current'])
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
485
    req.content_type = "text/plain"
486
    req.write(response)
487
488
def handle_get_project_groups(req, fields):
489
    """Required cap: None
490
    Returns all the project groups in an offering grouped by project set
491
    Required:
492
        offeringid
493
    """
494
495
    offeringid = fields.getfirst('offeringid')
496
    if offeringid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
497
        raise BadRequest("Required: offeringid")
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
498
    try:
499
        offeringid = int(offeringid)
500
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
501
        raise BadRequest("offeringid must be an integer")
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
502
503
    offering = req.store.get(ivle.database.Offering, offeringid)
504
505
    dict_projectsets = []
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
506
    for p in offering.project_sets:
507
        dict_projectsets.append({
508
            'projectsetid': p.id,
509
            'max_students_per_group': p.max_students_per_group,
510
            'groups': [{'groupid': g.id,
511
                        'groupnm': g.name,
512
                        'nick': g.nick} for g in p.project_groups]
513
        })
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
514
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
515
    response = cjson.encode(dict_projectsets)
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
516
    req.write(response)
517
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
518
@require_method('POST')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
519
@require_role_anywhere('tutor', 'lecturer')
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
520
def handle_create_group(req, fields):
521
    """Required cap: CAP_MANAGEGROUPS
522
    Creates a project group in a specific project set
523
    Required:
524
        projectsetid, groupnm
525
    Optional:
526
        nick
527
    Returns:
528
        groupid
529
    """
530
    # Get required fields
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
531
    projectsetid = fields.getfirst('projectsetid').value
532
    groupnm = fields.getfirst('groupnm').value
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
533
    if projectsetid is None or groupnm is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
534
        raise BadRequest("Required: projectsetid, groupnm")
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
535
    groupnm = unicode(groupnm)
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
536
    try:
537
        projectsetid = int(projectsetid)
538
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
539
        raise BadRequest("projectsetid must be an integer")
540
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
541
    # Get optional fields
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
542
    nick = fields.getfirst('nick').value
543
    if nick is not None:
544
        nick = unicode(nick)
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
545
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
546
    group = ivle.database.ProjectGroup(name=groupnm,
547
                                       project_set_id=projectsetid,
548
                                       nick=nick,
549
                                       created_by=req.user,
550
                                       epoch=datetime.datetime.now())
551
    req.store.add(group)
552
553
    # Create the group repository
554
    # Yes, this is ugly, and it would be nice to just pass in the groupid,
555
    # but the object isn't visible to the extra transaction in
556
    # usrmgt-server until we commit, which we only do once the repo is
557
    # created.
558
    offering = group.project_set.offering
559
560
    args = {
561
        "subj_short_name": offering.subject.short_name,
562
        "year": offering.semester.year,
563
        "semester": offering.semester.semester,
564
        "groupnm": group.name,
565
    }
566
    msg = {'create_group_repository': args}
567
568
    # Contact the usrmgt server
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
569
    try:
1204 by William Grant
Use the config from the request, rather than ivle.conf, in userservice.
570
        usrmgt = chat.chat(req.config['usrmgt']['host'],
571
                           req.config['usrmgt']['port'],
572
                           msg,
573
                           req.config['usrmgt']['magic'],
574
                          )
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
575
    except cjson.DecodeError, e:
576
        raise Exception("Could not understand usrmgt server response:" +
577
                        e.message)
578
579
    if 'response' not in usrmgt or usrmgt['response']=='failure':
580
        raise Exception("Failure creating repository: " + str(usrmgt))
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
581
582
    req.content_type = "text/plain"
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
583
    req.write('')
981 by dcoles
Groups: Added in support for creating groups in the database through
584
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
585
def handle_get_group_membership(req, fields):
586
    """ Required cap: None
587
    Returns two lists. One of people in the group and one of people not in the 
588
    group (but enroled in the offering)
589
    Required:
590
        groupid, offeringid
591
    """
592
    # Get required fields
593
    groupid = fields.getfirst('groupid')
594
    offeringid = fields.getfirst('offeringid')
595
    if groupid is None or offeringid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
596
        raise BadRequest("Required: groupid, offeringid")
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
597
    try:
598
        groupid = int(groupid)
599
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
600
        raise BadRequest("groupid must be an integer")
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
601
    group = req.store.get(ivle.database.ProjectGroup, groupid)
602
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
603
    try:
604
        offeringid = int(offeringid)
605
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
606
        raise BadRequest("offeringid must be an integer")
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
607
    offering = req.store.get(ivle.database.Offering, offeringid)
608
609
610
    offeringmembers = [{'login': user.login,
611
                        'fullname': user.fullname
612
                       } for user in offering.members.order_by(
613
                            ivle.database.User.login)
614
                      ]
615
    groupmembers = [{'login': user.login,
616
                        'fullname': user.fullname
617
                       } for user in group.members.order_by(
618
                            ivle.database.User.login)
619
                      ]
620
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
621
    # Make sure we don't include members in both lists
622
    for member in groupmembers:
623
        if member in offeringmembers:
624
            offeringmembers.remove(member)
625
626
    response = cjson.encode(
627
        {'groupmembers': groupmembers, 'available': offeringmembers})
628
629
    req.content_type = "text/plain"
630
    req.write(response)
631
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
632
@require_method('POST')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
633
@require_role_anywhere('tutor', 'lecturer')
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
634
def handle_assign_group(req, fields):
635
    """ Required cap: CAP_MANAGEGROUPS
636
    Assigns a user to a project group
637
    Required:
638
        login, groupid
639
    """
640
    # Get required fields
641
    login = fields.getfirst('login')
642
    groupid = fields.getfirst('groupid')
643
    if login is None or groupid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
644
        raise BadRequest("Required: login, groupid")
1080.1.84 by William Grant
wwww/apps/userservice: Create group memberships using Storm, not ivle.db.
645
646
    group = req.store.get(ivle.database.ProjectGroup, int(groupid))
647
    user = ivle.database.User.get_by_login(req.store, login)
648
649
    # Add membership to database
650
    # We can't keep a transaction open until the end here, as usrmgt-server
651
    # needs to see the changes!
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
652
    group.members.add(user)
653
    req.store.commit()
654
655
    # Rebuild the svn config file
656
    # Contact the usrmgt server
657
    msg = {'rebuild_svn_group_config': {}}
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
658
    try:
1204 by William Grant
Use the config from the request, rather than ivle.conf, in userservice.
659
        usrmgt = chat.chat(req.config['usrmgt']['host'],
660
                           req.config['usrmgt']['port'],
661
                           msg,
662
                           req.config['usrmgt']['magic'],
663
                          )
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
664
    except cjson.DecodeError, e:
665
        raise Exception("Could not understand usrmgt server response: %s" +
666
                        e.message)
667
668
        if 'response' not in usrmgt or usrmgt['response']=='failure':
669
            raise Exception("Failure creating repository: " + str(usrmgt))
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
670
671
    return(cjson.encode({'response': 'okay'}))
981 by dcoles
Groups: Added in support for creating groups in the database through
672
1181 by William Grant
Allow revocation of group memberships by tutors and lecturers.
673
@require_method('POST')
674
@require_role_anywhere('tutor', 'lecturer')
675
def handle_unassign_group(req, fields):
676
    """Remove a user from a project group.
677
678
    The user is removed from the group in the database, and access to the
679
    group Subversion repository is revoked.
680
681
    Note that any checkouts will remain, although they will be unusable.
682
    """
683
684
    # Get required fields
685
    login = fields.getfirst('login')
686
    groupid = fields.getfirst('groupid')
687
    if login is None or groupid is None:
688
        raise BadRequest("Required: login, groupid")
689
690
    group = req.store.get(ivle.database.ProjectGroup, int(groupid))
691
    user = ivle.database.User.get_by_login(req.store, login)
692
693
    # Remove membership from the database
694
    # We can't keep a transaction open until the end here, as usrmgt-server
695
    # needs to see the changes!
696
    group.members.remove(user)
697
    req.store.commit()
698
699
    # Rebuild the svn config file
700
    # Contact the usrmgt server
701
    msg = {'rebuild_svn_group_config': {}}
702
    try:
1204 by William Grant
Use the config from the request, rather than ivle.conf, in userservice.
703
        usrmgt = chat.chat(req.config['usrmgt']['host'],
704
                           req.config['usrmgt']['port'],
705
                           msg,
706
                           req.config['usrmgt']['magic'],
707
                          )
1181 by William Grant
Allow revocation of group memberships by tutors and lecturers.
708
    except cjson.DecodeError, e:
709
        raise Exception("Could not understand usrmgt server response: %s" +
710
                        e.message)
711
712
        if 'response' not in usrmgt or usrmgt['response']=='failure':
713
            raise Exception("Failure creating repository: " + str(usrmgt))
714
715
    return(cjson.encode({'response': 'okay'}))
716
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
717
# Map action names (from the path)
718
# to actual function objects
719
actions_map = {
720
    "activate_me": handle_activate_me,
721
    "create_user": handle_create_user,
722
    "update_user": handle_update_user,
552 by mattgiuca
userservice: Small fixes to update_user.
723
    "get_user": handle_get_user,
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
724
    "get_enrolments": handle_get_enrolments,
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
725
    "get_active_offerings": handle_get_active_offerings,
726
    "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
727
    "get_group_membership": handle_get_group_membership,
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
728
    "create_group": handle_create_group,
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
729
    "assign_group": handle_assign_group,
1181 by William Grant
Allow revocation of group memberships by tutors and lecturers.
730
    "unassign_group": handle_unassign_group,
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
731
}