~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
25
# NOTE: This app does NOT require authentication. This is because otherwise it
26
# would be blocked from receiving requests to activate when the user is trying
27
# to accept the TOS.
28
29
# It must do its own authentication and authorization.
30
486 by mattgiuca
caps: Added a few new capabilities.
31
### Actions ###
32
33
# The one-and-only path segment to userservice determines the action being
34
# undertaken.
35
# All actions require that you are logged in.
36
# All actions require method = POST, unless otherwise stated.
37
38
# userservice/activate_me
39
# Required cap: None
40
# declaration = "I accept the IVLE Terms of Service"
41
# Activate the currently-logged-in user's account. Requires that "declaration"
42
# is as above, and that the user's state is "no_agreement".
43
44
# userservice/create_user
45
# Required cap: CAP_CREATEUSER
46
# Arguments are the same as the database columns for the "login" table.
47
# Required:
48
#   login, fullname, rolenm
49
# Optional:
50
#   password, nick, email, studentid
51
52
# userservice/get_user
53
# method: May be GET
54
# Required cap: None to see yourself.
55
#   CAP_GETUSER to see another user.
56
# Gets the login details of a user. Returns as a JSON object.
57
# login = Optional login name of user to get. If omitted, get yourself.
58
59
# userservice/update_user
60
# Required cap: None to update yourself.
61
#   CAP_UPDATEUSER to update another user (and also more fields).
62
#   (This is all-powerful so should be only for admins)
63
# login = Optional login name of user to update. If omitted, update yourself.
64
# Other fields are optional, and will set the given field of the user.
65
# Without CAP_UPDATEUSER, you may change the following fields of yourself:
66
#   password, nick, email
67
# With CAP_UPDATEUSER, you may also change the following fields of any user:
68
#   password, nick, email, login, rolenm, unixid, fullname, studentid
69
# (You can't change "state", but see userservice/[en|dis]able_user).
70
71
# TODO
72
# userservice/enable_user
73
# Required cap: CAP_UPDATEUSER
74
# Enable a user whose account has been disabled. Does not work for
75
# no_agreement or pending users.
76
# login = Login name of user to enable.
77
78
# TODO
79
# userservice/disable_user
80
# Required cap: CAP_UPDATEUSER
81
# Disable a user's account. Does not work for no_agreement or pending users.
82
# login = Login name of user to disable.
83
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
84
# userservice/get_enrolments
85
# Required cap: None (for yourself)
86
# Returns a JSON encoded listing of a students is enrollments
87
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
88
# userservice/get_active_offerings(req, fields):
89
# Required cap: None
90
# Returns all the active offerings for a particular subject
91
# Required:
92
#   subjectid
93
981 by dcoles
Groups: Added in support for creating groups in the database through
94
# PROJECTS AND GROUPS
95
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
96
# userservice/get_project_groups
97
# Required cap: None
98
# Returns all the project groups in an offering grouped by project set
99
# Required:
100
#   offeringid
101
981 by dcoles
Groups: Added in support for creating groups in the database through
102
# userservice/create_project_set
103
# Required cap: CAP_MANAGEPROJECTS
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
104
# Creates a project set for a offering
981 by dcoles
Groups: Added in support for creating groups in the database through
105
# Required:
106
#   offeringid, max_students_per_group
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
107
# Returns:
108
#   projectsetid
981 by dcoles
Groups: Added in support for creating groups in the database through
109
110
# userservice/create_project
111
# Required cap: CAP_MANAGEPROJECTS
112
# Creates a project in a specific project set
113
# Required:
114
#   projectsetid
115
# Optional:
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
116
#   synopsis, url, deadline
117
# Returns:
118
#   projectid
981 by dcoles
Groups: Added in support for creating groups in the database through
119
120
# userservice/create_group
121
# Required cap: CAP_MANAGEGROUPS
122
# Creates a project group in a specific project set
123
# Required:
124
#   projectsetid, groupnm
125
# Optional:
126
#   nick
127
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
128
# userservice/get_group_membership
129
# Required cap: None
130
# Returns two lists. One of people in the group and one of people not in the 
131
# group (but enroled in the offering)
132
# Required:
133
#   groupid
134
981 by dcoles
Groups: Added in support for creating groups in the database through
135
# userservice/assign_to_group
136
# Required cap: CAP_MANAGEGROUPS
137
# 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
138
# Required: login, groupid
981 by dcoles
Groups: Added in support for creating groups in the database through
139
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
140
import os
141
import sys
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
142
import time
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
143
144
import cjson
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
145
import pg
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
146
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
147
import ivle.db
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
148
import ivle.database
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
149
import ivle.makeuser
150
from ivle import (util, chat, caps)
151
from ivle.conf import (usrmgt_host, usrmgt_port, usrmgt_magic)
152
1080.1.12 by me at id
ivle.auth.autherror: Remove, moving AuthError into ivle.auth itself.
153
from ivle.auth import AuthError, authenticate
1078 by chadnickbok
Updated the settings page to require the old password
154
import urllib
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",
164
    "rolenm", "studentid", "acct_exp", "pass_exp", "last_login",
