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

« back to all changes in this revision

Viewing changes to lib/common/db.py

  • Committer: dcoles
  • Date: 2008-04-03 21:59:40 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:721
download/serve: Removed the setting of mime-type here. If it's a download file 
server will set the mimetype to application/octet-stream or application/zip and  
mark it as "attachment" while if it's a serve file then server will only set it 
to the recognised mimetype.

Show diffs side-by-side

added added

removed removed

Lines of Context:
35
35
import conf
36
36
import md5
37
37
import copy
 
38
import time
38
39
 
39
40
from common import (caps, user)
40
41
 
 
42
TIMESTAMP_FORMAT = '%Y-%m-%d %H:%M:%S'
 
43
 
41
44
def _escape(val):
42
45
    """Wrapper around pg.escape_string. Prepares the Python value for use in
43
46
    SQL. Returns a string, which may be safely placed verbatim into an SQL
58
61
    # WARNING: PostgreSQL-specific code
59
62
    if val is None:
60
63
        return "NULL"
61
 
    elif isinstance(val, str):
 
64
    elif isinstance(val, str) or isinstance(val, unicode):
62
65
        return "E'" + pg.escape_string(val) + "'"
63
66
    elif isinstance(val, bool):
64
67
        return "TRUE" if val else "FALSE"
67
70
        return str(val)
68
71
    elif isinstance(val, caps.Role):
69
72
        return _escape(str(val))
 
73
    elif isinstance(val, time.struct_time):
 
74
        return _escape(time.strftime(TIMESTAMP_FORMAT, val))
70
75
    else:
71
76
        raise DBException("Attempt to insert an unsupported type "
72
77
            "into the database")
140
145
        Raises a DBException if the dictionary contains invalid fields.
141
146
        """
142
147
        if not DB.check_dict(dict, tablefields, disallowed):
143
 
            raise DBException("Supplied dictionary contains invalid fields.")
 
148
            extras = set(dict.keys()) - tablefields
 
149
            raise DBException("Supplied dictionary contains invalid fields. (%s)" % (repr(extras)))
144
150
        # Build two lists concurrently: field names and values, as SQL strings
145
151
        fieldnames = []
146
152
        values = []
172
178
        """
173
179
        if (not (DB.check_dict(primarydict, primary_keys, must=True)
174
180
            and DB.check_dict(updatedict, tablefields, disallowed_update))):
175
 
            raise DBException("Supplied dictionary contains invalid or "
176
 
                " missing fields.")
 
181
            raise DBException("Supplied dictionary contains invalid or missing fields (1).")
177
182
        # Make a list of SQL fragments of the form "field = 'new value'"
178
183
        # These fragments are ALREADY-ESCAPED
179
184
        setlist = []
199
204
        primarydict, tablename, primary_keys: See update.
200
205
        """
201
206
        if not DB.check_dict(primarydict, primary_keys, must=True):
202
 
            raise DBException("Supplied dictionary contains invalid or "
203
 
                " missing fields.")
 
207
            raise DBException("Supplied dictionary contains invalid or missing fields (2).")
204
208
        wherelist = []
205
209
        for k,v in primarydict.items():
206
210
            wherelist.append("%s = %s" % (k, _escape(v)))
225
229
            primary_keys is indeed the primary key).
226
230
        """
227
231
        if not DB.check_dict(primarydict, primary_keys, must=True):
228
 
            raise DBException("Supplied dictionary contains invalid or "
229
 
                " missing fields.")
 
232
            raise DBException("Supplied dictionary contains invalid or missing fields (3).")
230
233
        wherelist = []
231
234
        for k,v in primarydict.items():
232
235
            wherelist.append("%s = %s" % (k, _escape(v)))
289
292
    login_primary = frozenset(["login"])
290
293
    login_fields_list = [
291
294
        "login", "passhash", "state", "unixid", "email", "nick", "fullname",
292
 
        "rolenm", "studentid", "acct_exp", "pass_exp", "last_login"
 
295
        "rolenm", "studentid", "acct_exp", "pass_exp", "last_login", "svn_pass"
293
296
    ]
294
297
    login_fields = frozenset(login_fields_list)
295
 
    # Do not return passhash when reading from the DB
296
 
    login_getfields = login_fields - frozenset(["passhash"])
297
298
 
298
 
    def create_user(self, dry=False, **kwargs):
 
299
    def create_user(self, user_obj=None, dry=False, **kwargs):
299
300
        """Creates a user login entry in the database.
 
301
        Two ways to call this - passing a user object, or passing
 
