import random
from typing import TypeVar
from tim_common.utils import Missing
[docs]def qst_rand_array(
max_count: int,
randoms: int,
seed_word: str,
random_seed: int = 0,
locks: int | list[int] | None = None,
) -> list[int]:
"""
Get array of count integers between 1 and max_count (incl.) using word and extra number as seed.
:param max_count: highest possible number (incl.) and max return list length
:param randoms: how many random numbers to fill the array with
:param seed_word: input word to generate random seed
:param random_seed: extra number to edit the seed
:param locks: positions that can't be shuffled, indexing starting from 1. Any position over max_count will be
interpreted as max_count
:return: shuffled array of integers of up to max_count values
"""
if locks is None:
locks = []
if isinstance(locks, int):
locks = [locks]
for i, val in enumerate(locks):
if val > max_count:
locks[i] = max_count
elif val < 1:
locks[i] = 1
locks = list(set(locks))
locks.sort()
total = randoms + len(locks)
if total > max_count:
total = max_count
ret: list[int] = []
seed_array = []
orig = list(range(1, max_count + 1))
for i, val in enumerate(locks):
if len(orig) == 0:
break
if val > max_count:
orig.pop(len(orig) - 1)
try:
orig.pop(val - 1 - i)
except IndexError:
pass
# Temp seed generator
for char in seed_word:
try:
seed_array.append(int(char, 36))
except ValueError:
pass
seed = int("".join(map(str, seed_array)))
random.seed(seed + random_seed)
random.shuffle(orig)
for i in range(1, total + 1):
if len(locks) >= total - len(ret):
ret.append(locks[0])
locks.pop(0)
elif len(locks) > 0 and locks[0] == i:
ret.append(i)
locks.pop(0)
else:
ret.append(orig.pop(0))
return ret
T = TypeVar("T")
[docs]def qst_set_array_order(arr: list[T], order_array: list[int]) -> list[T]:
"""
pick items from arr in order given by order_array
indices start from 1
"""
ret = []
for val in order_array:
try:
ret.append(arr[val - 1])
except IndexError:
pass
return ret
[docs]def qst_pick_expls(orig_expls: dict[str, T], order_array: list[int]) -> dict[str, T]:
"""
pick items from dict where keys are str converted integers in order given by order_array
indices start from 1
"""
if orig_expls is None:
orig_expls = {}
ret = {}
for i, val in enumerate(order_array):
pos = str(val)
picked = orig_expls.get(pos)
if picked is not None:
ret[str(i + 1)] = picked
return ret
[docs]def create_points_table(points: str) -> list[dict[str, float]]:
points_table = []
if points and points != "":
points = str(points)
points_split = points.split("|")
for row in points_split:
row_points = row.split(";")
row_points_dict = {}
for col in row_points:
if col != "":
col_points = col.split(":", 2)
if len(col_points) == 1:
row_points_dict[col_points[0]] = 1.0
else:
try:
row_points_dict[col_points[0]] = float(col_points[1])
except ValueError:
pass
points_table.append(row_points_dict)
return points_table
[docs]def calculate_points_from_json_answer(
single_answers: list[list[str]],
points_table: list[dict[str, float]] | None,
default_points: float | None | Missing = 0,
) -> float:
points = 0.0
if not isinstance(default_points, float) and not isinstance(default_points, int):
default_points = 0
if points_table is None:
points_table = [{}] * len(single_answers)
for (oneAnswer, point_row) in zip(single_answers, points_table):
for oneLine in oneAnswer:
if oneLine in point_row:
points += point_row[oneLine]
else:
points += default_points
return points
[docs]def qst_filter_markup_points(
points: str, question_type: str, rand_arr: list[int]
) -> str:
"""
filter markup's points field based on pre-generated array
"""
# TODO: Use constants
if question_type == "true-false" or question_type == "matrix":
# point format 1:1;2:-0.5|1:-0.5;2:1 where | splits rows input, ; column input
arr = points.split("|")
arr = qst_set_array_order(arr, rand_arr)
ret = "|".join(arr)
else:
# point format 1:1;2:-0.5;3:-0.5 where ; splits row input
tab = create_points_table(points)[0]
tab = qst_pick_expls(tab, rand_arr)
ret = ";".join(str(key) + ":" + str(val) for [key, val] in tab.items())
return ret
[docs]def qst_handle_randomization(jso: dict) -> None:
"""
Check if markup calls for randomization, or previous state contains randomization data
Update answer options, explanations and points accordingly.
:param jso: request json to modify
"""
markup = jso["markup"]
rand_arr = None
prev_state = jso.get("state", None)
if prev_state and isinstance(prev_state, dict):
rand_arr = prev_state.get("order")
jso["state"] = prev_state.get("c")
rows = markup.get("rows", [])
if (
not prev_state and rand_arr is None
): # no previous answer, check markup for new order
rcount = markup.get("randomizedRows", 0)
# TODO: try to convert string
if not isinstance(rcount, int):
rcount = 0
if rcount is None:
rcount = 0
if rcount > 0:
# markup['rows'] = qst_randomize_rows(rows,rcount,jso['user_id'])
random_seed = markup.get("randomSeed", 0)
# TODO: use random seed generation within qst_rand_array if seed was string
if not isinstance(random_seed, int):
random_seed = 0
locks = markup.get("doNotMove", [])
# TODO: MarkupModel should handle these checks?
if locks is None:
locks = []
if isinstance(locks, int):
locks = [locks]
for val in locks:
if not isinstance(val, int):
locks = []
break
if random_seed is None:
random_seed = 0
seed_string = str(jso.get("user_id", "")) + str(jso.get("taskID", ""))
rand_arr = qst_rand_array(
len(rows), rcount, seed_string, random_seed, locks
)
if rand_arr is not None: # specific order found in prev.ans or markup
markup["rows"] = qst_set_array_order(rows, rand_arr)
markup["expl"] = qst_pick_expls(markup["expl"], rand_arr)
points = markup.get("points")
if points:
question_type = markup.get("questionType")
points = qst_filter_markup_points(points, question_type, rand_arr)
markup["points"] = points