165
    "svn_pass"
166
)
167
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
168
def handle(req):
169
    """Handler for the Console Service AJAX backend application."""
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
170
    if req.user is None:
478 by mattgiuca
scripts/usrmgr-server: Renamed actions from dashes to underscores.
171
        # Not logged in
550 by mattgiuca
userservice: Added error messages.
172
        req.throw_error(req.HTTP_FORBIDDEN,
173
        "You are not logged in to IVLE.")
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
174
    if len(req.path) > 0 and req.path[-1] == os.sep:
175
        path = req.path[:-1]
176
    else:
177
        path = req.path
178
    # The path determines which "command" we are receiving
486 by mattgiuca
caps: Added a few new capabilities.
179
    fields = req.get_fieldstorage()
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
180
    try:
181
        func = actions_map[req.path]
182
    except KeyError:
550 by mattgiuca
userservice: Added error messages.
183
        req.throw_error(req.HTTP_BAD_REQUEST,
184
        "%s is not a valid userservice action." % repr(req.path))
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
185
    func(req, fields)
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
186
486 by mattgiuca
caps: Added a few new capabilities.
187
def handle_activate_me(req, fields):
465 by mattgiuca
Added new app: userservice, which is an ajax service for user management
188
    """Create the jail, svn, etc, for the currently logged in user (this is
189
    put in the queue for usermgt to do).
190
    This will block until usermgt returns, which could take seconds to minutes
191
    in the extreme. Therefore, it is designed to be called by Ajax, with a
192
    nice "Please wait" message on the frontend.
193
194
    This will signal that the user has accepted the terms of the license
195
    agreement, and will result in the user's database status being set to
196
    "enabled". (Note that it will be set to "pending" for the duration of the
197
    handling).
198
199
    As such, it takes a single POST field, "declaration", which
200
    must have the value, "I accept the IVLE Terms of Service".
201
    (Otherwise users could navigate to /userservice/createme without
202
    "accepting" the terms - at least this way requires them to acknowledge
203
    their acceptance). It must only be called through a POST request.
204
    """
486 by mattgiuca
caps: Added a few new capabilities.
205
    try:
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
206
        if req.method != "POST":
550 by mattgiuca
userservice: Added error messages.
207
            req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
208
            "Only POST requests are valid methods to activate_me.")
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
209
        try:
210
            declaration = fields.getfirst('declaration')
211
        except AttributeError:
550 by mattgiuca
userservice: Added error messages.
212
            declaration = None      # Will fail next test
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
213
        if declaration != USER_DECLARATION:
550 by mattgiuca
userservice: Added error messages.
214
            req.throw_error(req.HTTP_BAD_REQUEST,
215
            "Please use the Terms of Service form instead of talking to "
216
            "this service directly.")
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
217
218
        # Make sure the user's status is "no_agreement", and set status to
219
        # pending, within the one transaction. This ensures we only do this
220
        # one time.
221
        try:
222
            # Check that the user's status is "no_agreement".
223
            # (Both to avoid redundant calls, and to stop disabled users from
224
            # re-enabling their accounts).
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
225
            if req.user.state != "no_agreement":