302
        all fields as separate arguments.
 
303
 
 
304
        Either pass a "user_obj" as the first argument (in which case other
 
305
        fields will be ignored), or pass all fields as arguments.
 
306
 
300
307
        All user fields are to be passed as args. The argument names
301
308
        are the field names of the "login" table of the DB schema.
302
309
        However, instead of supplying a "passhash", you must supply a
307
314
        invalid keys or is missing required keys.
308
315
        """
309
316
        if 'passhash' in kwargs:
310
 
            raise DBException("Supplied arguments include passhash (invalid).")
 
317
            raise DBException("Supplied arguments include passhash (invalid) (1).")
311
318
        # Make a copy of the dict. Change password to passhash (hashing it),
312
319
        # and set 'state' to "no_agreement".
313
 
        kwargs = copy.copy(kwargs)
314
 
        if 'password' in kwargs:
315
 
            kwargs['passhash'] = _passhash(kwargs['password'])
316
 
            del kwargs['password']
317
 
        kwargs['state'] = "no_agreement"
 
320
        if user_obj is None:
 
321
            # Use the kwargs
 
322
            fields = copy.copy(kwargs)
 
323
        else:
 
324
            # Use the user object
 
325
            fields = dict(user_obj)
 
326
        if 'password' in fields:
 
327
            fields['passhash'] = _passhash(fields['password'])
 
328
            del fields['password']
 
329
        if 'role' in fields:
 
330
            # Convert role to rolenm
 
331
            fields['rolenm'] = str(user_obj.role)
 
332
            del fields['role']
 
333
        if user_obj is None:
 
334
            fields['state'] = "no_agreement"
 
335
            # else, we'll trust the user, but it SHOULD be "no_agreement"
 
336
            # (We can't change it because then the user object would not
 
337
            # reflect the DB).
 
338
        if 'local_password' in fields:
 
339
            del fields['local_password']
318
340
        # Execute the query.
319
 
        return self.insert(kwargs, "login", self.login_fields, dry=dry)
 
341
        return self.insert(fields, "login", self.login_fields, dry=dry)
320
342
 
321
343
    def update_user(self, login, dry=False, **kwargs):
322
344
        """Updates fields of a particular user. login is the name of the user
333
355
        with a new one.
334
356
        """
335
357
        if 'passhash' in kwargs:
336
 
            raise DBException("Supplied arguments include passhash (invalid).")
 
358
            raise DBException("Supplied arguments include passhash (invalid) (2).")
337
359
        if "password" in kwargs:
338
360
            kwargs = copy.copy(kwargs)
339
361
            kwargs['passhash'] = _passhash(kwargs['password'])
349
371
        Raises a DBException if the login is not found in the DB.
350
372
        """
351
373
        userdict = self.get_single({"login": login}, "login",
352
 
            self.login_getfields, self.login_primary,
 
374
            self.login_fields, self.login_primary,
353
375
            error_notfound="get_user: No user with that login name", dry=dry)
354
376
        if dry:
355
377
            return userdict     # Query string
359
381
    def get_users(self, dry=False):
360
382
        """Returns a list of all users in the DB, as User objects.
361
383
        """
362
 
        userdicts = self.get_all("login", self.login_getfields, dry=dry)
 
384
        userdicts = self.get_all("login", self.login_fields, dry=dry)
363
385
        if dry:
364
386
            return userdicts    # Query string
365
387
        # Package into User objects
366
388
        return [user.User(**userdict) for userdict in userdicts]
367
389
 
 
390
    def get_user_loginid(self, login, dry=False):
 
391
        """Given a login, returns the integer loginid for this user.
 
392
 
 
393
        Raises a DBException if the login is not found in the DB.
 
394
        """
 
395
        userdict = self.get_single({"login": login}, "login",
 
396
            ['loginid'], self.login_primary,
 
397
            error_notfound="get_user_loginid: No user with that login name",
 
398
            dry=dry)
 
399
        if dry:
 
400
            return userdict     # Query string
 
401
        return userdict['loginid']
 
402
 
368
403
    def user_authenticate(self, login, password, dry=False):
369
404
        """Performs a password authentication on a user. Returns True if
370
405
        "passhash" is the correct passhash for the given login, False
371
 
        otherwise.
 
406
        if the passhash does not match the password in the DB,
 
407
        and None if the passhash in the DB is NULL.
372
408
        Also returns False if the login does not exist (so if you want to
373
409
        differentiate these cases, use get_user and catch an exception).
374
410
        """
