~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)
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
142
from ivle.conf import (usrmgt_host, usrmgt_port, usrmgt_magic)
1099.1.161 by William Grant
Move ivle.dispatch.login.get_user_details() to ivle.webapp.security.
143
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
144
import ivle.pulldown_subj
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
145
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
146
from ivle.rpc.decorators import require_method, require_role_anywhere, \
147
                                require_admin
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
148
1080.1.12 by me at id
ivle.auth.autherror: Remove, moving AuthError into ivle.auth itself.
149
from ivle.auth import AuthError, authenticate
1078 by chadnickbok
Updated the settings page to require the old password
150
import urllib
151
1099.1.132 by William Grant
Direct port of userservice to the new framework.
152
from ivle.webapp.base.views import BaseView
153
from ivle.webapp.base.plugins import ViewPlugin
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
154
from ivle.webapp.errors import NotFound, BadRequest, Unauthorized
1099.1.132 by William Grant
Direct port of userservice to the new framework.
155
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
156
# The user must send this declaration message to ensure they acknowledge the
157
# TOS
158
USER_DECLARATION = "I accept the IVLE Terms of Service"
159
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
160
# List of fields returned as part of the user JSON dictionary
161
# (as returned by the get_user action)
162
user_fields_list = (
163
    "login", "state", "unixid", "email", "nick", "fullname",
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
164
    "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
165
    "svn_pass", "admin",
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
166
)
167
1099.1.132 by William Grant
Direct port of userservice to the new framework.
168
class UserServiceView(BaseView):
169
    def __init__(self, req, path):
170
        if len(path) > 0 and path[-1] == os.sep:
171
            self.path = path[:-1]
172
        else:
173
            self.path = path
174
175
    def authorize(self, req):
1099.1.127 by William Grant
Implement ToS acceptance in the new login machinery. Now implemented through
176
        # 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.
177
        if req.path == 'activate_me' and get_user_details(req) is not None:
178
            return True
179
        return req.user is not None
180
181
    def render(self, req):
182
        # The path determines which "command" we are receiving
183
        fields = req.get_fieldstorage()
184
        try:
185
            func = actions_map[self.path]
186
        except KeyError:
187
            raise NotFound()
188
        func(req, fields)
189
190
class Plugin(ViewPlugin):
191
    urls = [
192
        ('userservice/*path', UserServiceView)
193
    ]
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
194
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
195
@require_method('POST')
486 by mattgiuca
caps: Added a few new capabilities.
196
def handle_activate_me(req, fields):
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
197
    """Create the jail, svn, etc, for the currently logged in user (this is
198
    put in the queue for usermgt to do).
199
    This will block until usermgt returns, which could take seconds to minutes
200
    in the extreme. Therefore, it is designed to be called by Ajax, with a
201
    nice "Please wait" message on the frontend.
202
203
    This will signal that the user has accepted the terms of the license
204
    agreement, and will result in the user's database status being set to
205
    "enabled". (Note that it will be set to "pending" for the duration of the
206
    handling).
207
208
    As such, it takes a single POST field, "declaration", which
209
    must have the value, "I accept the IVLE Terms of Service".
210
    (Otherwise users could navigate to /userservice/createme without
211
    "accepting" the terms - at least this way requires them to acknowledge
212
    their acceptance). It must only be called through a POST request.
213
    """
1099.1.127 by William Grant
Implement ToS acceptance in the new login machinery. Now implemented through
214
1099.1.137 by William Grant
Actually set user in userservice/activate_me.
215
    user = get_user_details(req)
216
486 by mattgiuca
caps: Added a few new capabilities.
217
    try:
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
218
        declaration = fields.getfirst('declaration')
219
    except AttributeError:
220
        declaration = None      # Will fail next test
221
    if declaration != USER_DECLARATION:
222
        raise BadRequest()
223
224
    # Make sure the user's status is "no_agreement", and set status to
225
    # pending, within the one transaction. This ensures we only do this