550 by mattgiuca
userservice: Added error messages.
226
                req.throw_error(req.HTTP_BAD_REQUEST,
227
                "You have already agreed to the terms.")
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
228
            # Write state "pending" to ensure we don't try this again
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
229
            req.user.state = u"pending"
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
230
        except:
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
231
            req.store.rollback()
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
232
            raise
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
233
        req.store.commit()
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
234
235
        # Get the arguments for usermgt.activate_user from the session
236
        # (The user must have already logged in to use this app)
237
        args = {
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
238
            "login": req.user.login,
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
239
        }
240
        msg = {'activate_user': args}
241
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
242
        # Release our lock on the db so usrmgt can write
243
        req.store.rollback()
244
842 by dcoles
Userservice: Made the userservice more robust so if usrmgt-server fails to
245
        # Try and contact the usrmgt server
246
        try:
247
            response = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic)
248
        except cjson.DecodeError:
249
            # Gave back rubbish - set the response to failure
250
            response = {'response': 'usrmgt-failure'}
251
252
        # Get the staus of the users request
253
        try:
254
            status = response['response']
255
        except KeyError:
256
            status = 'failure'
257
        
258
        if status == 'okay':
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
259
            req.user.state = u"enabled"
842 by dcoles
Userservice: Made the userservice more robust so if usrmgt-server fails to
260
        else:
261
            # Reset the user back to no agreement
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
262
            req.user.state = u"no_agreement"
263
            req.store.commit()
842 by dcoles
Userservice: Made the userservice more robust so if usrmgt-server fails to
264
497 by mattgiuca
userservice: activate_me is now database enabled. It checks the state from the
265
        # Write the response
266
        req.content_type = "text/plain"
842 by dcoles
Userservice: Made the userservice more robust so if usrmgt-server fails to
267
        req.write(cjson.encode(response))
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
268
    except:
269
        req.store.rollback()
270
        raise
486 by mattgiuca
caps: Added a few new capabilities.
271
272
create_user_fields_required = [
273
    'login', 'fullname', 'rolenm'
274
]
275
create_user_fields_optional = [
943 by wagrant
makeuser, userservice: Don't allow specification of a custom unixid, as
276
    'password', 'nick', 'email', 'studentid'
486 by mattgiuca
caps: Added a few new capabilities.
277
]
278
def handle_create_user(req, fields):
279
    """Create a new user, whose state is no_agreement.
280
    This does not create the user's jail, just an entry in the database which
281
    allows the user to accept an agreement.
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
282
       Expected fields:
283
        login       - used as a unix login name and svn repository name.
284
                      STRING REQUIRED 
285
        password    - the clear-text password for the user. If this property is
286
                      absent or None, this is an indication that external
287
                      authentication should be used (i.e. LDAP).
288
                      STRING OPTIONAL
289
        email       - the user's email address.
290
                      STRING OPTIONAL
291
        nick        - the display name to use.
292
                      STRING REQUIRED
293
        fullname    - The name of the user for results and/or other official
294
                      purposes.
295
                      STRING REQUIRED
296
        rolenm      - The user's role. Must be one of "anyone", "student",
297
                      "tutor", "lecturer", "admin".
298
                      STRING/ENUM REQUIRED
299
        studentid   - If supplied and not None, the student id of the user for
300
                      results and/or other official purposes.
301
                      STRING OPTIONAL
302
       Return Value: the uid associated with the user. INT
486 by mattgiuca
caps: Added a few new capabilities.
303
    """
304
    if req.method != "POST":
550 by mattgiuca
userservice: Added error messages.
305
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
306
            "Only POST requests are valid methods to create_user.")
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
307
    # Check if this user has CAP_UPDATEUSER
308
    if not req.user.hasCap(caps.CAP_UPDATEUSER):
550 by mattgiuca
userservice: Added error messages.
309
        req.throw_error(req.HTTP_FORBIDDEN,
310
        "You do not have permission to create users.")
486 by mattgiuca
caps: Added a few new capabilities.
311
312
    # Make a dict of fields to create
313
    create = {}
314
    for f in create_user_fields_required:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
315
        val = fields.getfirst(f)
316
        if val is not None:
317
            create[f] = val
318
        else:
550 by mattgiuca
userservice: Added error messages.
319
            req.throw_error(req.HTTP_BAD_REQUEST,
320
            "Required field %s missing." % repr(f))