375
 
        query = ("SELECT login FROM login "
376
 
            "WHERE login = '%s' AND passhash = %s;"
377
 
            % (login, _escape(_passhash(password))))
378
 
        if dry: return query
379
 
        result = self.db.query(query)
380
 
        # If one row was returned, succeed.
381
 
        # Otherwise, fail to authenticate.
382
 
        return result.ntuples() == 1
 
411
        query = ("SELECT passhash FROM login WHERE login = %s;"
 
412
            % _escape(login))
 
413
        if dry: return query
 
414
        result = self.db.query(query)
 
415
        if result.ntuples() == 1:
 
416
            # Valid username. Check password.
 
417
            passhash = result.getresult()[0][0]
 
418
            if passhash is None:
 
419
                return None
 
420
            return _passhash(password) == passhash
 
421
        else:
 
422
            return False
 
423
 
 
424
    # PROBLEM AND PROBLEM ATTEMPT FUNCTIONS #
 
425
 
 
426
    def get_problem_problemid(self, exercisename, dry=False):
 
427
        """Given an exercise name, returns the associated problemID.
 
428
        If the exercise name is NOT in the database, it inserts it and returns
 
429
        the new problemID. Hence this may mutate the DB, but is idempotent.
 
430
        """
 
431
        try:
 
432
            d = self.get_single({"identifier": exercisename}, "problem",
 
433
                ['problemid'], frozenset(["identifier"]),
 
434
                dry=dry)
 
435
            if dry:
 
436
                return d        # Query string
 
437
        except DBException:
 
438
            if dry:
 
439
                # Shouldn't try again, must have failed for some other reason
 
440
                raise
 
441
            # if we failed to get a problemid, it was probably because
 
442
            # the exercise wasn't in the db. So lets insert it!
 
443
            #
 
444
            # The insert can fail if someone else simultaneously does
 
445
            # the insert, so if the insert fails, we ignore the problem. 
 
446
            try:
 
447
                self.insert({'identifier': exercisename}, "problem",
 
448
                        frozenset(['identifier']))
 
449
            except Exception, e:
 
450
                pass
 
451
 
 
452
            # Assuming the insert succeeded, we should be able to get the
 
453
            # problemid now.
 
454
            d = self.get_single({"identifier": exercisename}, "problem",
 
455
                ['problemid'], frozenset(["identifier"]))
 
456
 
 
457
        return d['problemid']
 
458
 
 
459
    def insert_problem_attempt(self, login, exercisename, date, complete,
 
460
        attempt, dry=False):
 
461
        """Inserts the details of a problem attempt into the database.
 
462
        exercisename: Name of the exercise. (identifier field of problem
 
463
            table). If this exercise does not exist, also creates a new row in
 
464
            the problem table for this exercise name.
 
465
        login: Name of the user submitting the attempt. (login field of the
 
466
            login table).
 
467
        date: struct_time, the date this attempt was made.
 
468
        complete: bool. Whether the test passed or not.
 
469
        attempt: Text of the attempt.
 
470
 
 
471
        Note: Even if dry, will still physically call get_problem_problemid,
 
472
        which may mutate the DB, and get_user_loginid, which may fail.
 
473
        """
 
474
        problemid = self.get_problem_problemid(exercisename)
 
475
        loginid = self.get_user_loginid(login)  # May raise a DBException
 
476
 
 
477
        return self.insert({
 
478
                'problemid': problemid,
 
479
                'loginid': loginid,
 
480
                'date': date,
 
481
                'complete': complete,
 
482
                'attempt': attempt,
 
483
            }, 'problem_attempt',
 
484
            frozenset(['problemid','loginid','date','complete','attempt']),
 
485
            dry=dry)
 
486
 
 
487
    def write_problem_save(self, login, exercisename, date, text, dry=False):
 
488
        """Writes text to the problem_save table (for when the user saves an
 
489
        exercise). Creates a new row, or overwrites an existing one if the
 
490
        user has already saved that problem.
 
491
        (Unlike problem_attempt, does not keep historical records).
 
492
        """
 
493
        problemid = self.get_problem_problemid(exercisename)
 
494
        loginid = self.get_user_loginid(login)  # May raise a DBException
 
495
 
 
496
        try:
 
497
            return self.insert({
 
498
                    'problemid': problemid,
 
499
                    'loginid': loginid,
 
500
                    'date': date,
 
501
                    'text': text,
 
502
                }, 'problem_save',
 
503
                frozenset(['problemid','loginid','date','text']),
 
504
                dry=dry)
 
505
        except pg.ProgrammingError:
 