226
    # one time.
227
    try:
228
        # Check that the user's status is "no_agreement".
229
        # (Both to avoid redundant calls, and to stop disabled users from
230
        # re-enabling their accounts).
231
        if user.state != "no_agreement":
232
            raise BadRequest("You have already agreed to the terms.")
233
        # Write state "pending" to ensure we don't try this again
234
        user.state = u"pending"
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
235
    except:
236
        req.store.rollback()
237
        raise
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
238
    req.store.commit()
239
240
    # Get the arguments for usermgt.activate_user from the session
241
    # (The user must have already logged in to use this app)
242
    args = {
243
        "login": user.login,
244
    }
245
    msg = {'activate_user': args}
246
247
    # Release our lock on the db so usrmgt can write
248
    req.store.rollback()
249
250
    # Try and contact the usrmgt server
251
    try:
252
        response = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic)
253
    except cjson.DecodeError:
254
        # Gave back rubbish - set the response to failure
255
        response = {'response': 'usrmgt-failure'}
256
257
    # Get the staus of the users request
258
    try:
259
        status = response['response']
260
    except KeyError:
261
        status = 'failure'
262
263
    if status == 'okay':
264
        user.state = u"enabled"
265
    else:
266
        # Reset the user back to no agreement
267
        user.state = u"no_agreement"
268
269
    # Write the response
270
    req.content_type = "text/plain"
271
    req.write(cjson.encode(response))
486 by mattgiuca
caps: Added a few new capabilities.
272
273
create_user_fields_required = [
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
274
    'login', 'fullname',
486 by mattgiuca
caps: Added a few new capabilities.
275
]
276
create_user_fields_optional = [
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
277
    'admin', 'password', 'nick', 'email', 'studentid'
486 by mattgiuca
caps: Added a few new capabilities.
278
]
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
279
280
@require_method('POST')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
281
@require_admin
486 by mattgiuca
caps: Added a few new capabilities.
282
def handle_create_user(req, fields):
283
    """Create a new user, whose state is no_agreement.
284
    This does not create the user's jail, just an entry in the database which
285
    allows the user to accept an agreement.
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
286
       Expected fields:
287
        login       - used as a unix login name and svn repository name.
288
                      STRING REQUIRED 
289
        password    - the clear-text password for the user. If this property is
290
                      absent or None, this is an indication that external
291
                      authentication should be used (i.e. LDAP).
292
                      STRING OPTIONAL
293
        email       - the user's email address.
294
                      STRING OPTIONAL
295
        nick        - the display name to use.
296
                      STRING REQUIRED
297
        fullname    - The name of the user for results and/or other official
298
                      purposes.
299
                      STRING REQUIRED
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
300
        admin       - Whether the user is an admin.
301
                      BOOLEAN REQUIRED
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
302
        studentid   - If supplied and not None, the student id of the user for
303
                      results and/or other official purposes.
304
                      STRING OPTIONAL
305
       Return Value: the uid associated with the user. INT
486 by mattgiuca
caps: Added a few new capabilities.
306
    """
307
    # Make a dict of fields to create
308
    create = {}
309
    for f in create_user_fields_required:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
310
        val = fields.getfirst(f)
311
        if val is not None:
312
            create[f] = val
313
        else:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
314
            raise BadRequest("Required field %s missing." % repr(f))
486 by mattgiuca
caps: Added a few new capabilities.
315
    for f in create_user_fields_optional:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
316
        val = fields.getfirst(f)
317
        if val is not None:
318
            create[f] = val
319
        else:
486 by mattgiuca
caps: Added a few new capabilities.
320
            pass
321
1080.1.73 by William Grant
www/apps/userservice: create_user now creates and enrols the User itself, not
322
    user = ivle.database.User(**create)
323
    req.store.add(user)
324
    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.
325
486 by mattgiuca
caps: Added a few new capabilities.
326
    req.content_type = "text/plain"
