Загрузка данных
CREATE OR REPLACE PACKAGE BODY quiz_platform AS
PROCEDURE info IS
BEGIN
DBMS_OUTPUT.PUT_LINE('quiz_platform package loaded');
END;
FUNCTION has_role(p_uid NUMBER, p_role_name VARCHAR2) RETURN NUMBER IS
v_cnt NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM users u
JOIN role r ON r.id_role = u.id_role
WHERE u.user_id = p_uid
AND u.is_active = 1
AND r.role_name = p_role_name;
IF v_cnt > 0 THEN
RETURN 1;
END IF;
RETURN 0;
END;
PROCEDURE require_author(p_uid NUMBER) IS
v_cnt NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM users u
JOIN role r ON r.id_role = u.id_role
WHERE u.user_id = p_uid
AND u.is_active = 1
AND r.role_name IN ('AUTHOR', 'ADMIN');
IF v_cnt = 0 THEN
RAISE_APPLICATION_ERROR(-20030, 'Доступ разрешен только автору или администратору');
END IF;
END;
FUNCTION check_author_role(p_uid NUMBER) RETURN NUMBER IS
v_cnt NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM users u
JOIN role r ON r.id_role = u.id_role
WHERE u.user_id = p_uid
AND u.is_active = 1
AND r.role_name IN ('AUTHOR', 'ADMIN');
IF v_cnt > 0 THEN
RETURN 1;
END IF;
RETURN 0;
END;
FUNCTION check_admin_role(p_uid NUMBER) RETURN NUMBER IS
BEGIN
RETURN has_role(p_uid, 'ADMIN');
END;
PROCEDURE require_admin(p_uid NUMBER) IS
BEGIN
IF check_admin_role(p_uid) <> 1 THEN
RAISE_APPLICATION_ERROR(-20031, 'Доступ разрешен только администратору');
END IF;
END;
FUNCTION is_terminal_attempt_status(p_status VARCHAR2) RETURN NUMBER IS
v_terminal NUMBER;
BEGIN
SELECT is_terminal
INTO v_terminal
FROM attempt_status
WHERE status_code = p_status;
RETURN v_terminal;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN 0;
END;
PROCEDURE register_user(p_user_name VARCHAR2, p_password VARCHAR2) IS
v_user_id users.user_id%TYPE;
BEGIN
v_user_id := register_user_id(p_user_name, p_password);
END;
FUNCTION register_user_id(p_user_name VARCHAR2, p_password VARCHAR2) RETURN NUMBER IS
v_password_hash users.password_hash%TYPE;
v_user_id users.user_id%TYPE;
v_role_id role.id_role%TYPE;
BEGIN
IF p_user_name IS NULL OR LENGTH(TRIM(p_user_name)) = 0 THEN
RAISE_APPLICATION_ERROR(-20010, 'Имя пользователя не может быть пустым');
END IF;
IF p_password IS NULL OR LENGTH(p_password) < 4 THEN
RAISE_APPLICATION_ERROR(-20011, 'Пароль слишком короткий');
END IF;
SELECT STANDARD_HASH(p_password, 'SHA256') INTO v_password_hash FROM dual;
SELECT id_role INTO v_role_id FROM role WHERE role_name = 'USER';
INSERT INTO users (id_role, password_hash, user_name, created_at, is_active)
VALUES (v_role_id, v_password_hash, TRIM(p_user_name), SYSDATE, 1)
RETURNING user_id INTO v_user_id;
RETURN v_user_id;
END;
FUNCTION login_user(p_uid NUMBER, p_password VARCHAR2) RETURN NUMBER IS
v_role_id users.id_role%TYPE;
v_hash users.password_hash%TYPE;
v_password_hash users.password_hash%TYPE;
v_active users.is_active%TYPE;
BEGIN
BEGIN
SELECT id_role, password_hash, is_active
INTO v_role_id, v_hash, v_active
FROM users
WHERE user_id = p_uid;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20001, 'Неверный user_id');
END;
IF v_active <> 1 THEN
RAISE_APPLICATION_ERROR(-20002, 'Пользователь неактивен');
END IF;
SELECT STANDARD_HASH(p_password, 'SHA256') INTO v_password_hash FROM dual;
IF v_hash <> v_password_hash THEN
RAISE_APPLICATION_ERROR(-20003, 'Неверный пароль');
END IF;
RETURN v_role_id;
END;
PROCEDURE create_test(
p_uid_author NUMBER,
p_test_name VARCHAR2,
p_test_description VARCHAR2,
p_id_category NUMBER,
p_id_level NUMBER,
p_time_limit NUMBER,
p_attempt_limit NUMBER,
p_show_feedback NUMBER,
p_question_count NUMBER
) IS
v_test_id test.id_test%TYPE;
BEGIN
v_test_id := create_test_id(
p_uid_author, p_test_name, p_test_description, p_id_category, p_id_level,
p_time_limit, p_attempt_limit, p_show_feedback, p_question_count
);
END;
FUNCTION create_test_id(
p_uid_author NUMBER,
p_test_name VARCHAR2,
p_test_description VARCHAR2,
p_id_category NUMBER,
p_id_level NUMBER,
p_time_limit NUMBER,
p_attempt_limit NUMBER,
p_show_feedback NUMBER,
p_question_count NUMBER
) RETURN NUMBER IS
v_test_id test.id_test%TYPE;
BEGIN
IF p_test_name IS NULL OR LENGTH(TRIM(p_test_name)) = 0 THEN
RAISE_APPLICATION_ERROR(-20100, 'Название теста обязательно');
END IF;
IF p_attempt_limit IS NULL OR p_attempt_limit <= 0 THEN
RAISE_APPLICATION_ERROR(-20101, 'Лимит попыток должен быть больше 0');
END IF;
IF p_show_feedback NOT IN (0, 1) THEN
RAISE_APPLICATION_ERROR(-20102, 'show_feedback должен быть 0 или 1');
END IF;
require_author(p_uid_author);
INSERT INTO test (
uid_author, id_category, id_level, test_name, test_description,
created_at, time_limit, attempt_limit, question_count, show_feedback, is_active
) VALUES (
p_uid_author, p_id_category, p_id_level, TRIM(p_test_name), p_test_description,
SYSDATE, p_time_limit, p_attempt_limit, p_question_count, p_show_feedback, 0
)
RETURNING id_test INTO v_test_id;
RETURN v_test_id;
END;
PROCEDURE publish_test(p_uid_author NUMBER, p_id_test NUMBER, p_direct_call NUMBER DEFAULT 1) IS
v_active NUMBER;
v_q_count NUMBER;
BEGIN
require_author(p_uid_author);
SELECT is_active
INTO v_active
FROM test
WHERE id_test = p_id_test
AND uid_author = p_uid_author;
SELECT COUNT(*) INTO v_q_count FROM question_in_test WHERE id_test = p_id_test;
IF v_active = 0 THEN
IF v_q_count = 0 THEN
RAISE_APPLICATION_ERROR(-20104, 'Нельзя опубликовать тест без вопросов');
END IF;
UPDATE test SET is_active = 1 WHERE id_test = p_id_test;
ELSE
IF p_direct_call = 1 THEN
UPDATE test SET is_active = 0 WHERE id_test = p_id_test;
END IF;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20105, 'Тест не найден');
END;
PROCEDURE include_question_in_test(
p_uid_author NUMBER,
p_id_test NUMBER,
p_id_question NUMBER,
p_weight NUMBER,
p_order_num NUMBER,
p_is_required NUMBER,
p_time_limit NUMBER
) IS
v_question_active NUMBER;
v_test_exists NUMBER;
BEGIN
require_author(p_uid_author);
IF p_is_required NOT IN (0, 1) THEN
RAISE_APPLICATION_ERROR(-20106, 'is_required должен быть 0 или 1');
END IF;
SELECT COUNT(*)
INTO v_test_exists
FROM test
WHERE id_test = p_id_test
AND uid_author = p_uid_author;
IF v_test_exists = 0 THEN
RAISE_APPLICATION_ERROR(-20105, 'Тест не найден');
END IF;
SELECT is_active
INTO v_question_active
FROM question
WHERE id_question = p_id_question
AND uid_author = p_uid_author;
IF v_question_active <> 1 THEN
RAISE_APPLICATION_ERROR(-20107, 'Нельзя добавить неактивный вопрос');
END IF;
INSERT INTO question_in_test (id_test, id_question, weight, order_num, is_required, time_limit)
VALUES (p_id_test, p_id_question, NVL(p_weight, 1), p_order_num, p_is_required, p_time_limit);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20108, 'Вопрос не найден');
WHEN DUP_VAL_ON_INDEX THEN
RAISE_APPLICATION_ERROR(-20109, 'Вопрос уже добавлен или занят порядок вопроса');
END;
PROCEDURE generate_test_questions(p_uid_author NUMBER, p_id_test NUMBER) IS
v_id_level test.id_level%TYPE;
v_question_count test.question_count%TYPE;
v_id_category test.id_category%TYPE;
v_exists NUMBER;
v_order NUMBER := 1;
BEGIN
require_author(p_uid_author);
SELECT id_level, question_count, id_category
INTO v_id_level, v_question_count, v_id_category
FROM test
WHERE id_test = p_id_test
AND uid_author = p_uid_author;
IF v_id_level IS NULL OR v_question_count IS NULL OR v_question_count <= 0 THEN
RAISE_APPLICATION_ERROR(-20110, 'Для автогенерации заполните сложность и количество вопросов');
END IF;
DELETE FROM question_in_test WHERE id_test = p_id_test;
FOR rec IN (
SELECT id_question
FROM (
SELECT q.id_question
FROM question q
WHERE q.is_active = 1
AND q.id_level = v_id_level
AND (v_id_category IS NULL OR q.id_category = v_id_category)
ORDER BY DBMS_RANDOM.VALUE
)
WHERE ROWNUM <= v_question_count
) LOOP
INSERT INTO question_in_test (id_test, id_question, weight, order_num, is_required, time_limit)
VALUES (p_id_test, rec.id_question, 1, v_order, 1, NULL);
v_order := v_order + 1;
END LOOP;
SELECT COUNT(*) INTO v_exists FROM question_in_test WHERE id_test = p_id_test;
IF v_exists < v_question_count THEN
RAISE_APPLICATION_ERROR(-20111, 'Недостаточно вопросов для генерации');
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20105, 'Тест не найден');
END;
PROCEDURE add_question_type(p_type_name VARCHAR2) IS
BEGIN
IF p_type_name IS NULL OR LENGTH(TRIM(p_type_name)) = 0 THEN
RAISE_APPLICATION_ERROR(-20020, 'Название типа вопроса не может быть пустым');
END IF;
INSERT INTO question_type (type_code, type_name, uses_options, is_multi_select, is_numeric_answer, is_text_answer)
VALUES (UPPER(REPLACE(TRIM(p_type_name), ' ', '_')), TRIM(p_type_name), 0, 0, 0, 1);
END;
PROCEDURE add_category(p_category_name VARCHAR2, p_category_description VARCHAR2) IS
BEGIN
IF p_category_name IS NULL OR LENGTH(TRIM(p_category_name)) = 0 THEN
RAISE_APPLICATION_ERROR(-20026, 'Название категории не может быть пустым');
END IF;
INSERT INTO category (category_name, category_description)
VALUES (TRIM(p_category_name), p_category_description);
END;
PROCEDURE add_difficulty_level(p_level_name VARCHAR2) IS
BEGIN
IF p_level_name IS NULL OR LENGTH(TRIM(p_level_name)) = 0 THEN
RAISE_APPLICATION_ERROR(-20027, 'Название уровня сложности не может быть пустым');
END IF;
INSERT INTO difficulty_level (level_name)
VALUES (TRIM(p_level_name));
END;
PROCEDURE add_question(p_uid_author NUMBER, p_question_text VARCHAR2, p_id_category NUMBER, p_id_level NUMBER, p_type_id NUMBER, p_correct_text VARCHAR2, p_correct_number NUMBER, p_tolerance NUMBER, p_explanation VARCHAR2) IS
v_question_id question.id_question%TYPE;
BEGIN
v_question_id := add_question_id(
p_uid_author, p_question_text, p_id_category, p_id_level, p_type_id,
p_correct_text, p_correct_number, p_tolerance, p_explanation
);
END;
FUNCTION add_question_id(p_uid_author NUMBER, p_question_text VARCHAR2, p_id_category NUMBER, p_id_level NUMBER, p_type_id NUMBER, p_correct_text VARCHAR2, p_correct_number NUMBER, p_tolerance NUMBER, p_explanation VARCHAR2) RETURN NUMBER IS
v_question_id question.id_question%TYPE;
BEGIN
IF p_question_text IS NULL OR LENGTH(TRIM(p_question_text)) = 0 THEN
RAISE_APPLICATION_ERROR(-20021, 'Текст вопроса не может быть пустым');
END IF;
require_author(p_uid_author);
INSERT INTO question (uid_author, id_category, id_level, type_id, question_text, explanation, correct_text, correct_number, tolerance, created_at, is_active)
VALUES (p_uid_author, p_id_category, p_id_level, p_type_id, TRIM(p_question_text), p_explanation, p_correct_text, p_correct_number, p_tolerance, SYSDATE, 1)
RETURNING id_question INTO v_question_id;
RETURN v_question_id;
END;
PROCEDURE add_answer_option(p_id_question NUMBER, p_option_text VARCHAR2, p_is_correct NUMBER) IS
v_question_cnt NUMBER;
BEGIN
IF p_option_text IS NULL OR LENGTH(TRIM(p_option_text)) = 0 THEN
RAISE_APPLICATION_ERROR(-20023, 'Текст варианта не может быть пустым');
END IF;
IF p_is_correct NOT IN (0, 1) THEN
RAISE_APPLICATION_ERROR(-20024, 'Признак правильности должен быть 0 или 1');
END IF;
SELECT COUNT(*) INTO v_question_cnt FROM question WHERE id_question = p_id_question;
IF v_question_cnt = 0 THEN
RAISE_APPLICATION_ERROR(-20025, 'Вопрос не найден');
END IF;
INSERT INTO answer_option (id_question, option_text, is_correct)
VALUES (p_id_question, TRIM(p_option_text), p_is_correct);
END;
PROCEDURE deactivate_question(p_id_question NUMBER, p_uid_author NUMBER) IS
BEGIN
require_author(p_uid_author);
UPDATE question
SET is_active = 0
WHERE id_question = p_id_question
AND uid_author = p_uid_author;
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20028, 'Вопрос не найден');
END IF;
END;
PROCEDURE grant_test_access(p_uid_author NUMBER, p_id_test NUMBER, p_uid NUMBER) IS
v_user_cnt NUMBER;
v_test_cnt NUMBER;
BEGIN
require_author(p_uid_author);
SELECT COUNT(*) INTO v_user_cnt FROM users WHERE user_id = p_uid AND is_active = 1;
IF v_user_cnt = 0 THEN
RAISE_APPLICATION_ERROR(-20200, 'Пользователь не найден или неактивен');
END IF;
SELECT COUNT(*)
INTO v_test_cnt
FROM test
WHERE id_test = p_id_test
AND uid_author = p_uid_author;
IF v_test_cnt = 0 THEN
RAISE_APPLICATION_ERROR(-20201, 'Тест не найден');
END IF;
MERGE INTO test_access ta
USING (SELECT p_uid user_id, p_id_test id_test FROM dual) src
ON (ta.user_id = src.user_id AND ta.id_test = src.id_test)
WHEN MATCHED THEN
UPDATE SET ta.is_active = 1, ta.granted_at = SYSDATE
WHEN NOT MATCHED THEN
INSERT (user_id, id_test, granted_at, is_active)
VALUES (src.user_id, src.id_test, SYSDATE, 1);
END;
PROCEDURE deactivate_test_access(p_id_test NUMBER, p_uid NUMBER, p_uid_author NUMBER) IS
BEGIN
require_author(p_uid_author);
UPDATE test_access ta
SET ta.is_active = 0
WHERE ta.id_test = p_id_test
AND ta.user_id = p_uid
AND EXISTS (
SELECT 1
FROM test t
WHERE t.id_test = ta.id_test
AND t.uid_author = p_uid_author
);
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20202, 'Доступ к тесту не найден');
END IF;
END;
FUNCTION check_access(p_id_test NUMBER, p_uid NUMBER) RETURN NUMBER IS
v_cnt NUMBER;
BEGIN
SELECT COUNT(*)
INTO v_cnt
FROM test_access ta
JOIN test t ON t.id_test = ta.id_test
JOIN users u ON u.user_id = ta.user_id
WHERE ta.id_test = p_id_test
AND ta.user_id = p_uid
AND ta.is_active = 1
AND t.is_active = 1
AND u.is_active = 1;
IF v_cnt > 0 THEN
RETURN 1;
END IF;
RETURN 0;
END;
PROCEDURE start_attempt(p_id_test NUMBER, p_uid NUMBER) IS
v_attempt_id attempt.id_attempt%TYPE;
BEGIN
v_attempt_id := start_attempt_id(p_id_test, p_uid);
END;
FUNCTION start_attempt_id(p_id_test NUMBER, p_uid NUMBER) RETURN NUMBER IS
v_user_active NUMBER;
v_test_active NUMBER;
v_access NUMBER;
v_attempt_limit NUMBER;
v_attempts_used NUMBER;
v_attempt_no NUMBER;
v_attempt_id attempt.id_attempt%TYPE;
BEGIN
SELECT is_active INTO v_user_active FROM users WHERE user_id = p_uid;
IF v_user_active <> 1 THEN
RAISE_APPLICATION_ERROR(-20300, 'Пользователь неактивен');
END IF;
SELECT is_active, attempt_limit
INTO v_test_active, v_attempt_limit
FROM test
WHERE id_test = p_id_test;
IF v_test_active <> 1 THEN
RAISE_APPLICATION_ERROR(-20301, 'Тест скрыт');
END IF;
v_access := check_access(p_id_test, p_uid);
IF v_access <> 1 THEN
RAISE_APPLICATION_ERROR(-20302, 'Нет доступа к тесту');
END IF;
SELECT NVL(MAX(attempt_number), 0)
INTO v_attempts_used
FROM attempt
WHERE user_id = p_uid AND id_test = p_id_test;
IF v_attempts_used >= v_attempt_limit THEN
RAISE_APPLICATION_ERROR(-20303, 'Превышен лимит попыток');
END IF;
v_attempt_no := v_attempts_used + 1;
INSERT INTO attempt (user_id, id_test, attempt_number, start_date, status, finished_in_time)
VALUES (p_uid, p_id_test, v_attempt_no, SYSDATE, 'STARTED', 1)
RETURNING id_attempt INTO v_attempt_id;
RETURN v_attempt_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20304, 'Пользователь или тест не найден');
END;
PROCEDURE save_answer(
p_id_attempt NUMBER,
p_id_qt NUMBER,
p_answer_text VARCHAR2,
p_answer_number NUMBER,
p_answer_time NUMBER
) IS
v_answer_id answer.id_answer%TYPE;
BEGIN
v_answer_id := save_answer_id(p_id_attempt, p_id_qt, p_answer_text, p_answer_number, p_answer_time, p_uid);
END;
FUNCTION save_answer_id(
p_id_attempt NUMBER,
p_id_qt NUMBER,
p_answer_text VARCHAR2,
p_answer_number NUMBER,
p_answer_time NUMBER
) RETURN NUMBER IS
v_status attempt.status%TYPE;
v_uid attempt.user_id%TYPE;
v_test_id attempt.id_test%TYPE;
v_started DATE;
v_test_time_limit NUMBER;
v_answer_id answer.id_answer%TYPE;
v_qt_cnt NUMBER;
BEGIN
SELECT a.status, a.user_id, a.id_test, a.start_date, t.time_limit
INTO v_status, v_uid, v_test_id, v_started, v_test_time_limit
FROM attempt a
JOIN test t ON t.id_test = a.id_test
WHERE a.id_attempt = p_id_attempt;
IF v_status <> 'STARTED' THEN
RAISE_APPLICATION_ERROR(-20305, 'Попытка завершена');
END IF;
SELECT COUNT(*)
INTO v_qt_cnt
FROM question_in_test
WHERE id_qt = p_id_qt
AND id_test = v_test_id;
IF v_qt_cnt = 0 THEN
RAISE_APPLICATION_ERROR(-20312, 'Вопрос не относится к данной попытке');
END IF;
IF v_test_time_limit IS NOT NULL
AND (SYSDATE - v_started) * 86400 > v_test_time_limit THEN
UPDATE attempt
SET status = 'TIME_EXPIRED', end_date = SYSDATE, finished_in_time = 0
WHERE id_attempt = p_id_attempt;
RAISE_APPLICATION_ERROR(-20306, 'Время теста истекло');
END IF;
INSERT INTO answer (
id_attempt, id_qt, answer_text, answer_number,
answer_date, answer_time, is_checked
) VALUES (
p_id_attempt, p_id_qt, p_answer_text, p_answer_number,
SYSDATE, p_answer_time, 0
)
RETURNING id_answer INTO v_answer_id;
RETURN v_answer_id;
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
RAISE_APPLICATION_ERROR(-20307, 'Ответ уже сохранен');
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20308, 'Попытка не найдена');
END;
PROCEDURE save_selected_option(p_id_answer NUMBER, p_id_option NUMBER) IS
v_id_question NUMBER;
v_opt_question NUMBER;
BEGIN
SELECT qt.id_question
INTO v_id_question
FROM answer a
JOIN question_in_test qt ON qt.id_qt = a.id_qt
WHERE a.id_answer = p_id_answer;
SELECT id_question INTO v_opt_question
FROM answer_option
WHERE id_option = p_id_option;
IF v_id_question <> v_opt_question THEN
RAISE_APPLICATION_ERROR(-20309, 'Вариант не относится к вопросу ответа');
END IF;
INSERT INTO answer_selected_option (id_answer, id_option)
VALUES (p_id_answer, p_id_option);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20310, 'Ответ или вариант не найден');
WHEN DUP_VAL_ON_INDEX THEN
NULL;
END;
FUNCTION check_answer(p_id_answer NUMBER) RETURN NUMBER IS
v_uses_options NUMBER;
v_is_multi_select NUMBER;
v_is_numeric_answer NUMBER;
v_is_text_answer NUMBER;
v_correct_text VARCHAR2(500);
v_correct_number NUMBER;
v_tolerance NUMBER;
v_answer_text VARCHAR2(500);
v_answer_number NUMBER;
v_cnt NUMBER;
v_correct NUMBER := 0;
v_selected_wrong NUMBER := 0;
v_selected_correct NUMBER := 0;
v_total_correct NUMBER := 0;
BEGIN
SELECT qt_type.uses_options, qt_type.is_multi_select, qt_type.is_numeric_answer, qt_type.is_text_answer,
q.correct_text, q.correct_number, NVL(q.tolerance, 0), a.answer_text, a.answer_number
INTO v_uses_options, v_is_multi_select, v_is_numeric_answer, v_is_text_answer,
v_correct_text, v_correct_number, v_tolerance, v_answer_text, v_answer_number
FROM answer a
JOIN question_in_test qt ON qt.id_qt = a.id_qt
JOIN question q ON q.id_question = qt.id_question
JOIN question_type qt_type ON qt_type.type_id = q.type_id
WHERE a.id_answer = p_id_answer;
IF v_uses_options = 1 AND v_is_multi_select = 0 THEN
SELECT COUNT(*)
INTO v_cnt
FROM answer_selected_option aso
JOIN answer_option ao ON ao.id_option = aso.id_option
WHERE aso.id_answer = p_id_answer
AND ao.is_correct = 1;
IF v_cnt = 1 THEN
SELECT COUNT(*)
INTO v_cnt
FROM answer_selected_option
WHERE id_answer = p_id_answer;
IF v_cnt = 1 THEN
v_correct := 1;
END IF;
END IF;
ELSIF v_uses_options = 1 AND v_is_multi_select = 1 THEN
SELECT COUNT(*) INTO v_selected_wrong
FROM answer_selected_option aso
JOIN answer_option ao ON ao.id_option = aso.id_option
WHERE aso.id_answer = p_id_answer
AND ao.is_correct = 0;
IF v_selected_wrong = 0 THEN
SELECT COUNT(*) INTO v_selected_correct
FROM answer_selected_option aso
JOIN answer_option ao ON ao.id_option = aso.id_option
WHERE aso.id_answer = p_id_answer
AND ao.is_correct = 1;
SELECT COUNT(*) INTO v_total_correct
FROM answer_option ao
JOIN answer a ON a.id_answer = p_id_answer
JOIN question_in_test qt ON qt.id_qt = a.id_qt
WHERE ao.id_question = qt.id_question
AND ao.is_correct = 1;
IF v_total_correct > 0 AND v_selected_correct = v_total_correct THEN
v_correct := 1;
END IF;
END IF;
ELSIF v_is_text_answer = 1 THEN
IF LOWER(TRIM(NVL(v_answer_text, ''))) = LOWER(TRIM(NVL(v_correct_text, '#NULL#'))) THEN
v_correct := 1;
END IF;
ELSIF v_is_numeric_answer = 1 THEN
IF v_answer_number IS NOT NULL
AND v_correct_number IS NOT NULL
AND ABS(v_answer_number - v_correct_number) <= v_tolerance THEN
v_correct := 1;
END IF;
END IF;
UPDATE answer SET is_correct = v_correct, is_checked = 1 WHERE id_answer = p_id_answer;
RETURN v_correct;
END;
FUNCTION calc_answer_score(p_id_answer NUMBER) RETURN NUMBER IS
v_is_multi_select NUMBER;
v_weight NUMBER;
v_q_time_limit NUMBER;
v_ans_time NUMBER;
v_score NUMBER := 0;
v_correct NUMBER;
v_selected_wrong NUMBER := 0;
v_selected_correct NUMBER := 0;
v_total_correct NUMBER := 0;
BEGIN
SELECT qt_type.is_multi_select, qt.weight, qt.time_limit, NVL(a.answer_time, 0)
INTO v_is_multi_select, v_weight, v_q_time_limit, v_ans_time
FROM answer a
JOIN question_in_test qt ON qt.id_qt = a.id_qt
JOIN question q ON q.id_question = qt.id_question
JOIN question_type qt_type ON qt_type.type_id = q.type_id
WHERE a.id_answer = p_id_answer;
IF v_q_time_limit IS NOT NULL AND v_ans_time > v_q_time_limit THEN
v_score := 0;
UPDATE answer SET earned_score = v_score WHERE id_answer = p_id_answer;
RETURN v_score;
END IF;
v_correct := check_answer(p_id_answer);
IF v_is_multi_select = 1 THEN
SELECT COUNT(*) INTO v_selected_wrong
FROM answer_selected_option aso
JOIN answer_option ao ON ao.id_option = aso.id_option
WHERE aso.id_answer = p_id_answer AND ao.is_correct = 0;
IF v_selected_wrong = 0 THEN
SELECT COUNT(*) INTO v_selected_correct
FROM answer_selected_option aso
JOIN answer_option ao ON ao.id_option = aso.id_option
WHERE aso.id_answer = p_id_answer AND ao.is_correct = 1;
SELECT COUNT(*) INTO v_total_correct
FROM answer_option ao
JOIN answer a ON a.id_answer = p_id_answer
JOIN question_in_test qt ON qt.id_qt = a.id_qt
WHERE ao.id_question = qt.id_question
AND ao.is_correct = 1;
IF v_total_correct > 0 THEN
v_score := v_weight * v_selected_correct / v_total_correct;
END IF;
END IF;
ELSE
IF v_correct = 1 THEN
v_score := v_weight;
ELSE
v_score := 0;
END IF;
END IF;
UPDATE answer SET earned_score = v_score WHERE id_answer = p_id_answer;
RETURN v_score;
END;
PROCEDURE calc_result(p_id_attempt NUMBER) IS
v_total_weight NUMBER := 0;
v_total_score NUMBER := 0;
v_percent NUMBER := 0;
BEGIN
FOR r IN (
SELECT a.id_answer
FROM answer a
WHERE a.id_attempt = p_id_attempt
) LOOP
v_total_score := v_total_score + NVL(calc_answer_score(r.id_answer), 0);
END LOOP;
SELECT NVL(SUM(qt.weight), 0)
INTO v_total_weight
FROM question_in_test qt
JOIN attempt at ON at.id_test = qt.id_test
WHERE at.id_attempt = p_id_attempt;
IF v_total_weight > 0 THEN
v_percent := ROUND(v_total_score / v_total_weight * 100, 2);
END IF;
UPDATE attempt
SET score = v_total_score,
percent_result = v_percent
WHERE id_attempt = p_id_attempt;
END;
PROCEDURE finish_attempt(p_id_attempt NUMBER, p_uid NUMBER) IS
v_status attempt.status%TYPE;
v_started DATE;
v_time_limit NUMBER;
v_new_status VARCHAR2(30);
v_finished_in_time NUMBER := 1;
BEGIN
SELECT a.status, a.start_date, t.time_limit
INTO v_status, v_started, v_time_limit
FROM attempt a
JOIN test t ON t.id_test = a.id_test
WHERE a.id_attempt = p_id_attempt;
IF v_status <> 'STARTED' THEN
RAISE_APPLICATION_ERROR(-20311, 'Попытка уже завершена');
END IF;
v_new_status := 'FINISHED';
IF v_time_limit IS NOT NULL
AND (SYSDATE - v_started) * 86400 > v_time_limit THEN
v_new_status := 'TIME_EXPIRED';
v_finished_in_time := 0;
END IF;
UPDATE attempt
SET end_date = SYSDATE,
status = v_new_status,
finished_in_time = v_finished_in_time
WHERE id_attempt = p_id_attempt;
calc_result(p_id_attempt);
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20308, 'Попытка не найдена');
END;
FUNCTION get_user(p_uid NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT u.user_id, u.user_name, u.id_role, r.role_name, r.role_description, u.is_active
FROM users u
JOIN role r ON r.id_role = u.id_role
WHERE u.user_id = p_uid;
RETURN rc;
END;
FUNCTION list_categories RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR SELECT id_category, category_name, category_description FROM category ORDER BY category_name;
RETURN rc;
END;
FUNCTION list_difficulty_levels RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR SELECT id_level, level_name FROM difficulty_level ORDER BY id_level;
RETURN rc;
END;
FUNCTION list_question_types RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR SELECT type_id, type_name, type_code, uses_options, is_multi_select, is_numeric_answer, is_text_answer FROM question_type ORDER BY type_id;
RETURN rc;
END;
FUNCTION list_author_questions(p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT q.id_question, q.question_text, qt.type_name, c.category_name, d.level_name, q.is_active
FROM question q
JOIN question_type qt ON qt.type_id = q.type_id
JOIN category c ON c.id_category = q.id_category
JOIN difficulty_level d ON d.id_level = q.id_level
WHERE q.uid_author = p_uid_author
ORDER BY q.id_question DESC;
RETURN rc;
END;
FUNCTION get_author_question(p_id_question NUMBER, p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT q.id_question, q.question_text, q.explanation, q.correct_text, q.correct_number, q.tolerance,
q.is_active, qt.type_name, c.category_name, d.level_name
FROM question q
JOIN question_type qt ON qt.type_id = q.type_id
JOIN category c ON c.id_category = q.id_category
JOIN difficulty_level d ON d.id_level = q.id_level
WHERE q.id_question = p_id_question AND q.uid_author = p_uid_author;
RETURN rc;
END;
FUNCTION list_answer_options(p_id_question NUMBER, p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT ao.id_option, ao.option_text, ao.is_correct
FROM answer_option ao
JOIN question q ON q.id_question = ao.id_question
WHERE ao.id_question = p_id_question
AND q.uid_author = p_uid_author
ORDER BY ao.id_option;
RETURN rc;
END;
FUNCTION list_available_tests(p_uid NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT t.id_test, t.test_name, t.test_description, c.category_name, d.level_name,
t.time_limit, t.attempt_limit, t.question_count, t.show_feedback,
NVL((SELECT MAX(a.attempt_number) FROM attempt a WHERE a.user_id = p_uid AND a.id_test = t.id_test), 0) AS used_attempts
FROM test_access ta
JOIN test t ON t.id_test = ta.id_test
LEFT JOIN category c ON c.id_category = t.id_category
LEFT JOIN difficulty_level d ON d.id_level = t.id_level
WHERE ta.user_id = p_uid
AND ta.is_active = 1
AND t.is_active = 1
ORDER BY t.id_test DESC;
RETURN rc;
END;
FUNCTION get_available_test(p_id_test NUMBER, p_uid NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT t.id_test, t.test_name, t.test_description, c.category_name, d.level_name,
t.time_limit, t.attempt_limit, t.question_count, t.show_feedback,
NVL((SELECT MAX(a.attempt_number) FROM attempt a WHERE a.user_id = p_uid AND a.id_test = t.id_test), 0) AS used_attempts
FROM test t
LEFT JOIN category c ON c.id_category = t.id_category
LEFT JOIN difficulty_level d ON d.id_level = t.id_level
WHERE t.id_test = p_id_test AND t.is_active = 1;
RETURN rc;
END;
FUNCTION get_attempt(p_id_attempt NUMBER, p_uid NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT a.id_attempt, a.status, a.start_date, a.end_date, t.id_test, t.test_name,
t.time_limit, FLOOR((SYSDATE - a.start_date) * 86400) AS elapsed_seconds,
CASE WHEN t.time_limit IS NULL THEN NULL ELSE GREATEST(0, t.time_limit - FLOOR((SYSDATE - a.start_date) * 86400)) END AS remaining_seconds
FROM attempt a
JOIN test t ON t.id_test = a.id_test
WHERE a.id_attempt = p_id_attempt AND a.user_id = p_uid;
RETURN rc;
END;
FUNCTION list_attempt_questions(p_id_attempt NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT qt.id_qt, qt.order_num, qt.weight, qt.time_limit,
q.id_question, q.question_text, q.type_id, q.explanation,
qt_type.uses_options, qt_type.is_multi_select, qt_type.is_numeric_answer, qt_type.is_text_answer
FROM question_in_test qt
JOIN question q ON q.id_question = qt.id_question
JOIN question_type qt_type ON qt_type.type_id = q.type_id
JOIN attempt a ON a.id_test = qt.id_test
WHERE a.id_attempt = p_id_attempt
ORDER BY qt.order_num;
RETURN rc;
END;
FUNCTION list_attempt_answers(p_id_attempt NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT id_answer, id_qt, answer_text, answer_number, is_correct, earned_score, answer_time
FROM answer
WHERE id_attempt = p_id_attempt;
RETURN rc;
END;
FUNCTION list_attempt_selected_options(p_id_attempt NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT aso.id_answer, aso.id_option, ao.option_text
FROM answer_selected_option aso
JOIN answer a ON a.id_answer = aso.id_answer
JOIN answer_option ao ON ao.id_option = aso.id_option
WHERE a.id_attempt = p_id_attempt
ORDER BY ao.id_option;
RETURN rc;
END;
FUNCTION list_attempt_question_options(p_id_attempt NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT ao.id_option, ao.id_question, ao.option_text
FROM answer_option ao
JOIN question q ON q.id_question = ao.id_question
JOIN question_in_test qt ON qt.id_question = q.id_question
JOIN attempt a ON a.id_test = qt.id_test
WHERE a.id_attempt = p_id_attempt
ORDER BY ao.id_option;
RETURN rc;
END;
FUNCTION get_attempt_result(p_id_attempt NUMBER, p_uid NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT a.id_attempt, a.attempt_number, a.start_date, a.end_date, a.status, a.score, a.percent_result,
t.id_test, t.test_name, t.show_feedback,
NVL((
SELECT AVG(x.percent_result)
FROM attempt x
WHERE x.id_test = a.id_test
AND EXISTS (
SELECT 1
FROM attempt_status s
WHERE s.status_code = x.status
AND s.is_successful = 1
)
AND x.percent_result IS NOT NULL
), 0) AS avg_percent
FROM attempt a
JOIN test t ON t.id_test = a.id_test
WHERE a.id_attempt = p_id_attempt AND a.user_id = p_uid;
RETURN rc;
END;
FUNCTION list_result_answers(p_id_attempt NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT qt.order_num, q.question_text, q.type_id, a.id_answer, a.answer_text, a.answer_number,
a.is_correct, a.earned_score, q.correct_text, q.correct_number, q.explanation,
qt_type.uses_options, qt_type.is_multi_select, qt_type.is_numeric_answer, qt_type.is_text_answer
FROM question_in_test qt
JOIN question q ON q.id_question = qt.id_question
JOIN question_type qt_type ON qt_type.type_id = q.type_id
JOIN attempt at ON at.id_test = qt.id_test
LEFT JOIN answer a ON a.id_qt = qt.id_qt AND a.id_attempt = at.id_attempt
WHERE at.id_attempt = p_id_attempt
ORDER BY qt.order_num;
RETURN rc;
END;
FUNCTION list_result_selected_options(p_id_attempt NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT aso.id_answer, ao.option_text
FROM answer_selected_option aso
JOIN answer_option ao ON ao.id_option = aso.id_option
JOIN answer a ON a.id_answer = aso.id_answer
WHERE a.id_attempt = p_id_attempt
ORDER BY ao.id_option;
RETURN rc;
END;
FUNCTION list_result_correct_options(p_id_attempt NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT qt.order_num, ao.option_text
FROM question_in_test qt
JOIN attempt at ON at.id_test = qt.id_test
JOIN answer_option ao ON ao.id_question = qt.id_question
WHERE at.id_attempt = p_id_attempt
AND ao.is_correct = 1
ORDER BY qt.order_num, ao.id_option;
RETURN rc;
END;
FUNCTION list_user_attempts(p_uid NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT a.id_attempt, t.test_name, a.attempt_number, a.status, a.score, a.percent_result, a.start_date, a.end_date
FROM attempt a
JOIN test t ON t.id_test = a.id_test
WHERE a.user_id = p_uid
ORDER BY a.id_attempt DESC;
RETURN rc;
END;
FUNCTION list_author_tests(p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT t.id_test, t.test_name, t.test_description, t.is_active,
t.question_count, t.attempt_limit, t.time_limit, t.show_feedback,
c.category_name, d.level_name
FROM test t
LEFT JOIN category c ON c.id_category = t.id_category
LEFT JOIN difficulty_level d ON d.id_level = t.id_level
WHERE t.uid_author = p_uid_author
ORDER BY t.id_test DESC;
RETURN rc;
END;
FUNCTION get_author_test(p_id_test NUMBER, p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT t.id_test, t.test_name, t.test_description, t.is_active, t.question_count, t.attempt_limit, t.time_limit,
t.show_feedback, c.category_name, d.level_name
FROM test t
LEFT JOIN category c ON c.id_category = t.id_category
LEFT JOIN difficulty_level d ON d.id_level = t.id_level
WHERE t.id_test = p_id_test AND t.uid_author = p_uid_author;
RETURN rc;
END;
FUNCTION list_test_questions(p_id_test NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT qt.id_qt, qt.order_num, qt.weight, qt.is_required, qt.time_limit, q.id_question, q.question_text
FROM question_in_test qt
JOIN question q ON q.id_question = qt.id_question
WHERE qt.id_test = p_id_test
ORDER BY qt.order_num;
RETURN rc;
END;
FUNCTION list_selected_test_questions(p_id_test NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT qt.order_num, q.id_question, q.question_text, qt.weight, qt.is_required, qt.time_limit
FROM question_in_test qt
JOIN question q ON q.id_question = qt.id_question
WHERE qt.id_test = p_id_test
ORDER BY qt.order_num;
RETURN rc;
END;
FUNCTION list_question_pool(p_id_test NUMBER, p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT q.id_question, q.question_text
FROM question q
JOIN test t ON t.uid_author = q.uid_author
WHERE t.id_test = p_id_test
AND q.uid_author = p_uid_author
AND q.is_active = 1
AND NOT EXISTS (
SELECT 1 FROM question_in_test x WHERE x.id_test = p_id_test AND x.id_question = q.id_question
)
ORDER BY q.id_question DESC;
RETURN rc;
END;
FUNCTION get_author_test_access_header(p_id_test NUMBER, p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT id_test, test_name
FROM test
WHERE id_test = p_id_test AND uid_author = p_uid_author;
RETURN rc;
END;
FUNCTION list_test_access(p_id_test NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT ta.user_id, u.user_name, ta.is_active, ta.granted_at
FROM test_access ta
JOIN users u ON u.user_id = ta.user_id
WHERE ta.id_test = p_id_test
ORDER BY ta.granted_at DESC;
RETURN rc;
END;
FUNCTION get_test_statistics_summary(p_id_test NUMBER, p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT
t.test_name,
COUNT(a.id_attempt) AS total_attempts,
SUM(CASE WHEN s.is_successful = 1 THEN 1 ELSE 0 END) AS finished_attempts,
ROUND(AVG(CASE WHEN s.is_successful = 1 THEN a.score END), 2) AS avg_score,
ROUND(AVG(CASE WHEN s.is_successful = 1 THEN a.percent_result END), 2) AS avg_percent,
MIN(CASE WHEN s.is_successful = 1 THEN a.percent_result END) AS min_percent,
MAX(CASE WHEN s.is_successful = 1 THEN a.percent_result END) AS max_percent
FROM test t
LEFT JOIN attempt a ON a.id_test = t.id_test
LEFT JOIN attempt_status s ON s.status_code = a.status
WHERE t.id_test = p_id_test
AND t.uid_author = p_uid_author
GROUP BY t.test_name;
RETURN rc;
END;
FUNCTION list_test_statistics_questions(p_id_test NUMBER, p_uid_author NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
OPEN rc FOR
SELECT
qt.order_num,
q.question_text,
COUNT(a.id_answer) AS total_answers,
SUM(CASE WHEN a.is_correct = 1 THEN 1 ELSE 0 END) AS correct_answers,
ROUND(
CASE WHEN COUNT(a.id_answer) = 0 THEN 0
ELSE SUM(CASE WHEN a.is_correct = 1 THEN 1 ELSE 0 END) / COUNT(a.id_answer) * 100
END, 2
) AS percent_correct,
ROUND(AVG(a.answer_time), 2) AS avg_answer_time,
ROUND(AVG(a.earned_score), 2) AS avg_earned_score
FROM test t
JOIN question_in_test qt ON qt.id_test = t.id_test
JOIN question q ON q.id_question = qt.id_question
LEFT JOIN answer a ON a.id_qt = qt.id_qt
AND EXISTS (
SELECT 1
FROM attempt at
JOIN attempt_status s ON s.status_code = at.status
WHERE at.id_attempt = a.id_attempt
AND s.is_successful = 1
)
WHERE t.id_test = p_id_test
AND t.uid_author = p_uid_author
GROUP BY qt.order_num, q.question_text
ORDER BY qt.order_num;
RETURN rc;
END;
FUNCTION list_admin_users(p_uid_admin NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
require_admin(p_uid_admin);
OPEN rc FOR
SELECT u.user_id, u.user_name, r.role_name, u.is_active, u.created_at
FROM users u
JOIN role r ON r.id_role = u.id_role
ORDER BY u.user_id DESC;
RETURN rc;
END;
FUNCTION list_admin_tests(p_uid_admin NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
require_admin(p_uid_admin);
OPEN rc FOR
SELECT t.id_test, t.test_name, u.user_name, t.is_active, t.created_at, t.attempt_limit, t.question_count
FROM test t
JOIN users u ON u.user_id = t.uid_author
ORDER BY t.id_test DESC;
RETURN rc;
END;
FUNCTION list_admin_questions(p_uid_admin NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
require_admin(p_uid_admin);
OPEN rc FOR
SELECT q.id_question, q.question_text, u.user_name, qt.type_name, q.is_active, q.created_at
FROM question q
JOIN users u ON u.user_id = q.uid_author
JOIN question_type qt ON qt.type_id = q.type_id
ORDER BY q.id_question DESC;
RETURN rc;
END;
FUNCTION get_admin_statistics(p_uid_admin NUMBER) RETURN SYS_REFCURSOR IS
rc SYS_REFCURSOR;
BEGIN
require_admin(p_uid_admin);
OPEN rc FOR
SELECT
(SELECT COUNT(*) FROM users) AS users_total,
(SELECT COUNT(*) FROM users WHERE is_active = 1) AS users_active,
(SELECT COUNT(*) FROM test) AS tests_total,
(SELECT COUNT(*) FROM test WHERE is_active = 1) AS tests_published,
(SELECT COUNT(*) FROM question) AS questions_total,
(SELECT COUNT(*) FROM attempt) AS attempts_total
FROM dual;
RETURN rc;
END;
PROCEDURE show_result(p_id_attempt NUMBER) IS
v_test_name test.test_name%TYPE;
v_attempt_number attempt.attempt_number%TYPE;
v_status attempt.status%TYPE;
v_score attempt.score%TYPE;
v_percent attempt.percent_result%TYPE;
v_avg_percent NUMBER;
BEGIN
SELECT t.test_name, a.attempt_number, a.status, a.score, a.percent_result
INTO v_test_name, v_attempt_number, v_status, v_score, v_percent
FROM attempt a
JOIN test t ON t.id_test = a.id_test
WHERE a.id_attempt = p_id_attempt;
SELECT NVL(AVG(x.percent_result), 0)
INTO v_avg_percent
FROM attempt x
WHERE x.id_test = (SELECT id_test FROM attempt WHERE id_attempt = p_id_attempt)
AND EXISTS (
SELECT 1
FROM attempt_status s
WHERE s.status_code = x.status
AND s.is_successful = 1
)
AND x.percent_result IS NOT NULL;
DBMS_OUTPUT.PUT_LINE('Тест: ' || v_test_name);
DBMS_OUTPUT.PUT_LINE('Номер попытки: ' || v_attempt_number);
DBMS_OUTPUT.PUT_LINE('Статус: ' || v_status);
DBMS_OUTPUT.PUT_LINE('Балл: ' || NVL(v_score, 0));
DBMS_OUTPUT.PUT_LINE('Процент: ' || NVL(v_percent, 0));
DBMS_OUTPUT.PUT_LINE('Средний процент по тесту: ' || ROUND(v_avg_percent, 2));
DBMS_OUTPUT.PUT_LINE('Разница: ' || ROUND(NVL(v_percent, 0) - v_avg_percent, 2));
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20400, 'Попытка не найдена');
END;
PROCEDURE show_user_attempts(p_uid NUMBER) IS
v_found NUMBER := 0;
BEGIN
FOR rec IN (
SELECT a.id_attempt, t.test_name, a.attempt_number, a.status, a.score, a.percent_result
FROM attempt a
JOIN test t ON t.id_test = a.id_test
WHERE a.user_id = p_uid
ORDER BY a.id_attempt DESC
) LOOP
v_found := 1;
DBMS_OUTPUT.PUT_LINE(
'Попытка #' || rec.id_attempt ||
' | Тест: ' || rec.test_name ||
' | Номер: ' || rec.attempt_number ||
' | Статус: ' || rec.status ||
' | Балл: ' || NVL(rec.score, 0) ||
' | Процент: ' || NVL(rec.percent_result, 0)
);
END LOOP;
IF v_found = 0 THEN
DBMS_OUTPUT.PUT_LINE('У пользователя нет попыток.');
END IF;
END;
PROCEDURE show_test_statistics(p_id_test NUMBER) IS
v_test_name test.test_name%TYPE;
v_total_attempts NUMBER;
v_finished_attempts NUMBER;
v_avg_score NUMBER;
v_avg_percent NUMBER;
v_min_percent NUMBER;
v_max_percent NUMBER;
BEGIN
SELECT test_name INTO v_test_name FROM test WHERE id_test = p_id_test;
SELECT
COUNT(*),
SUM(CASE WHEN s.is_successful = 1 THEN 1 ELSE 0 END),
ROUND(AVG(CASE WHEN s.is_successful = 1 THEN score END), 2),
ROUND(AVG(CASE WHEN s.is_successful = 1 THEN percent_result END), 2),
MIN(CASE WHEN s.is_successful = 1 THEN percent_result END),
MAX(CASE WHEN s.is_successful = 1 THEN percent_result END)
INTO
v_total_attempts,
v_finished_attempts,
v_avg_score,
v_avg_percent,
v_min_percent,
v_max_percent
FROM attempt a
LEFT JOIN attempt_status s ON s.status_code = a.status
WHERE a.id_test = p_id_test;
DBMS_OUTPUT.PUT_LINE('Статистика теста: ' || v_test_name || ' (#' || p_id_test || ')');
DBMS_OUTPUT.PUT_LINE('Попыток: ' || NVL(v_total_attempts, 0));
DBMS_OUTPUT.PUT_LINE('Завершено: ' || NVL(v_finished_attempts, 0));
DBMS_OUTPUT.PUT_LINE('Средний балл: ' || NVL(v_avg_score, 0));
DBMS_OUTPUT.PUT_LINE('Средний процент: ' || NVL(v_avg_percent, 0));
DBMS_OUTPUT.PUT_LINE('Минимальный процент: ' || NVL(v_min_percent, 0));
DBMS_OUTPUT.PUT_LINE('Максимальный процент: ' || NVL(v_max_percent, 0));
FOR q IN (
SELECT
qt.order_num,
q.question_text,
COUNT(a.id_answer) AS total_answers,
SUM(CASE WHEN a.is_correct = 1 THEN 1 ELSE 0 END) AS correct_answers,
ROUND(
CASE WHEN COUNT(a.id_answer) = 0 THEN 0
ELSE SUM(CASE WHEN a.is_correct = 1 THEN 1 ELSE 0 END) / COUNT(a.id_answer) * 100
END, 2
) AS percent_correct,
ROUND(AVG(a.answer_time), 2) AS avg_answer_time,
ROUND(AVG(a.earned_score), 2) AS avg_earned_score
FROM question_in_test qt
JOIN question q ON q.id_question = qt.id_question
LEFT JOIN answer a ON a.id_qt = qt.id_qt
AND EXISTS (
SELECT 1
FROM attempt at
JOIN attempt_status s ON s.status_code = at.status
WHERE at.id_attempt = a.id_attempt
AND s.is_successful = 1
)
WHERE qt.id_test = p_id_test
GROUP BY qt.order_num, q.question_text
ORDER BY qt.order_num
) LOOP
DBMS_OUTPUT.PUT_LINE(
'Q' || q.order_num ||
' | ' || q.question_text ||
' | Ответов: ' || NVL(q.total_answers, 0) ||
' | Правильных: ' || NVL(q.correct_answers, 0) ||
' | %: ' || NVL(q.percent_correct, 0) ||
' | Ср. время: ' || NVL(q.avg_answer_time, 0) ||
' | Ср. балл: ' || NVL(q.avg_earned_score, 0)
);
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE_APPLICATION_ERROR(-20401, 'Тест не найден');
END;
END quiz_platform;
/