486 by mattgiuca
caps: Added a few new capabilities.
321
    for f in create_user_fields_optional:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
322
        val = fields.getfirst(f)
323
        if val is not None:
324
            create[f] = val
325
        else:
486 by mattgiuca
caps: Added a few new capabilities.
326
            pass
327
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
328
    ivle.makeuser.make_user_db(**create)
329
    user = ivle.db.DB().get_user(create["login"])
486 by mattgiuca
caps: Added a few new capabilities.
330
    req.content_type = "text/plain"
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
331
    req.write(str(user.unixid))
486 by mattgiuca
caps: Added a few new capabilities.
332
333
update_user_fields_anyone = [
334
    'password', 'nick', 'email'
335
]
336
update_user_fields_admin = [
552 by mattgiuca
userservice: Small fixes to update_user.
337
    'password', 'nick', 'email', 'rolenm', 'unixid', 'fullname',
486 by mattgiuca
caps: Added a few new capabilities.
338
    'studentid'
339
]
340
def handle_update_user(req, fields):
341
    """Update a user's account details.
342
    This can be done in a limited form by any user, on their own account,
343
    or with full powers by a user with CAP_UPDATEUSER on any account.
344
    """
345
    if req.method != "POST":
550 by mattgiuca
userservice: Added error messages.
346
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
677 by mattgiuca
userservice: Fix error message text.
347
        "Only POST requests are valid methods to update_user.")
486 by mattgiuca
caps: Added a few new capabilities.
348
349
    # Only give full powers if this user has CAP_UPDATEUSER
505 by mattgiuca
dispatch.html, consoleservice, userservice, interpret:
350
    fullpowers = req.user.hasCap(caps.CAP_UPDATEUSER)
486 by mattgiuca
caps: Added a few new capabilities.
351
    # List of fields that may be changed
352
    fieldlist = (update_user_fields_admin if fullpowers
353
        else update_user_fields_anyone)
354
355
    try:
356
        login = fields.getfirst('login')
552 by mattgiuca
userservice: Small fixes to update_user.
357
        if login is None:
358
            raise AttributeError()
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
359
        if not fullpowers and login != req.user.login:
486 by mattgiuca
caps: Added a few new capabilities.
360
            # Not allowed to edit other users
550 by mattgiuca
userservice: Added error messages.
361
            req.throw_error(req.HTTP_FORBIDDEN,
362
            "You do not have permission to update another user.")
486 by mattgiuca
caps: Added a few new capabilities.
363
    except AttributeError:
364
        # If login not specified, update yourself
506 by mattgiuca
dispatch.__init__, dispatch.request, cgirequest:
365
        login = req.user.login
486 by mattgiuca
caps: Added a few new capabilities.
366
367
    # Make a dict of fields to update
1078 by chadnickbok
Updated the settings page to require the old password
368
    oldpassword = fields.getfirst('oldpass')
369
    
486 by mattgiuca
caps: Added a few new capabilities.
370
    for f in fieldlist:
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
371
        val = fields.getfirst(f)
372
        if val is not None:
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
373
            # Note: May be rolled back if auth check below fails
374
            user = ivle.database.User.get_by_login(req.store, login)
375
            setattr(user, f, val.value.decode('utf-8'))
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
376
        else:
486 by mattgiuca
caps: Added a few new capabilities.
377
            pass
1078 by chadnickbok
Updated the settings page to require the old password
378
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
379
    # If the user is trying to set a new password, check that they have
380
    # entered old password and it authenticates.
381
    if fields.getfirst('password') is not None:
1078 by chadnickbok
Updated the settings page to require the old password
382
        try:
383
            authenticate.authenticate(login, oldpassword)
384
        except AuthError:
385
            req.headers_out['X-IVLE-Action-Error'] = \
386
                urllib.quote("Old password incorrect.")
387
            req.status = req.HTTP_BAD_REQUEST
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
388
            # Cancel all the changes made to user (including setting new pass)
389
            req.store.rollback()
1078 by chadnickbok
Updated the settings page to require the old password
390
            return
391
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
392
    req.store.commit()
591 by mattgiuca
userservice: Added code to update req.session after updating the user who is
393
486 by mattgiuca
caps: Added a few new capabilities.
394
    req.content_type = "text/plain"