1080.1.73 by William Grant
www/apps/userservice: create_user now creates and enrols the User itself, not
327
    req.write(str(user.unixid))
486 by mattgiuca
caps: Added a few new capabilities.
328
329
update_user_fields_anyone = [
330
    'password', 'nick', 'email'
331
]
332
update_user_fields_admin = [
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
333
    'password', 'nick', 'email', 'admin', 'unixid', 'fullname',
486 by mattgiuca
caps: Added a few new capabilities.
334
    'studentid'
335
]
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
336
337
@require_method('POST')
486 by mattgiuca
caps: Added a few new capabilities.
338
def handle_update_user(req, fields):
339
    """Update a user's account details.
340
    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.
341
    or with full powers by an admin user on any account.
486 by mattgiuca
caps: Added a few new capabilities.
342
    """
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
343
    # Only give full powers if this user is an admin.
344
    fullpowers = req.user.admin
486 by mattgiuca
caps: Added a few new capabilities.
345
    # List of fields that may be changed
346
    fieldlist = (update_user_fields_admin if fullpowers
347
        else update_user_fields_anyone)
348
349
    try:
350
        login = fields.getfirst('login')
552 by mattgiuca
userservice: Small fixes to update_user.
351
        if login is None:
352
            raise AttributeError()
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
353
        if not fullpowers and login != req.user.login:
486 by mattgiuca
caps: Added a few new capabilities.
354
            # Not allowed to edit other users
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
355
            raise Unauthorized()
486 by mattgiuca
caps: Added a few new capabilities.
356
    except AttributeError:
357
        # If login not specified, update yourself
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
358
        login = req.user.login
486 by mattgiuca
caps: Added a few new capabilities.
359
1080.1.20 by me at id
userservice/update_user: Only get the user once, not for each attr.
360
    user = ivle.database.User.get_by_login(req.store, login)
361
1078 by chadnickbok
Updated the settings page to require the old password
362
    oldpassword = fields.getfirst('oldpass')
1094 by me at id
www/apps/userservice#get_user: Fix fallout from the Storm migration.
363
    if oldpassword is not None: # It was specified.
364
        oldpassword = oldpassword.value
1078 by chadnickbok
Updated the settings page to require the old password
365
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
366
    # If the user is trying to set a new password, check that they have
367
    # entered old password and it authenticates.
368
    if fields.getfirst('password') is not None:
1078 by chadnickbok
Updated the settings page to require the old password
369
        try:
1094 by me at id
www/apps/userservice#get_user: Fix fallout from the Storm migration.
370
            authenticate.authenticate(req.store, login, oldpassword)
1078 by chadnickbok
Updated the settings page to require the old password
371
        except AuthError:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
372
            # XXX: Duplicated!
1078 by chadnickbok
Updated the settings page to require the old password
373
            req.headers_out['X-IVLE-Action-Error'] = \
374
                urllib.quote("Old password incorrect.")
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
375
            raise BadRequest("Old password incorrect.")
1078 by chadnickbok
Updated the settings page to require the old password
376
1094 by me at id
www/apps/userservice#get_user: Fix fallout from the Storm migration.
377
    # Make a dict of fields to update
378
    for f in fieldlist:
379
        val = fields.getfirst(f)
380
        if val is not None:
381
            # Note: May be rolled back if auth check below fails
382
            setattr(user, f, val.value.decode('utf-8'))
383
        else:
384
            pass
385
486 by mattgiuca
caps: Added a few new capabilities.
386
    req.content_type = "text/plain"
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
387
    req.write('')
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
388
552 by mattgiuca
userservice: Small fixes to update_user.
389
def handle_get_user(req, fields):
390
    """
391
    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.
392
    module is willing to give up, EXCEPT the following fields:
393
        svn_pass
552 by mattgiuca
userservice: Small fixes to update_user.
394
    """
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
395
    # Only give full powers if this user is an admin
396
    fullpowers = req.user.admin