506
            # May have failed because this problemid/loginid row already
 
507
            # exists (they have a unique key constraint).
 
508
            # Do an update instead.
 
509
            if dry:
 
510
                # Shouldn't try again, must have failed for some other reason
 
511
                raise
 
512
            self.update({
 
513
                    'problemid': problemid,
 
514
                    'loginid': loginid,
 
515
                },
 
516
                {
 
517
                    'date': date,
 
518
                    'text': text,
 
519
                }, "problem_save",
 
520
                frozenset(['date', 'text']),
 
521
                frozenset(['problemid', 'loginid']))
 
522
 
 
523
    def get_problem_stored_text(self, login, exercisename, dry=False):
 
524
        """Given a login name and exercise name, returns the text of the
 
525
        last saved/submitted attempt for this question.
 
526
        Returns None if the user has not saved or made an attempt on this
 
527
        problem.
 
528
        (If the user has both saved and submitted, it returns whichever was
 
529
        made last).
 
530
 
 
531
        Note: Even if dry, will still physically call get_problem_problemid,
 
532
        which may mutate the DB, and get_user_loginid, which may fail.
 
533
        """
 
534
        problemid = self.get_problem_problemid(exercisename)
 
535
        loginid = self.get_user_loginid(login)  # May raise a DBException
 
536
        # This very complex query finds all submissions made by this user for
 
537
        # this problem, as well as the save made by this user for this
 
538
        # problem, and returns the text of the newest one.
 
539
        # (Whichever is newer out of the save or the submit).
 
540
        query = """SELECT text FROM
 
541
    (
 
542
        (SELECT * FROM problem_save WHERE loginid = %d AND problemid = %d)
 
543
    UNION
 
544
        (SELECT problemid, loginid, date, text FROM problem_attempt
 
545
         AS problem_attempt (problemid, loginid, date, text)
 
546
         WHERE loginid = %d AND problemid = %d)
 
547
    )
 
548
    AS _
 
549
    ORDER BY date DESC
 
550
    LIMIT 1;""" % (loginid, problemid, loginid, problemid)
 
551
        if dry: return query
 
552
        result = self.db.query(query)
 
553
        if result.ntuples() == 1:
 
554
            # The user has made at least 1 attempt. Return the newest.
 
555
            return result.getresult()[0][0]
 
556
        else:
 
557
            return None
 
558
 
 
559
    def get_problem_status(self, login, exercisename, dry=False):
 
560
        """Given a login name and exercise name, returns information about the
 
561
        user's performance on that problem.
 
562
        Returns a tuple of:
 
563
            - A boolean, whether they have successfully passed this exercise.
 
564
            - An int, the number of attempts they have made up to and
 
565
              including the first successful attempt (or the total number of
 
566
              attempts, if not yet successful).
 
567
        """
 
568
        problemid = self.get_problem_problemid(exercisename)
 
569
        loginid = self.get_user_loginid(login)  # May raise a DBException
 
570
 
 
571
        # ASSUME that it is completed, get the total number of attempts up to
 
572
        # and including the first successful attempt.
 
573
        # (Get the date of the first successful attempt. Then count the number
 
574
        # of attempts made <= that date).
 
575
        # Will return an empty table if the problem has never been
 
576
        # successfully completed.
 
577
        query = """SELECT COUNT(*) FROM problem_attempt
 
578
    WHERE loginid = %d AND problemid = %d AND date <=
 
579
        (SELECT date FROM problem_attempt
 
580
            WHERE loginid = %d AND problemid = %d AND complete = TRUE
 
581
            ORDER BY date ASC
 
582
            LIMIT 1);""" % (loginid, problemid, loginid, problemid)
 
583
        if dry: return query
 
584
        result = self.db.query(query)
 
585
        count = int(result.getresult()[0][0])
 
586
        if count > 0:
 
587
            # The user has made at least 1 successful attempt.
 
588
            # Return True for success, and the number of attempts up to and
 
589
            # including the successful one.
 
590
            return (True, count)
 
591
        else:
 
592
            # Returned 0 rows - this indicates that the problem has not been
 
593
            # completed.
 
594
            # Return the total number of attempts, and False for success.
 
595
            query = """SELECT COUNT(*) FROM problem_attempt
 
596
    WHERE loginid = %d AND problemid = %d;""" % (loginid, problemid)
 
597
            result = self.db.query(query)
 
598
            count = int(result.getresult()[0][0])
 
599
            return (False, count)
383
600
 
384
601
    def close(self):
385
602
        """Close the DB connection. Do not call any other functions after