940 by wagrant
userservice: Repeat after me: I will not talk to code that runs as root
395
    req.write('')
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
396
552 by mattgiuca
userservice: Small fixes to update_user.
397
def handle_get_user(req, fields):
398
    """
399
    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.
400
    module is willing to give up, EXCEPT the following fields:
401
        svn_pass
552 by mattgiuca
userservice: Small fixes to update_user.
402
    """
403
    # Only give full powers if this user has CAP_GETUSER
404
    fullpowers = req.user.hasCap(caps.CAP_GETUSER)
405
406
    try:
407
        login = fields.getfirst('login')
408
        if login is None:
409
            raise AttributeError()
410
        if not fullpowers and login != req.user.login:
411
            # Not allowed to edit other users
412
            req.throw_error(req.HTTP_FORBIDDEN,
413
            "You do not have permission to see another user.")
414
    except AttributeError:
415
        # If login not specified, update yourself
416
        login = req.user.login
417
418
    # Just talk direct to the DB
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
419
    user = ivle.database.User.get_by_login(req.store, login)
420
    user = ivle.util.object_to_dict(user_fields_list, user)
674 by mattgiuca
userservice: Fixed encoding of User objects into JSON
421
    # 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
422
    for k in 'pass_exp', 'acct_exp', 'last_login':
423
        if user[k] is not None:
424
            user[k] = unicode(user[k])
425
554 by mattgiuca
userservice: Does not return svn_pass to the user.
426
    response = cjson.encode(user)
552 by mattgiuca
userservice: Small fixes to update_user.
427
    req.content_type = "text/plain"
428
    req.write(response)
429
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
430
def handle_get_enrolments(req, fields):
431
    """
995 by wagrant
common.db: Add get_enrolment_groups. Will return group information for
432
    Retrieve a user's enrolment details. Each enrolment includes any group
433
    memberships for that offering.
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
434
    """
435
    # For the moment we're only able to query ourselves
436
    fullpowers = False
437
    ## Only give full powers if this user has CAP_GETUSER
438
    ##fullpowers = req.user.hasCap(caps.CAP_GETUSER)
439
440
    try:
441
        login = fields.getfirst('login')
442
        if login is None:
443
            raise AttributeError()
444
        if not fullpowers and login != req.user.login:
445
            # Not allowed to edit other users
446
            req.throw_error(req.HTTP_FORBIDDEN,
447
            "You do not have permission to see another user's subjects.")
448
    except AttributeError:
449
        # If login not specified, update yourself
450
        login = req.user.login
451
452
    # Just talk direct to the DB
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
453
    db = ivle.db.DB()
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
454
    enrolments = db.get_enrolment(login)
995 by wagrant
common.db: Add get_enrolment_groups. Will return group information for
455
    for e in enrolments:
456
        e['groups'] = db.get_enrolment_groups(login, e['offeringid'])
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
457
    db.close()
458
    response = cjson.encode(enrolments)
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:
471
        req.throw_error(req.HTTP_BAD_REQUEST,
472
            "Required: subjectid")
473
    try:
474
        subjectid = int(subjectid)
475
    except:
476
        req.throw_error(req.HTTP_BAD_REQUEST,
477
            "subjectid must be a integer")
478
    
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
479
    db = ivle.db.DB()
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
480
    try:
481
        offerings = db.get_offering_semesters(subjectid)
482
    finally:
483
        db.close()
484
485
    response = cjson.encode([o for o in offerings if o['active']])
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:
498
        req.throw_error(req.HTTP_BAD_REQUEST,
499
            "Required: offeringid")
500
    try:
501
        offeringid = int(offeringid)
502
    except:
503
        req.throw_error(req.HTTP_BAD_REQUEST,
504
            "offeringid must be a integer")
505
    
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
506
    db = ivle.db.DB()
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
507
    try:
508
        projectsets = db.get_projectsets_by_offering(offeringid)
509
        for p in projectsets:
510
            p['groups'] = db.get_groups_by_projectset(p['projectsetid'])
511
    except Exception, e:
512
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR, repr(e))
513
    finally:
514
        db.close()
