359
381
def get_users(self, dry=False):
360
382
"""Returns a list of all users in the DB, as User objects.
362
userdicts = self.get_all("login", self.login_getfields, dry=dry)
384
userdicts = self.get_all("login", self.login_fields, dry=dry)
364
386
return userdicts # Query string
365
387
# Package into User objects
366
388
return [user.User(**userdict) for userdict in userdicts]
390
def get_user_loginid(self, login, dry=False):
391
"""Given a login, returns the integer loginid for this user.
393
Raises a DBException if the login is not found in the DB.
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",
400
return userdict # Query string
401
return userdict['loginid']
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
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).
375
query = ("SELECT login FROM login "
376
"WHERE login = '%s' AND passhash = %s;"
377
% (login, _escape(_passhash(password))))
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;"
414
result = self.db.query(query)
415
if result.ntuples() == 1:
416
# Valid username. Check password.
417
passhash = result.getresult()[0][0]
420
return _passhash(password) == passhash
424
# PROBLEM AND PROBLEM ATTEMPT FUNCTIONS #
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.
432
d = self.get_single({"identifier": exercisename}, "problem",
433
['problemid'], frozenset(["identifier"]),
436
return d # Query string
439
# Shouldn't try again, must have failed for some other reason
441
# if we failed to get a problemid, it was probably because
442
# the exercise wasn't in the db. So lets insert it!
444
# The insert can fail if someone else simultaneously does
445
# the insert, so if the insert fails, we ignore the problem.
447
self.insert({'identifier': exercisename}, "problem",
448
frozenset(['identifier']))
452
# Assuming the insert succeeded, we should be able to get the
454
d = self.get_single({"identifier": exercisename}, "problem",
455
['problemid'], frozenset(["identifier"]))
457
return d['problemid']
459
def insert_problem_attempt(self, login, exercisename, date, complete,
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
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.
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.
474
problemid = self.get_problem_problemid(exercisename)
475
loginid = self.get_user_loginid(login) # May raise a DBException
478
'problemid': problemid,
481
'complete': complete,
483
}, 'problem_attempt',
484
frozenset(['problemid','loginid','date','complete','attempt']),
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).
493
problemid = self.get_problem_problemid(exercisename)
494
loginid = self.get_user_loginid(login) # May raise a DBException
498
'problemid': problemid,
503
frozenset(['problemid','loginid','date','text']),
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.
510
# Shouldn't try again, must have failed for some other reason
513
'problemid': problemid,
520
frozenset(['date', 'text']),
521
frozenset(['problemid', 'loginid']))
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
528
(If the user has both saved and submitted, it returns whichever was
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.
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
542
(SELECT * FROM problem_save WHERE loginid = %d AND problemid = %d)
544
(SELECT problemid, loginid, date, text FROM problem_attempt
545
AS problem_attempt (problemid, loginid, date, text)
546
WHERE loginid = %d AND problemid = %d)
550
LIMIT 1;""" % (loginid, problemid, loginid, problemid)
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]
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.
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).
568
problemid = self.get_problem_problemid(exercisename)
569
loginid = self.get_user_loginid(login) # May raise a DBException
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
582
LIMIT 1);""" % (loginid, problemid, loginid, problemid)
584
result = self.db.query(query)
585
count = int(result.getresult()[0][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.
592
# Returned 0 rows - this indicates that the problem has not been
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)
385
602
"""Close the DB connection. Do not call any other functions after