552 by mattgiuca
userservice: Small fixes to update_user.
397
398
    try:
399
        login = fields.getfirst('login')
400
        if login is None:
401
            raise AttributeError()
402
        if not fullpowers and login != req.user.login:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
403
            raise Unauthorized()
552 by mattgiuca
userservice: Small fixes to update_user.
404
    except AttributeError:
405
        # If login not specified, update yourself
406
        login = req.user.login
407
408
    # 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
409
    userobj = ivle.database.User.get_by_login(req.store, login)
410
    user = ivle.util.object_to_dict(user_fields_list, userobj)
674 by mattgiuca
userservice: Fixed encoding of User objects into JSON
411
    # 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
412
    for k in 'pass_exp', 'acct_exp', 'last_login':
413
        if user[k] is not None:
414
            user[k] = unicode(user[k])
415
1092 by me at id
www/apps/userservice#get_user: Set local_password in the output dict to True
416
    user['local_password'] = userobj.passhash is not None
417
554 by mattgiuca
userservice: Does not return svn_pass to the user.
418
    response = cjson.encode(user)
552 by mattgiuca
userservice: Small fixes to update_user.
419
    req.content_type = "text/plain"
420
    req.write(response)
421
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
422
def handle_get_enrolments(req, fields):
423
    """
995 by wagrant
common.db: Add get_enrolment_groups. Will return group information for
424
    Retrieve a user's enrolment details. Each enrolment includes any group
425
    memberships for that offering.
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
426
    """
427
    # For the moment we're only able to query ourselves
428
    fullpowers = False
429
430
    try:
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
431
        user = ivle.database.User.get_by_login(req.store,
432
                    fields.getfirst('login'))
433
        if user is None:
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
434
            raise AttributeError()
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
435
        if not fullpowers and user != req.user:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
436
            raise Unauthorized()
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
437
    except AttributeError:
438
        # 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.
439
        user = req.user
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
440
1080.1.28 by me at id
www/apps/groups: Use User.active_enrolments rather than ivle.db.get_enrolment.
441
    dict_enrolments = []
442
    for e in user.active_enrolments:
443
        dict_enrolments.append({
444
            'offeringid':      e.offering.id,
445
            'subj_code':       e.offering.subject.code,
446
            'subj_name':       e.offering.subject.name,
447
            'subj_short_name': e.offering.subject.short_name,
448
            'url':             e.offering.subject.url,
449
            'year':            e.offering.semester.year,
450
            'semester':        e.offering.semester.semester,
1080.1.81 by William Grant
ivle.database.Enrolment: Add a groups attribute, containing groups of which
451
            'groups':          [{'name': group.name,
452
                                 '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.
453
        })
454
    response = cjson.encode(dict_enrolments)
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
455
    req.content_type = "text/plain"
456
    req.write(response)
457
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
458
def handle_get_active_offerings(req, fields):
459
    """Required cap: None
460
    Returns all the active offerings for a particular subject
461
    Required:
462
        subjectid
463
    """
464
465
    subjectid = fields.getfirst('subjectid')
466
    if subjectid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
467
        raise BadRequest("Required: subjectid")
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
468
    try:
469
        subjectid = int(subjectid)
470
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
471
        raise BadRequest("subjectid must be an integer")
1080.1.82 by William Grant
www/apps/userservice: Use Storm rather than get_offering_semesters.
472
473
    subject = req.store.get(ivle.database.Subject, subjectid)
474
475
    response = cjson.encode([{'offeringid': offering.id,
476
                              'subj_name': offering.subject.name,
477
                              'year': offering.semester.year,
478
                              'semester': offering.semester.semester,
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
479
                              'active': True # XXX: Eliminate from protocol.
1080.1.82 by William Grant
www/apps/userservice: Use Storm rather than get_offering_semesters.
480
                             } for offering in subject.offerings
1104 by William Grant
Replace Semester.active with Semester.state, allowing more useful state
481
                                    if offering.semester.state == 'current'])
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
482
    req.content_type = "text/plain"
483
    req.write(response)
484
485
def handle_get_project_groups(req, fields):
486
    """Required cap: None
487
    Returns all the project groups in an offering grouped by project set
488
    Required:
489
        offeringid
490
    """
491
492
    offeringid = fields.getfirst('offeringid')
493
    if offeringid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
494
        raise BadRequest("Required: offeringid")
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
495
    try:
496
        offeringid = int(offeringid)
497
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
498
        raise BadRequest("offeringid must be an integer")
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
499
500
    offering = req.store.get(ivle.database.Offering, offeringid)
501
502
    dict_projectsets = []
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
503
    for p in offering.project_sets:
504
        dict_projectsets.append({
505
            'projectsetid': p.id,
506
            'max_students_per_group': p.max_students_per_group,
507
            'groups': [{'groupid': g.id,
508
                        'groupnm': g.name,
509
                        'nick': g.nick} for g in p.project_groups]
510
        })
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
511
1080.1.76 by William Grant
ivle.database.Offering: Add project_sets referenceset.
512
    response = cjson.encode(dict_projectsets)
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
513
    req.write(response)
514
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
515
@require_method('POST')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
516
@require_role_anywhere('tutor', 'lecturer')
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
517
def handle_create_group(req, fields):
518
    """Required cap: CAP_MANAGEGROUPS
519
    Creates a project group in a specific project set
520
    Required:
521
        projectsetid, groupnm
522
    Optional:
523
        nick
524
    Returns:
525
        groupid
526
    """
527
    # Get required fields
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
528
    projectsetid = fields.getfirst('projectsetid').value
529
    groupnm = fields.getfirst('groupnm').value
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
530
    if projectsetid is None or groupnm is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
531
        raise BadRequest("Required: projectsetid, groupnm")
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
532
    groupnm = unicode(groupnm)
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
533
    try:
534
        projectsetid = int(projectsetid)
535
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
536
        raise BadRequest("projectsetid must be an integer")
537
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
538
    # Get optional fields
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
539
    nick = fields.getfirst('nick').value
540
    if nick is not None:
541
        nick = unicode(nick)
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
542
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
543
    group = ivle.database.ProjectGroup(name=groupnm,
544
                                       project_set_id=projectsetid,
545
                                       nick=nick,
546
                                       created_by=req.user,
547
                                       epoch=datetime.datetime.now())
548
    req.store.add(group)
549
550
    # Create the group repository
551
    # Yes, this is ugly, and it would be nice to just pass in the groupid,
552
    # but the object isn't visible to the extra transaction in
553
    # usrmgt-server until we commit, which we only do once the repo is
554
    # created.
555
    offering = group.project_set.offering
556
557
    args = {
558
        "subj_short_name": offering.subject.short_name,
559
        "year": offering.semester.year,
560
        "semester": offering.semester.semester,
561
        "groupnm": group.name,
562
    }
563
    msg = {'create_group_repository': args}
564
565
    # Contact the usrmgt server
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
566
    try:
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
567
        usrmgt = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic)
568
    except cjson.DecodeError, e:
569
        raise Exception("Could not understand usrmgt server response:" +
570
                        e.message)
571
572
    if 'response' not in usrmgt or usrmgt['response']=='failure':
573
        raise Exception("Failure creating repository: " + str(usrmgt))
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
574
575
    req.content_type = "text/plain"
1080.1.80 by William Grant
www/apps/userservice: Port create_group to Storm.
576
    req.write('')
981 by dcoles
Groups: Added in support for creating groups in the database through
577
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
578
def handle_get_group_membership(req, fields):
579
    """ Required cap: None
580
    Returns two lists. One of people in the group and one of people not in the 
581
    group (but enroled in the offering)
582
    Required:
583
        groupid, offeringid
584
    """
585
    # Get required fields
586
    groupid = fields.getfirst('groupid')
587
    offeringid = fields.getfirst('offeringid')
588
    if groupid is None or offeringid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
589
        raise BadRequest("Required: groupid, offeringid")
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
590
    try:
591
        groupid = int(groupid)
592
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
593
        raise BadRequest("groupid must be an integer")
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
594
    group = req.store.get(ivle.database.ProjectGroup, groupid)
595
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
596
    try:
597
        offeringid = int(offeringid)
598
    except:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
599
        raise BadRequest("offeringid must be an integer")
1080.1.79 by William Grant
ivle.database.Offering: Add a members ReferenceSet.
600
    offering = req.store.get(ivle.database.Offering, offeringid)
601
602
603
    offeringmembers = [{'login': user.login,
604
                        'fullname': user.fullname
605
                       } for user in offering.members.order_by(
606
                            ivle.database.User.login)
607
                      ]
608
    groupmembers = [{'login': user.login,
609
                        'fullname': user.fullname
610
                       } for user in group.members.order_by(
611
                            ivle.database.User.login)
612
                      ]
613
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
614
    # Make sure we don't include members in both lists
615
    for member in groupmembers:
616
        if member in offeringmembers:
617
            offeringmembers.remove(member)
618
619
    response = cjson.encode(
620
        {'groupmembers': groupmembers, 'available': offeringmembers})
621
622
    req.content_type = "text/plain"
623
    req.write(response)
624
1080.1.90 by William Grant
ivle.rpc.decorators: Add (new package, too). Has a couple of decorators to
625
@require_method('POST')
1101 by William Grant
Privileges (apart from admin) are now offering-local, not global.
626
@require_role_anywhere('tutor', 'lecturer')
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
627
def handle_assign_group(req, fields):
628
    """ Required cap: CAP_MANAGEGROUPS
629
    Assigns a user to a project group
630
    Required:
631
        login, groupid
632
    """
633
    # Get required fields
634
    login = fields.getfirst('login')
635
    groupid = fields.getfirst('groupid')
636
    if login is None or groupid is None:
1099.1.134 by William Grant
Replace most userservice req.throw_error()s with new exceptions.
637
        raise BadRequest("Required: login, groupid")
1080.1.84 by William Grant
wwww/apps/userservice: Create group memberships using Storm, not ivle.db.
638
639
    group = req.store.get(ivle.database.ProjectGroup, int(groupid))
640
    user = ivle.database.User.get_by_login(req.store, login)
641
642
    # Add membership to database
643
    # We can't keep a transaction open until the end here, as usrmgt-server
644
    # needs to see the changes!
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
645
    group.members.add(user)
646
    req.store.commit()
647
648
    # Rebuild the svn config file
649
    # Contact the usrmgt server
650
    msg = {'rebuild_svn_group_config': {}}
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
651
    try:
1099.1.136 by William Grant
Remove some useless try-except blocks that just raise a 500.
652
        usrmgt = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic)
653
    except cjson.DecodeError, e:
654
        raise Exception("Could not understand usrmgt server response: %s" +
655
                        e.message)
656
657
        if 'response' not in usrmgt or usrmgt['response']=='failure':
658
            raise Exception("Failure creating repository: " + str(usrmgt))
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
659
660
    return(cjson.encode({'response': 'okay'}))
981 by dcoles
Groups: Added in support for creating groups in the database through
661
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
662
# Map action names (from the path)
663
# to actual function objects
664
actions_map = {
665
    "activate_me": handle_activate_me,
666
    "create_user": handle_create_user,
667
    "update_user": handle_update_user,
552 by mattgiuca
userservice: Small fixes to update_user.
668
    "get_user": handle_get_user,
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
669
    "get_enrolments": handle_get_enrolments,
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
670
    "get_active_offerings": handle_get_active_offerings,
671
    "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
672
    "get_group_membership": handle_get_group_membership,
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
673
    "create_group": handle_create_group,
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
674
    "assign_group": handle_assign_group,
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
675
}