Skip to content

Commit

Permalink
fix: quiz is broken if user is not logged in (hedyorg#1192)
Browse files Browse the repository at this point in the history
If the user is not logged in, the username is `''`.

First of all, DynamoDB rejects this value with an error.

Second of all, if we would record all anonymous users under the same
key, the data partition would become too big.

Instead, we'll record the responses under a username like
`'anonymous:e91f5ac2a6c946e893683c5c02781117'`.
  • Loading branch information
rix0rrr authored Nov 7, 2021
1 parent 1bec5ee commit e131ed0
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 10 deletions.
7 changes: 5 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,9 +603,12 @@ def submit_answer(level_source, question_nr, attempt):

is_correct = quiz.is_correct_answer(question, chosen_letter)

# Store the answer in the database
# Store the answer in the database. If we don't have a username,
# use the session ID as a username.
username = current_user()['username'] or f'anonymous:{session_id()}'

DATABASE.record_quiz_answer(session['quiz-attempt-id'],
username=current_user()['username'],
username=username,
level=level_source,
is_correct=is_correct,
question_number=question_nr,
Expand Down
11 changes: 10 additions & 1 deletion utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,20 @@ def wrapper(*args, **kwargs):
return wrapper



def timems():
"""Return the UNIX timestamp in milliseconds.
You only need to use this function if you are doing performance-sensitive
timing. Otherwise, `times` (which returns second-resolution) is probably
a better choice.
"""
return int(round(time.time() * 1000))

def times():
"""Return the UNIX timestamp in seconds.
If you need to store a date/time in the database, prefer this function.
"""
return int(round(time.time()))


Expand Down
4 changes: 2 additions & 2 deletions website/database.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from utils import timems
from utils import timems, times
from . import dynamo

storage = dynamo.AwsDynamoStorage.from_env() or dynamo.MemoryStorage('dev_database.json')
Expand Down Expand Up @@ -43,7 +43,7 @@ def record_quiz_answer(self, attempt_id, username, level, question_number, answe
updates = {
"attemptId": attempt_id,
"level": level,
"date": timems(),
"date": times(),
"q" + str(question_number): dynamo.DynamoAddToList(answer),
}

Expand Down
19 changes: 14 additions & 5 deletions website/dynamo.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ def create(self, data):
"""Put a single complete record into the database."""
if self.partition_key not in data:
raise ValueError(f"Expecting '{self.partition_key}' field in create() call, got: {data}")
if self.sort_key and self.sort_key not in data:
raise ValueError(f"Expecting '{self.sort_key}' field in create() call, got: {data}")

querylog.log_counter(f'db_create:{self.table_name}')
self.storage.put(self.table_name, self._extract_key(data), data)
Expand All @@ -99,8 +101,7 @@ def update(self, key, updates):
updates that aren't representable as plain values.
"""
querylog.log_counter(f'db_update:{self.table_name}')
if key.keys() != self._key_names():
raise RuntimeError(f'update: key fields incorrect: {key} != {self._key_names()}')
self._validate_key(key)

return self.storage.update(self.table_name, key, updates)

Expand All @@ -111,8 +112,7 @@ def delete(self, key):
Returns the delete item.
"""
querylog.log_counter('db_del:' + self.table_name)
if key.keys() != self._key_names():
raise RuntimeError(f'update: key fields incorrect: {key} != {self._key_names()}')
self._validate_key(key)

return self.storage.delete(self.table_name, key)

Expand All @@ -138,7 +138,7 @@ def del_many (self, key):
def scan(self):
"""Reads the entire table into memory."""
querylog.log_counter('db_scan:' + self.table_name)
return self.storage.scan (self.table_name)
return self.storage.scan(self.table_name)


@querylog.timed_as('db_describe')
Expand All @@ -147,6 +147,9 @@ def item_count(self):
return self.storage.item_count(self.table_name)

def _determine_lookup(self, key_data, many):
if any(not v for v in key_data.values()):
raise ValueError(f'Key data cannot have empty values: {key_data}')

keys = set(key_data.keys())
expected_keys = self._key_names()

Expand Down Expand Up @@ -186,6 +189,12 @@ def _extract_key(self, data):
def _key_names(self):
return set(x for x in [self.partition_key, self.sort_key] if x is not None)

def _validate_key(self, key):
if key.keys() != self._key_names():
raise RuntimeError(f'key fields incorrect: {key} != {self._key_names()}')
if any(not v for v in key.values()):
raise RuntimeError(f'key fields cannot be empty: {key}')

DDB_SERIALIZER = TypeSerializer()
DDB_DESERIALIZER = TypeDeserializer()

Expand Down

0 comments on commit e131ed0

Please sign in to comment.