515
516
    response = cjson.encode(projectsets)
517
    req.write(response)
518
981 by dcoles
Groups: Added in support for creating groups in the database through
519
def handle_create_project_set(req, fields):
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
520
    """Required cap: CAP_MANAGEPROJECTS
521
    Creates a project set for a offering - returns the projectsetid
522
    Required:
523
        offeringid, max_students_per_group
524
    """
981 by dcoles
Groups: Added in support for creating groups in the database through
525
    
526
    if req.method != "POST":
527
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
528
            "Only POST requests are valid methods to create_user.")
529
    # Check if this user has CAP_MANAGEPROJECTS
530
    if not req.user.hasCap(caps.CAP_MANAGEPROJECTS):
531
        req.throw_error(req.HTTP_FORBIDDEN,
532
        "You do not have permission to manage projects.")
533
    # Get required fields
534
    offeringid = fields.getfirst('offeringid')
535
    max_students_per_group = fields.getfirst('max_students_per_group')
536
    if offeringid is None or max_students_per_group is None:
537
        req.throw_error(req.HTTP_BAD_REQUEST,
538
            "Required: offeringid, max_students_per_group")
539
540
    # Talk to the DB
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
541
    db = ivle.db.DB()
981 by dcoles
Groups: Added in support for creating groups in the database through
542
    dbquery = db.return_insert(
543
        {
544
            'offeringid': offeringid,
545
            'max_students_per_group': max_students_per_group,
546
        },
547
        "project_set",
548
        frozenset(["offeringid", "max_students_per_group"]),
549
        ["projectsetid"],
550
    )
551
    db.close()
552
    
553
    response = cjson.encode(dbquery.dictresult()[0])
554
555
    req.content_type = "text/plain"
556
    req.write(response)
557
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
558
def handle_create_project(req, fields):
559
    """Required cap: CAP_MANAGEPROJECTS
560
    Creates a project in a specific project set
561
    Required:
562
        projectsetid
563
    Optional:
564
        synopsis, url, deadline
565
    Returns:
566
        projectid
567
    """
568
    
569
    if req.method != "POST":
570
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
571
            "Only POST requests are valid methods to create_user.")
572
    # Check if this user has CAP_MANAGEPROJECTS
573
    if not req.user.hasCap(caps.CAP_MANAGEPROJECTS):
574
        req.throw_error(req.HTTP_FORBIDDEN,
575
        "You do not have permission to manage projects.")
576
    # Get required fields
577
    projectsetid = fields.getfirst('projectsetid')
578
    if projectsetid is None:
579
        req.throw_error(req.HTTP_BAD_REQUEST,
580
            "Required: projectsetid")
581
    # Get optional fields
582
    synopsis = fields.getfirst('synopsis')
583
    url = fields.getfirst('url')
584
    deadline = fields.getfirst('deadline')
585
    if deadline is not None:
586
        try:
587
            deadline = util.parse_iso8601(deadline).timetuple()
588
        except ValueError, e:
589
            req.throw_error(req.HTTP_BAD_REQUEST, e.message)
590
591
    # Talk to the DB
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
592
    db = ivle.db.DB()
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
593
    try:
594
        dbquery = db.return_insert(
595
            {
596
                'projectsetid': projectsetid,
597
                'synopsis': synopsis,
598
                'url': url,
599
                'deadline': deadline,
600
            },
601
            "project", # table
602
            frozenset(["projectsetid", "synopsis", "url", "deadline"]),
603
            ["projectid"], # returns
604
        )
605
    except Exception, e:
606
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR, repr(e))
607
    finally:
608
        db.close()
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
609
    
610
    response = cjson.encode(dbquery.dictresult()[0])
611
612
    req.content_type = "text/plain"
613
    req.write(response)
614
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
615
def handle_create_group(req, fields):
616
    """Required cap: CAP_MANAGEGROUPS
617
    Creates a project group in a specific project set
618
    Required:
619
        projectsetid, groupnm
620
    Optional:
621
        nick
622
    Returns:
623
        groupid
624
    """
625
    if req.method != "POST":
626
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
627
            "Only POST requests are valid methods to create_user.")
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
628
    # Check if this is allowed to manage groups
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
629
    if not req.user.hasCap(caps.CAP_MANAGEGROUPS):
630
        req.throw_error(req.HTTP_FORBIDDEN,
631
        "You do not have permission to manage groups.")
632
    # Get required fields
633
    projectsetid = fields.getfirst('projectsetid')
634
    groupnm = fields.getfirst('groupnm')
635
    if projectsetid is None or groupnm is None:
636
        req.throw_error(req.HTTP_BAD_REQUEST,
637
            "Required: projectsetid, groupnm")
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
638
    try:
639
        projectsetid = int(projectsetid)
640
    except:
641
        req.throw_error(req.HTTP_BAD_REQUEST,
642
            "projectsetid must be an int")
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
643
    # Get optional fields
644
    nick = fields.getfirst('nick')
645
646
    # Talk to the DB
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
647
    db = ivle.db.DB()
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
648
    # Other fields
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
649
    createdby = req.user.id
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
650
    epoch = time.localtime()
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
651
652
    # Begin transaction since things can go wrong
653
    db.start_transaction()
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
654
    try:
655
        dbquery = db.return_insert(
656
            {
657
                'groupnm': groupnm,
658
                'projectsetid': projectsetid,
659
                'nick': nick,
660
                'createdby': createdby,
661
                'epoch': epoch,
662
            },
663
            "project_group", # table
664
            frozenset(["groupnm", "projectsetid", "nick", "createdby",
665
                "epoch"]), # fields
666
            ["groupid"], # returns
667
        )
668
 
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
669
        singlerow = dbquery.dictresult()[0]
670
        groupid = singlerow['groupid']
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
671
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
672
        # Create the groups repository
673
        # Get the arguments for usermgt.activate_user from the session
674
        # (The user must have already logged in to use this app)
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
675
    
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
676
        # Find the rest of the parameters we need
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
677
        offeringinfo = db.get_offering_info(projectsetid)
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
678
                
679
        subj_short_name = offeringinfo['subj_short_name']
680
        year = offeringinfo['year']
681
        semester = offeringinfo['semester']
682
683
        args = {
684
            "subj_short_name": subj_short_name,
685
            "year": year,
686
            "semester": semester,
687
            "groupnm": groupnm,
688
        }
689
        msg = {'create_group_repository': args}
690
691
        # Contact the usrmgt server
692
        try:
693
            usrmgt = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic)
694
        except cjson.DecodeError, e:
695
            req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
696
                "Could not understand usrmgt server response: %s"%e.message)
697
698
        if 'response' not in usrmgt or usrmgt['response']=='failure':
699
            req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
700
                "Failure creating repository: %s"%str(usrmgt))
701
    
702
        # Everything went OK. Lock it in
703
        db.commit()
704
705
    except Exception, e:
706
        db.rollback()
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
707
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR, repr(e))
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
708
    finally:
709
        db.close()
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
710
711
    response = cjson.encode(singlerow)
712
713
    req.content_type = "text/plain"
714
    req.write(response)
981 by dcoles
Groups: Added in support for creating groups in the database through
715
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
716
def handle_get_group_membership(req, fields):
717
    """ Required cap: None
718
    Returns two lists. One of people in the group and one of people not in the 
719
    group (but enroled in the offering)
720
    Required:
721
        groupid, offeringid
722
    """
723
    # Get required fields
724
    groupid = fields.getfirst('groupid')
725
    offeringid = fields.getfirst('offeringid')
726
    if groupid is None or offeringid is None:
727
        req.throw_error(req.HTTP_BAD_REQUEST,
728
            "Required: groupid, offeringid")
729
    try:
730
        groupid = int(groupid)
731
    except:
732
        req.throw_error(req.HTTP_BAD_REQUEST,
733
            "groupid must be an int")
734
    try:
735
        offeringid = int(offeringid)
736
    except:
737
        req.throw_error(req.HTTP_BAD_REQUEST,
738
            "offeringid must be an int")
739
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
740
    db = ivle.db.DB()
1004 by dcoles
Groups: Now you can add people to a group as well as creating groups from the
741
    try:
742
        offeringmembers = db.get_offering_members(offeringid)
743
        groupmembers = db.get_projectgroup_members(groupid)
744
    finally:
745
        db.close()
746
    
747
    # Make sure we don't include members in both lists
748
    for member in groupmembers:
749
        if member in offeringmembers:
750
            offeringmembers.remove(member)
751
752
    response = cjson.encode(
753
        {'groupmembers': groupmembers, 'available': offeringmembers})
754
755
    req.content_type = "text/plain"
756
    req.write(response)
757
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
758
def handle_assign_group(req, fields):
759
    """ Required cap: CAP_MANAGEGROUPS
760
    Assigns a user to a project group
761
    Required:
762
        login, groupid
763
    """
764
    if req.method != "POST":
765
        req.throw_error(req.HTTP_METHOD_NOT_ALLOWED,
766
            "Only POST requests are valid methods to create_user.")
767
    # Check if this user is allowed to manage groups
768
    if not req.user.hasCap(caps.CAP_MANAGEGROUPS):
769
        req.throw_error(req.HTTP_FORBIDDEN,
770
        "You do not have permission to manage groups.")
771
    # Get required fields
772
    login = fields.getfirst('login')
773
    groupid = fields.getfirst('groupid')
774
    if login is None or groupid is None:
775
        req.throw_error(req.HTTP_BAD_REQUEST,
776
            "Required: login, groupid")
777
    groupid = int(groupid)
778
1080.1.7 by matt.giuca
The new ivle.database.User class is now used in Request and usrmgt, which
779
    loginid = ivle.database.User.get_by_login(req.store, login).id
780
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
781
    # Talk to the DB
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
782
    db = ivle.db.DB()
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
783
784
    # Add assignment to database
1011 by wagrant
userservice: Don't wrap group membership creation in a transaction, or
785
    # We can't use a transaction here, as usrmgt-server needs to see the
786
    # changes!
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
787
    try:
788
        dbquery = db.insert(
789
            {
790
                'loginid': loginid,
791
                'groupid': groupid,
792
            },
793
            "group_member", # table
794
            frozenset(["loginid", "groupid"]), # fields
795
        )
1006 by dcoles
Groups: Use db transactions for risky usermanagement operations. (Such as when
796
797
        # Rebuild the svn config file
798
        # Contact the usrmgt server
799
        msg = {'rebuild_svn_group_config': {}}
800
        try:
801
            usrmgt = chat.chat(usrmgt_host, usrmgt_port, msg, usrmgt_magic)
802
        except cjson.DecodeError, e:
803
            req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
804
                "Could not understand usrmgt server response: %s"%e.message)
805
806
            if 'response' not in usrmgt or usrmgt['response']=='failure':
807
                req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR,
808
                    "Failure creating repository: %s"%str(usrmgt))
809
    except Exception, e:
810
        req.throw_error(req.HTTP_INTERNAL_SERVER_ERROR, repr(e))
811
    finally:
812
        db.close()
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
813
814
    return(cjson.encode({'response': 'okay'}))
981 by dcoles
Groups: Added in support for creating groups in the database through
815
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
816
# Map action names (from the path)
817
# to actual function objects
818
actions_map = {
819
    "activate_me": handle_activate_me,
820
    "create_user": handle_create_user,
821
    "update_user": handle_update_user,
552 by mattgiuca
userservice: Small fixes to update_user.
822
    "get_user": handle_get_user,
930 by dcoles
Userservice: Added get_enrolments handler to allow a JSON query of a students
823
    "get_enrolments": handle_get_enrolments,
1001 by dcoles
Groups: Added the view half of the group admin panel. This lets people with the
824
    "get_active_offerings": handle_get_active_offerings,
825
    "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
826
    "get_group_membership": handle_get_group_membership,
981 by dcoles
Groups: Added in support for creating groups in the database through
827
    "create_project_set": handle_create_project_set,
984 by dcoles
Groups: Added userservice/create_project call. You can now create a project in
828
    "create_project": handle_create_project,
986 by dcoles
Groups: Added userservice/create_group call. You can now create a project in
829
    "create_group": handle_create_group,
989 by dcoles
Groups: Added userservice/assign_group call. This allows a user with the
830
    "assign_group": handle_assign_group,
536 by mattgiuca
apps/userservice: Generalised searching for actions (dict mapping action names to
831
}