From a8515d15433e0f47be600b3ff10f5c46d9dcf95d Mon Sep 17 00:00:00 2001 From: george-misan Date: Tue, 12 Mar 2024 12:50:04 +0200 Subject: [PATCH 01/34] add migration files for the feature --- ...0312115600_add_suspected_cheaters.down.sql | 1 + ...240312115600_add_suspected_cheaters.up.sql | 18 +++++++++++++ ..._suspected_cheaters_exercise_list.down.sql | 1 + ...dd_suspected_cheaters_exercise_list.up.sql | 25 +++++++++++++++++++ 4 files changed, 45 insertions(+) create mode 100644 services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql create mode 100644 services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql create mode 100644 services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql create mode 100644 services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql new file mode 100644 index 000000000000..7e0a3e62137a --- /dev/null +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql @@ -0,0 +1 @@ +DROP TABLE suspected_cheaters; diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql new file mode 100644 index 000000000000..c2c09d19a73e --- /dev/null +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE suspected_cheaters ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + student_id UUID NOT NULL REFERENCES course_module_completion, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + deleted_at TIMESTAMP WITH TIME ZONE, + total_duration VARCHAR(32) NOT NULL, + student_average_duration VARCHAR(32) NOT NULL, + total_points VARCHAR(32) NOT NULL student_average_points VARCHAR(32) NOT NULL +); +COMMENT ON TABLE suspected_cheaters IS 'This table stores data regarding student that are suspected of cheating in a course.'; +COMMENT ON COLUMN suspected_cheaters.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN suspected_cheaters.student_id IS 'The id of the student being suspected.'; +COMMENT ON COLUMN suspected_cheaters.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +COMMENT ON COLUMN suspected_cheaters.total_duration IS 'The total duration the student spend completing the course.'; +COMMENT ON COLUMN suspected_cheaters.student_average_duration IS 'The average total duration other student spent completing the course.'; +COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points the student acquired in the course.'; +COMMENT ON COLUMN suspected_cheaters.student_average_points IS 'The average total points other students acquired in the course.'; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql new file mode 100644 index 000000000000..ffcc39447327 --- /dev/null +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql @@ -0,0 +1 @@ +DROP TABLE exercise_list_of_suspected_cheaters; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql new file mode 100644 index 000000000000..3ecbf623c791 --- /dev/null +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE exercise_list_of_suspected_cheaters_exercise_list ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + student_id UUID NOT NULL REFERENCES course_module_completion, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + deleted_at TIMESTAMP WITH TIME ZONE, + exercise_id UUID NOT NULL, + duration VARCHAR(32) NOT NULL, + student_average_duration VARCHAR(32) NOT NULL, + points VARCHAR(32) NOT NULL, + student_average_points VARCHAR(32) NOT NULL, + attempts VARCHAR(32) NOT NULL, + status TEXT NOT NULL +); +COMMENT ON TABLE suspected_cheaters_exercise_list IS 'This table stores data regarding the list of exercises pertaining to students that have been suspected of cheating in a course.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.student_id IS 'The id of the student being suspected.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.exercise_id IS 'Exercise Id of an exercise completed by the suspected student.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.duration IS 'The duration a suspected student used in completing an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.student_average_duration IS 'The average duration a other student used in completing an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspected student received from completing an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.student_average_points IS 'The average points other student received from completing an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; From a693be6ff89ec03f854d28cfae7b7285954cd4df Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 07:23:16 +0300 Subject: [PATCH 02/34] [cheater-feature]: update migration files --- ...0312115600_add_suspected_cheaters.down.sql | 1 + ...240312115600_add_suspected_cheaters.up.sql | 36 +++++++++++-- ..._suspected_cheaters_exercise_list.down.sql | 1 + ...dd_suspected_cheaters_exercise_list.up.sql | 51 ++++++++++++++++--- 4 files changed, 79 insertions(+), 10 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql index 7e0a3e62137a..cbd8c3c503d2 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql @@ -1 +1,2 @@ DROP TABLE suspected_cheaters; +DROP TABLE student_average; diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index c2c09d19a73e..d818354dee41 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -2,17 +2,47 @@ CREATE TABLE suspected_cheaters ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, student_id UUID NOT NULL REFERENCES course_module_completion, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - total_duration VARCHAR(32) NOT NULL, - student_average_duration VARCHAR(32) NOT NULL, - total_points VARCHAR(32) NOT NULL student_average_points VARCHAR(32) NOT NULL + total_duration INTEGER NOT NULL, + total_points INTEGER NOT NULL, ); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON suspected_cheaters +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + COMMENT ON TABLE suspected_cheaters IS 'This table stores data regarding student that are suspected of cheating in a course.'; COMMENT ON COLUMN suspected_cheaters.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters.student_id IS 'The id of the student being suspected.'; COMMENT ON COLUMN suspected_cheaters.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN suspected_cheaters.updates_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters.total_duration IS 'The total duration the student spend completing the course.'; COMMENT ON COLUMN suspected_cheaters.student_average_duration IS 'The average total duration other student spent completing the course.'; COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points the student acquired in the course.'; COMMENT ON COLUMN suspected_cheaters.student_average_points IS 'The average total points other students acquired in the course.'; + +CREATE TABLE course_student_average ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + course_id UUID NOT NULL REFERENCES courses, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + deleted_at TIMESTAMP WITH TIME ZONE, + course_average_duration INTEGER NOT NULL, + course_average_points INTEGER NOT NULL, +); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON course_student_average +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +COMMENT ON TABLE course_student_average IS 'This table stores data regarding the average in a specific course.'; +COMMENT ON COLUMN course_student_average.course_id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN course_student_average.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN course_student_average.updates_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN course_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +COMMENT ON COLUMN course_student_average.student_average_duration IS 'The average total duration other student spent completing the course.'; +COMMENT ON COLUMN course_student_average.student_average_points IS 'The average total points other students acquired in the course.'; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql index ffcc39447327..155db2d15898 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql @@ -1 +1,2 @@ DROP TABLE exercise_list_of_suspected_cheaters; +DROP TABLE exercise_student_average; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql index 3ecbf623c791..3013c15271c7 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -3,14 +3,18 @@ CREATE TABLE exercise_list_of_suspected_cheaters_exercise_list ( student_id UUID NOT NULL REFERENCES course_module_completion, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - exercise_id UUID NOT NULL, - duration VARCHAR(32) NOT NULL, - student_average_duration VARCHAR(32) NOT NULL, - points VARCHAR(32) NOT NULL, - student_average_points VARCHAR(32) NOT NULL, - attempts VARCHAR(32) NOT NULL, - status TEXT NOT NULL + exercise_id UUID REFERENCES exercises NOT NULL , + duration INTEGER NOT NULL, + points INTEGER NOT NULL, + attempts INTEGER NOT NULL, + status activity_progress NOT NULL DEFAULT 'initialized', ); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON exercise_list_of_suspected_cheaters_exercise_list +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + COMMENT ON TABLE suspected_cheaters_exercise_list IS 'This table stores data regarding the list of exercises pertaining to students that have been suspected of cheating in a course.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.student_id IS 'The id of the student being suspected.'; @@ -23,3 +27,36 @@ COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspe COMMENT ON COLUMN suspected_cheaters_exercise_list.student_average_points IS 'The average points other student received from completing an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; + +-- user_exercise_states +CREATE TYPE activity_progress AS ENUM ( + 'initialized', + 'started', + 'in-progress', + 'submitted', + 'completed' +); + +CREATE TABLE exercise_student_average ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + exercise_id UUID NOT NULL REFERENCES exercise, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + deleted_at TIMESTAMP WITH TIME ZONE, + exercise_average_duration INTEGER NOT NULL, + exercise_average_points INTEGER NOT NULL, +); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON exercise_student_average +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +COMMENT ON TABLE exercise_student_average IS 'This table stores data regarding the average in a specific course.'; +COMMENT ON COLUMN exercise_student_average.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of the exercise.'; +COMMENT ON COLUMN exercise_student_average.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN exercise_student_average.updates_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +COMMENT ON COLUMN exercise_student_average.student_average_duration IS 'The average duration a other student used in completing an exercise.'; +COMMENT ON COLUMN exercise_student_average.student_average_points IS 'The average points other student received from completing an exercise.'; From 43998a5ca9677b175dd2f4be207df97f7ac73746 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 09:11:06 +0300 Subject: [PATCH 03/34] [cheater-feature]: update migration files --- .../20240312115600_add_suspected_cheaters.up.sql | 8 +++----- ...21500_add_suspected_cheaters_exercise_list.up.sql | 12 +++++------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index d818354dee41..ba635518b373 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -5,7 +5,7 @@ CREATE TABLE suspected_cheaters ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, total_duration INTEGER NOT NULL, - total_points INTEGER NOT NULL, + total_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp @@ -20,9 +20,7 @@ COMMENT ON COLUMN suspected_cheaters.created_at IS 'Timestamp when the record wa COMMENT ON COLUMN suspected_cheaters.updates_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters.total_duration IS 'The total duration the student spend completing the course.'; -COMMENT ON COLUMN suspected_cheaters.student_average_duration IS 'The average total duration other student spent completing the course.'; COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points the student acquired in the course.'; -COMMENT ON COLUMN suspected_cheaters.student_average_points IS 'The average total points other students acquired in the course.'; CREATE TABLE course_student_average ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, @@ -44,5 +42,5 @@ COMMENT ON COLUMN course_student_average.course_id IS 'A unique, stable identifi COMMENT ON COLUMN course_student_average.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN course_student_average.updates_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN course_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN course_student_average.student_average_duration IS 'The average total duration other student spent completing the course.'; -COMMENT ON COLUMN course_student_average.student_average_points IS 'The average total points other students acquired in the course.'; +COMMENT ON COLUMN course_student_average.student_average_duration IS 'The average duration all student spent completing the course.'; +COMMENT ON COLUMN course_student_average.student_average_points IS 'The average points all students acquired in the course.'; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql index 3013c15271c7..cf233dce92f0 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -7,7 +7,7 @@ CREATE TABLE exercise_list_of_suspected_cheaters_exercise_list ( duration INTEGER NOT NULL, points INTEGER NOT NULL, attempts INTEGER NOT NULL, - status activity_progress NOT NULL DEFAULT 'initialized', + status activity_progress NOT NULL DEFAULT 'initialized' ); CREATE TRIGGER set_timestamp @@ -22,13 +22,11 @@ COMMENT ON COLUMN suspected_cheaters_exercise_list.created_at IS 'Timestamp when COMMENT ON COLUMN suspected_cheaters_exercise_list.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.exercise_id IS 'Exercise Id of an exercise completed by the suspected student.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.duration IS 'The duration a suspected student used in completing an exercise.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.student_average_duration IS 'The average duration a other student used in completing an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspected student received from completing an exercise.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.student_average_points IS 'The average points other student received from completing an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; --- user_exercise_states +-- suspected_cheater_exercise_states CREATE TYPE activity_progress AS ENUM ( 'initialized', 'started', @@ -44,7 +42,7 @@ CREATE TABLE exercise_student_average ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, exercise_average_duration INTEGER NOT NULL, - exercise_average_points INTEGER NOT NULL, + exercise_average_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp @@ -58,5 +56,5 @@ COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of th COMMENT ON COLUMN exercise_student_average.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN exercise_student_average.updates_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN exercise_student_average.student_average_duration IS 'The average duration a other student used in completing an exercise.'; -COMMENT ON COLUMN exercise_student_average.student_average_points IS 'The average points other student received from completing an exercise.'; +COMMENT ON COLUMN exercise_student_average.student_average_duration IS 'The average duration a all student used in completing an exercise.'; +COMMENT ON COLUMN exercise_student_average.student_average_points IS 'The average points all student received from completing an exercise.'; From d40c2f83b56c281a00d59dac13dd63c95d6945c6 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 09:19:07 +0300 Subject: [PATCH 04/34] [cheater-feature]: update migration files --- .../migrations/20240312115600_add_suspected_cheaters.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index ba635518b373..000267567693 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -29,7 +29,7 @@ CREATE TABLE course_student_average ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, course_average_duration INTEGER NOT NULL, - course_average_points INTEGER NOT NULL, + course_average_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp From c66b946419fae112ae47da54fcde3d80a09ff8ba Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 09:26:45 +0300 Subject: [PATCH 05/34] [cheater-feature]: update migration files --- .../20240312121500_add_suspected_cheaters_exercise_list.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql index cf233dce92f0..d4746eb7ec4d 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -1,6 +1,6 @@ CREATE TABLE exercise_list_of_suspected_cheaters_exercise_list ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - student_id UUID NOT NULL REFERENCES course_module_completion, + student_id UUID NOT NULL REFERENCES course_module_completions, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, exercise_id UUID REFERENCES exercises NOT NULL , From 3341d1d051c1cd05dade09699f20ab48db98a338 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 09:59:37 +0300 Subject: [PATCH 06/34] [cheater-feature]: update migration files --- .../migrations/20240312115600_add_suspected_cheaters.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 000267567693..72ad07ba3ab1 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -1,6 +1,6 @@ CREATE TABLE suspected_cheaters ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - student_id UUID NOT NULL REFERENCES course_module_completion, + student_id UUID NOT NULL REFERENCES course_module_completions, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, From fce9ec2406ce9580568dbf760d01d88d697670bb Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 10:57:20 +0300 Subject: [PATCH 07/34] [cheater-feature]: update migration files --- .../migrations/20240312115600_add_suspected_cheaters.down.sql | 2 +- .../migrations/20240312115600_add_suspected_cheaters.up.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql index cbd8c3c503d2..3f56a2ea6a87 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql @@ -1,2 +1,2 @@ DROP TABLE suspected_cheaters; -DROP TABLE student_average; +DROP TABLE course_student_average; diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 72ad07ba3ab1..f05aac6b3f68 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -17,7 +17,7 @@ COMMENT ON TABLE suspected_cheaters IS 'This table stores data regarding student COMMENT ON COLUMN suspected_cheaters.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters.student_id IS 'The id of the student being suspected.'; COMMENT ON COLUMN suspected_cheaters.created_at IS 'Timestamp when the record was created.'; -COMMENT ON COLUMN suspected_cheaters.updates_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN suspected_cheaters.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters.total_duration IS 'The total duration the student spend completing the course.'; COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points the student acquired in the course.'; From ceb852b6edf7d861d6f11823f31d8f0475cc1693 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 11:54:49 +0300 Subject: [PATCH 08/34] [cheater-feature]: update migration files --- .../migrations/20240312115600_add_suspected_cheaters.up.sql | 2 +- ...20240312121500_add_suspected_cheaters_exercise_list.up.sql | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index f05aac6b3f68..06f31e7ec5a3 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -40,7 +40,7 @@ EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE course_student_average IS 'This table stores data regarding the average in a specific course.'; COMMENT ON COLUMN course_student_average.course_id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN course_student_average.created_at IS 'Timestamp when the record was created.'; -COMMENT ON COLUMN course_student_average.updates_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN course_student_average.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN course_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN course_student_average.student_average_duration IS 'The average duration all student spent completing the course.'; COMMENT ON COLUMN course_student_average.student_average_points IS 'The average points all students acquired in the course.'; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql index d4746eb7ec4d..64a1aaff8551 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -2,6 +2,7 @@ CREATE TABLE exercise_list_of_suspected_cheaters_exercise_list ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, student_id UUID NOT NULL REFERENCES course_module_completions, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, exercise_id UUID REFERENCES exercises NOT NULL , duration INTEGER NOT NULL, @@ -18,6 +19,7 @@ EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE suspected_cheaters_exercise_list IS 'This table stores data regarding the list of exercises pertaining to students that have been suspected of cheating in a course.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.student_id IS 'The id of the student being suspected.'; +COMMENT ON COLUMN exercise_list_of_suspected_cheaters_exercise_list.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.exercise_id IS 'Exercise Id of an exercise completed by the suspected student.'; @@ -54,7 +56,7 @@ COMMENT ON TABLE exercise_student_average IS 'This table stores data regarding t COMMENT ON COLUMN exercise_student_average.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of the exercise.'; COMMENT ON COLUMN exercise_student_average.created_at IS 'Timestamp when the record was created.'; -COMMENT ON COLUMN exercise_student_average.updates_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN exercise_student_average.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN exercise_student_average.student_average_duration IS 'The average duration a all student used in completing an exercise.'; COMMENT ON COLUMN exercise_student_average.student_average_points IS 'The average points all student received from completing an exercise.'; From 818e1a7d74516a343111048f8ffebd586446b484 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 12:09:13 +0300 Subject: [PATCH 09/34] [cheater-feature]: update migration files --- .../20240312115600_add_suspected_cheaters.up.sql | 8 ++++---- ...0312121500_add_suspected_cheaters_exercise_list.up.sql | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 06f31e7ec5a3..1449fc72d72a 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -28,8 +28,8 @@ CREATE TABLE course_student_average ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - course_average_duration INTEGER NOT NULL, - course_average_points INTEGER NOT NULL + average_duration INTEGER NOT NULL, + average_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp @@ -42,5 +42,5 @@ COMMENT ON COLUMN course_student_average.course_id IS 'A unique, stable identifi COMMENT ON COLUMN course_student_average.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN course_student_average.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN course_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN course_student_average.student_average_duration IS 'The average duration all student spent completing the course.'; -COMMENT ON COLUMN course_student_average.student_average_points IS 'The average points all students acquired in the course.'; +COMMENT ON COLUMN course_student_average.average_duration IS 'The average duration all student spent completing the course.'; +COMMENT ON COLUMN course_student_average.average_points IS 'The average points all students acquired in the course.'; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql index 64a1aaff8551..6e24786cbaa4 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -43,8 +43,8 @@ CREATE TABLE exercise_student_average ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - exercise_average_duration INTEGER NOT NULL, - exercise_average_points INTEGER NOT NULL + average_duration INTEGER NOT NULL, + average_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp @@ -58,5 +58,5 @@ COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of th COMMENT ON COLUMN exercise_student_average.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN exercise_student_average.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN exercise_student_average.student_average_duration IS 'The average duration a all student used in completing an exercise.'; -COMMENT ON COLUMN exercise_student_average.student_average_points IS 'The average points all student received from completing an exercise.'; +COMMENT ON COLUMN exercise_student_average.average_duration IS 'The average duration a all student used in completing an exercise.'; +COMMENT ON COLUMN exercise_student_average.average_points IS 'The average points all student received from completing an exercise.'; From 7cc7d8aea82ff0505b8086c7a630bc2b01f7b0f8 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 12:37:48 +0300 Subject: [PATCH 10/34] [cheater-feature]: update migration files --- .../20240312121500_add_suspected_cheaters_exercise_list.up.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql index 6e24786cbaa4..1a92ea509122 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -1,4 +1,4 @@ -CREATE TABLE exercise_list_of_suspected_cheaters_exercise_list ( +CREATE TABLE exercise_list_of_suspected_cheaters ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, student_id UUID NOT NULL REFERENCES course_module_completions, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), From 5047e7f55828815981eb8b9a7675b6bc2cc6f337 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 12:58:14 +0300 Subject: [PATCH 11/34] [cheater-feature]: update migration files --- ...240312121500_add_suspected_cheaters_exercise_list.down.sql | 2 +- ...20240312121500_add_suspected_cheaters_exercise_list.up.sql | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql index 155db2d15898..38eec42818e0 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql @@ -1,2 +1,2 @@ -DROP TABLE exercise_list_of_suspected_cheaters; +DROP TABLE suspected_cheaters_exercise_list; DROP TABLE exercise_student_average; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql index 1a92ea509122..574b3659cdff 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -1,4 +1,4 @@ -CREATE TABLE exercise_list_of_suspected_cheaters ( +CREATE TABLE suspected_cheaters_exercise_list ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, student_id UUID NOT NULL REFERENCES course_module_completions, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), @@ -19,7 +19,7 @@ EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE suspected_cheaters_exercise_list IS 'This table stores data regarding the list of exercises pertaining to students that have been suspected of cheating in a course.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.student_id IS 'The id of the student being suspected.'; -COMMENT ON COLUMN exercise_list_of_suspected_cheaters_exercise_list.updated_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.exercise_id IS 'Exercise Id of an exercise completed by the suspected student.'; From 5fedc9960be99d4b9ff16405cde1bd1bda5e4b54 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 16:52:03 +0300 Subject: [PATCH 12/34] [cheater-feature]: update migration files --- .../20240312115600_add_suspected_cheaters.up.sql | 4 ++-- ...1500_add_suspected_cheaters_exercise_list.up.sql | 13 ++----------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 1449fc72d72a..2fbfcb27461b 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -1,6 +1,6 @@ CREATE TABLE suspected_cheaters ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - student_id UUID NOT NULL REFERENCES course_module_completions, + user_id UUID NOT NULL REFERENCES users, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, @@ -15,7 +15,7 @@ EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE suspected_cheaters IS 'This table stores data regarding student that are suspected of cheating in a course.'; COMMENT ON COLUMN suspected_cheaters.id IS 'A unique, stable identifier for the record.'; -COMMENT ON COLUMN suspected_cheaters.student_id IS 'The id of the student being suspected.'; +COMMENT ON COLUMN suspected_cheaters.user_id IS 'The user_id of the student being suspected.'; COMMENT ON COLUMN suspected_cheaters.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN suspected_cheaters.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql index 574b3659cdff..8609ee11c76c 100644 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql @@ -12,7 +12,7 @@ CREATE TABLE suspected_cheaters_exercise_list ( ); CREATE TRIGGER set_timestamp -BEFORE UPDATE ON exercise_list_of_suspected_cheaters_exercise_list +BEFORE UPDATE ON suspected_cheaters_exercise_list FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); @@ -28,18 +28,9 @@ COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspe COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; --- suspected_cheater_exercise_states -CREATE TYPE activity_progress AS ENUM ( - 'initialized', - 'started', - 'in-progress', - 'submitted', - 'completed' -); - CREATE TABLE exercise_student_average ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - exercise_id UUID NOT NULL REFERENCES exercise, + exercise_id UUID NOT NULL REFERENCES exercises, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, From 45d75aa7ee0b7712e2109e166c52178dc78c4dc3 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 4 Apr 2024 16:56:32 +0300 Subject: [PATCH 13/34] [migration-task]: merge migration into one file --- ...0312115600_add_suspected_cheaters.down.sql | 5 ++ ...240312115600_add_suspected_cheaters.up.sql | 57 +++++++++++++++++++ ..._suspected_cheaters_exercise_list.down.sql | 2 - ...dd_suspected_cheaters_exercise_list.up.sql | 53 ----------------- 4 files changed, 62 insertions(+), 55 deletions(-) delete mode 100644 services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql delete mode 100644 services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql index 3f56a2ea6a87..f1f8fee434d7 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql @@ -1,2 +1,7 @@ DROP TABLE suspected_cheaters; DROP TABLE course_student_average; + +-- + +DROP TABLE suspected_cheaters_exercise_list; +DROP TABLE exercise_student_average; diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 2fbfcb27461b..782fb4ee2120 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -44,3 +44,60 @@ COMMENT ON COLUMN course_student_average.updated_at IS 'Timestamp when the recor COMMENT ON COLUMN course_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN course_student_average.average_duration IS 'The average duration all student spent completing the course.'; COMMENT ON COLUMN course_student_average.average_points IS 'The average points all students acquired in the course.'; + +-- + +CREATE TABLE suspected_cheaters_exercise_list ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + student_id UUID NOT NULL REFERENCES course_module_completions, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + deleted_at TIMESTAMP WITH TIME ZONE, + exercise_id UUID REFERENCES exercises NOT NULL , + duration INTEGER NOT NULL, + points INTEGER NOT NULL, + attempts INTEGER NOT NULL, + status activity_progress NOT NULL DEFAULT 'initialized' +); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON suspected_cheaters_exercise_list +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +COMMENT ON TABLE suspected_cheaters_exercise_list IS 'This table stores data regarding the list of exercises pertaining to students that have been suspected of cheating in a course.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.student_id IS 'The id of the student being suspected.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.updated_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.exercise_id IS 'Exercise Id of an exercise completed by the suspected student.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.duration IS 'The duration a suspected student used in completing an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspected student received from completing an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; + +CREATE TABLE exercise_student_average ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + exercise_id UUID NOT NULL REFERENCES exercises, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + deleted_at TIMESTAMP WITH TIME ZONE, + average_duration INTEGER NOT NULL, + average_points INTEGER NOT NULL +); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON exercise_student_average +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +COMMENT ON TABLE exercise_student_average IS 'This table stores data regarding the average in a specific course.'; +COMMENT ON COLUMN exercise_student_average.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of the exercise.'; +COMMENT ON COLUMN exercise_student_average.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN exercise_student_average.updated_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +COMMENT ON COLUMN exercise_student_average.average_duration IS 'The average duration a all student used in completing an exercise.'; +COMMENT ON COLUMN exercise_student_average.average_points IS 'The average points all student received from completing an exercise.'; + diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql deleted file mode 100644 index 38eec42818e0..000000000000 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.down.sql +++ /dev/null @@ -1,2 +0,0 @@ -DROP TABLE suspected_cheaters_exercise_list; -DROP TABLE exercise_student_average; diff --git a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql b/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql deleted file mode 100644 index 8609ee11c76c..000000000000 --- a/services/headless-lms/migrations/20240312121500_add_suspected_cheaters_exercise_list.up.sql +++ /dev/null @@ -1,53 +0,0 @@ -CREATE TABLE suspected_cheaters_exercise_list ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - student_id UUID NOT NULL REFERENCES course_module_completions, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - deleted_at TIMESTAMP WITH TIME ZONE, - exercise_id UUID REFERENCES exercises NOT NULL , - duration INTEGER NOT NULL, - points INTEGER NOT NULL, - attempts INTEGER NOT NULL, - status activity_progress NOT NULL DEFAULT 'initialized' -); - -CREATE TRIGGER set_timestamp -BEFORE UPDATE ON suspected_cheaters_exercise_list -FOR EACH ROW -EXECUTE PROCEDURE trigger_set_timestamp(); - -COMMENT ON TABLE suspected_cheaters_exercise_list IS 'This table stores data regarding the list of exercises pertaining to students that have been suspected of cheating in a course.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.id IS 'A unique, stable identifier for the record.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.student_id IS 'The id of the student being suspected.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.updated_at IS 'Timestamp when the record was updated.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.created_at IS 'Timestamp when the record was created.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.exercise_id IS 'Exercise Id of an exercise completed by the suspected student.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.duration IS 'The duration a suspected student used in completing an exercise.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspected student received from completing an exercise.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; - -CREATE TABLE exercise_student_average ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - exercise_id UUID NOT NULL REFERENCES exercises, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - deleted_at TIMESTAMP WITH TIME ZONE, - average_duration INTEGER NOT NULL, - average_points INTEGER NOT NULL -); - -CREATE TRIGGER set_timestamp -BEFORE UPDATE ON exercise_student_average -FOR EACH ROW -EXECUTE PROCEDURE trigger_set_timestamp(); - -COMMENT ON TABLE exercise_student_average IS 'This table stores data regarding the average in a specific course.'; -COMMENT ON COLUMN exercise_student_average.id IS 'A unique, stable identifier for the record.'; -COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of the exercise.'; -COMMENT ON COLUMN exercise_student_average.created_at IS 'Timestamp when the record was created.'; -COMMENT ON COLUMN exercise_student_average.updated_at IS 'Timestamp when the record was updated.'; -COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN exercise_student_average.average_duration IS 'The average duration a all student used in completing an exercise.'; -COMMENT ON COLUMN exercise_student_average.average_points IS 'The average points all student received from completing an exercise.'; From d5d9b033e784c70bcbe715e60bd63f6c8cfb1ac2 Mon Sep 17 00:00:00 2001 From: george-misan Date: Mon, 8 Apr 2024 16:52:28 +0300 Subject: [PATCH 14/34] [cheater-feature]: WIP --- .../models/src/course_module_completions.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/services/headless-lms/models/src/course_module_completions.rs b/services/headless-lms/models/src/course_module_completions.rs index 0222634a0081..35cc7564e11f 100644 --- a/services/headless-lms/models/src/course_module_completions.rs +++ b/services/headless-lms/models/src/course_module_completions.rs @@ -26,6 +26,18 @@ pub struct CourseModuleCompletion { pub completion_granter_user_id: Option, } +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct CourseModuleAverage { + pub id: Uuid, + pub course_instance_id: Uuid, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, + pub average_duration: u64, + pub average_points: f32, +} + #[derive(Clone, PartialEq, Deserialize, Serialize)] pub enum CourseModuleCompletionGranter { Automatic, @@ -349,6 +361,31 @@ WHERE course_id = $1 Ok(res.count.unwrap_or(0)) } +// Get the student average in the course +pub async fn get_course_average( + conn: &mut PgConnection, + course_instance_id: Uuid, +) -> ModelResult { + let res = sqlx::query_as!( + CourseModuleAverage, + r" + SELECT course_instance_id, + SUM(grade) AS total_score, + AVG(grade) AS average_score, + COUNT(DISTINCT user_id) AS student_count + FROM + course_module_completions + WHERE + course_id = $1 + AND deleted_at IS NULL; + ", + course_instance_id + ) + .fetch_all(conn) + .await?; + Ok(res) +} + /// Gets automatically granted course module completion for the given user on the specified course /// instance. This entry is quaranteed to be unique in database by the index /// `course_module_automatic_completion_uniqueness`. From 7d58e6deb817286b1ccec804a83c41fb5f312079 Mon Sep 17 00:00:00 2001 From: george-misan Date: Tue, 9 Apr 2024 12:59:58 +0300 Subject: [PATCH 15/34] [cheater-feature]: add cheater_thresholds TABLE --- ...0312115600_add_suspected_cheaters.down.sql | 1 + ...240312115600_add_suspected_cheaters.up.sql | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql index f1f8fee434d7..c96d05823b98 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql @@ -5,3 +5,4 @@ DROP TABLE course_student_average; DROP TABLE suspected_cheaters_exercise_list; DROP TABLE exercise_student_average; +DROP TABLE cheater_thresholds; diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 782fb4ee2120..e2d552023f22 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -101,3 +101,28 @@ COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the rec COMMENT ON COLUMN exercise_student_average.average_duration IS 'The average duration a all student used in completing an exercise.'; COMMENT ON COLUMN exercise_student_average.average_points IS 'The average points all student received from completing an exercise.'; +CREATE TABLE cheater_thresholds ( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + course_id UUID NOT NULL REFERENCES courses, + created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), + deleted_at TIMESTAMP WITH TIME ZONE, + points INTEGER NOT NULL, + duration INTEGER NOT NULL +); + +CREATE TRIGGER set_timestamp +BEFORE UPDATE ON cheater_thresholds +FOR EACH ROW +EXECUTE PROCEDURE trigger_set_timestamp(); + +COMMENT ON TABLE cheater_thresholds IS 'This table stores threshold for measuring cheaters.'; +COMMENT ON COLUMN cheater_thresholds.id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN cheater_thresholds.course_id IS 'The course_id of the course.'; +COMMENT ON COLUMN cheater_thresholds.created_at IS 'Timestamp when the record was created.'; +COMMENT ON COLUMN cheater_thresholds.updated_at IS 'Timestamp when the record was updated.'; +COMMENT ON COLUMN cheater_thresholds.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; +COMMENT ON COLUMN cheater_thresholds.points IS 'The score threshold of the course.'; +COMMENT ON COLUMN cheater_thresholds.duration IS 'The duration threshold of the course.'; + + From e1f3df0d97cc907b00444dea4a64ea5108ca74df Mon Sep 17 00:00:00 2001 From: george-misan Date: Tue, 9 Apr 2024 13:12:01 +0300 Subject: [PATCH 16/34] [cheater-feature]: add cheater_thresholds TABLE --- .../models/src/suspected_cheaters.rs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 services/headless-lms/models/src/suspected_cheaters.rs diff --git a/services/headless-lms/models/src/suspected_cheaters.rs b/services/headless-lms/models/src/suspected_cheaters.rs new file mode 100644 index 000000000000..8b1f5258dc09 --- /dev/null +++ b/services/headless-lms/models/src/suspected_cheaters.rs @@ -0,0 +1,110 @@ +use crate::prelude::*; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct SuspectedCheaters { + pub id: Uuid, + pub user_id: Uuid, + pub created_at: DateTime, + pub deleted_at: Option>, + pub updated_at: Option>, + pub total_duration: u64, // Represented in milliseconds + pub total_points: f32, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct Threshold { + pub id: Uuid, + pub course_id: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, + pub score_threshold: f32, + pub duration_threshold: Option, +} + +pub async fn insert( + conn: &mut PgConnection, + user_id: Uuid, + total_duration: u64, + score_threshold: f32, +) -> SuspectedCheaters<()> { + sqlx::query!( + " + INSERT INTO suspected_cheaters ( + user_id, + total_duration, + total_points + ) + VALUES ($1, $2, $3) + ", + user_id, + total_duration, + total_points + ) + .execute(conn) + .await?; + Ok(()) +} + +pub async fn insert_threshold( + conn: &mut PgConnection, + user_id: Uuid, + duration: Option, + points: f32, +) -> SuspectedCheaters<()> { + sqlx::query!( + " + INSERT INTO suspected_cheaters ( + user_id, + duration, + points + ) + VALUES ($1, $2, $3) + ", + user_id, + duration, + points + ) + .execute(conn) + .await?; + Ok(()) +} + +pub async fn delete_suspected_cheaters( + conn: &mut PgConnection, + id: Uuid +) -> ModelResult { + let repsonse = sqlx::query!( + r#" + UPDATE suspected_cheaters + SET deleted_at = now() + WHERE id = $1 + RETURNING id + "#, + id + ) + .fetch_one(conn) + .await?; + Ok(response.id) +} + +pub async fn get_suspected_cheaters_by_id( + conn: &mut PgConnection, + id: Uuid, +) -> ModelResult { + let cheaters = sqlx::query_as!( + SuspectedCheaters, + " + SELECT * + FROM suspected_cheaters + WHERE id = $1 + AND deleted_at IS NULL; + ", + id + ) + .fetch_one(conn) + .await?; + Ok(cheaters) +} From 1cdb94f6b13ac227e3d7ccc0860c81c1b689e88b Mon Sep 17 00:00:00 2001 From: george-misan Date: Mon, 15 Apr 2024 15:30:11 +0300 Subject: [PATCH 17/34] stash commit --- .../models/src/user_exercise_states.rs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/services/headless-lms/models/src/user_exercise_states.rs b/services/headless-lms/models/src/user_exercise_states.rs index 2e50edd3c207..abe0aad755b0 100644 --- a/services/headless-lms/models/src/user_exercise_states.rs +++ b/services/headless-lms/models/src/user_exercise_states.rs @@ -564,6 +564,59 @@ WHERE id = $1 Ok(res) } +pub async fn get_user_exercise_state_by_user_id( + conn: &mut PgConnection, + user_id: Uuid, + course_instance_or_exam_id: CourseInstanceOrExamId, +) -> ModelResult< { + let (course_instance_id, exam_id) = course_instance_or_exam_id.to_instance_and_exam_ids(); + let res = sqlx::query_as!( + UserExerciseState, + r#" + SELECT + id, + user_id, + exercise_id, + course_instance_id, + exam_id, + created_at, + updated_at, + deleted_at, + score_given, + grading_progress AS "grading_progress: _", + activity_progress AS "activity_progress: _", + reviewing_stage AS "reviewing_stage: _", + selected_exercise_slide_id + FROM + user_exercise_states ues + JOIN ( + SELECT + exercise_id, + MAX(updated_at) AS max_updated_at, + COUNT(exercise.id) AS exercise_count + FROM + user_exercise_states + WHERE + user_id = $1 + AND (course_instance_id = $2 OR exam_id = $3) + AND deleted_at IS NULL + GROUP BY + exercise_id + ) max_dates ON ues.exercise_id = max_dates.exercise_id AND ues.updated_at = max_dates.max_updated_at + WHERE + user_id = $1 + AND (course_instance_id = $2 OR exam_id = $3) + AND deleted_at IS NULL + "#, + user_id, + course_instance_id, + exam_id + ) + .fetch_all(conn) + .await?; + Ok(res) +} + pub async fn get_users_current_by_exercise( conn: &mut PgConnection, user_id: Uuid, From bd6cac2ad6f78a72d7cd4348e367c290f424d413 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 18 Apr 2024 17:12:43 +0300 Subject: [PATCH 18/34] [cheater-feature]: add function to get total points of student in a course --- ...0f37f6e1c3bace558d4b5a762297c22a60ba5.json | 73 ------------ .../models/src/course_module_completions.rs | 35 ++++-- .../models/src/suspected_cheaters.rs | 52 ++++++-- .../models/src/user_exercise_states.rs | 111 +++++++++++------- 4 files changed, 137 insertions(+), 134 deletions(-) delete mode 100644 services/headless-lms/models/.sqlx/query-277f66811c6e7fc3d4e7dcc6f8b0f37f6e1c3bace558d4b5a762297c22a60ba5.json diff --git a/services/headless-lms/models/.sqlx/query-277f66811c6e7fc3d4e7dcc6f8b0f37f6e1c3bace558d4b5a762297c22a60ba5.json b/services/headless-lms/models/.sqlx/query-277f66811c6e7fc3d4e7dcc6f8b0f37f6e1c3bace558d4b5a762297c22a60ba5.json deleted file mode 100644 index fed3ead3b412..000000000000 --- a/services/headless-lms/models/.sqlx/query-277f66811c6e7fc3d4e7dcc6f8b0f37f6e1c3bace558d4b5a762297c22a60ba5.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nSELECT *\nFROM exercise_tasks\nWHERE exercise_slide_id = $1\n AND deleted_at IS NULL\n AND exercise_type = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 2, - "name": "updated_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 3, - "name": "exercise_type", - "type_info": "Varchar" - }, - { - "ordinal": 4, - "name": "assignment", - "type_info": "Jsonb" - }, - { - "ordinal": 5, - "name": "deleted_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 6, - "name": "private_spec", - "type_info": "Jsonb" - }, - { - "ordinal": 7, - "name": "public_spec", - "type_info": "Jsonb" - }, - { - "ordinal": 8, - "name": "model_solution_spec", - "type_info": "Jsonb" - }, - { - "ordinal": 9, - "name": "copied_from", - "type_info": "Uuid" - }, - { - "ordinal": 10, - "name": "exercise_slide_id", - "type_info": "Uuid" - }, - { - "ordinal": 11, - "name": "order_number", - "type_info": "Int4" - } - ], - "parameters": { - "Left": ["Uuid", "Text"] - }, - "nullable": [false, false, false, false, false, true, true, true, true, true, false, false] - }, - "hash": "277f66811c6e7fc3d4e7dcc6f8b0f37f6e1c3bace558d4b5a762297c22a60ba5" -} diff --git a/services/headless-lms/models/src/course_module_completions.rs b/services/headless-lms/models/src/course_module_completions.rs index 35cc7564e11f..344dd354f08d 100644 --- a/services/headless-lms/models/src/course_module_completions.rs +++ b/services/headless-lms/models/src/course_module_completions.rs @@ -34,8 +34,20 @@ pub struct CourseModuleAverage { pub created_at: DateTime, pub updated_at: DateTime, pub deleted_at: Option>, - pub average_duration: u64, - pub average_points: f32, + pub average_duration: Option, + pub average_points: i32, + pub total_points: i32, + pub total_student: i32, +} + +// Define the CourseModulePointsAverage struct to match the result of the SQL query +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct CourseModulePointsAverage { + pub course_instance_id: Uuid, + pub average_points: Option, + pub total_points: Option, + pub total_student: Option, } #[derive(Clone, PartialEq, Deserialize, Serialize)] @@ -365,23 +377,24 @@ WHERE course_id = $1 pub async fn get_course_average( conn: &mut PgConnection, course_instance_id: Uuid, -) -> ModelResult { +) -> ModelResult { let res = sqlx::query_as!( - CourseModuleAverage, - r" + CourseModulePointsAverage, + r#" SELECT course_instance_id, - SUM(grade) AS total_score, - AVG(grade) AS average_score, - COUNT(DISTINCT user_id) AS student_count + SUM(grade)::integer AS total_points, + AVG(grade)::REAL AS average_points, + COUNT(DISTINCT user_id)::integer AS total_student FROM course_module_completions WHERE course_id = $1 - AND deleted_at IS NULL; - ", + AND deleted_at IS NULL + GROUP BY course_instance_id; + "#, course_instance_id ) - .fetch_all(conn) + .fetch_one(conn) .await?; Ok(res) } diff --git a/services/headless-lms/models/src/suspected_cheaters.rs b/services/headless-lms/models/src/suspected_cheaters.rs index 8b1f5258dc09..d0cd03874950 100644 --- a/services/headless-lms/models/src/suspected_cheaters.rs +++ b/services/headless-lms/models/src/suspected_cheaters.rs @@ -29,7 +29,7 @@ pub async fn insert( user_id: Uuid, total_duration: u64, score_threshold: f32, -) -> SuspectedCheaters<()> { +) -> ModelResult<()> { sqlx::query!( " INSERT INTO suspected_cheaters ( @@ -48,22 +48,22 @@ pub async fn insert( Ok(()) } -pub async fn insert_threshold( +pub async fn insert_thresholds( conn: &mut PgConnection, - user_id: Uuid, + course_id: Uuid, duration: Option, points: f32, -) -> SuspectedCheaters<()> { +) -> ModelResult<()> { sqlx::query!( " - INSERT INTO suspected_cheaters ( - user_id, + INSERT INTO cheater_thresholds ( + course_id, duration, points ) VALUES ($1, $2, $3) ", - user_id, + course_id, duration, points ) @@ -72,6 +72,44 @@ pub async fn insert_threshold( Ok(()) } +pub async fn update_thresholds_by_point( + conn: &mut PgConnection, + course_id: Uuid, + points: f32, +) -> ModelResult<()> { + sqlx::query!( + " + UPDATE cheater_thresholds + SET points = $2 + WHERE course_id = $1 + ", + course_id, + points + ) + .execute(conn) + .await?; + Ok(()) +} + +pub async fn get_thresholds_by_id( + conn: &mut PgConnection, + course_id: Uuid, +) -> ModelResult { + let thresholds = sqlx::query_as!( + Threshold, + " + SELECT * + FROM cheater_thresholds + WHERE course_id = $1 + AND deleted_at IS NULL; + ", + id + ) + .fetch_one(conn) + .await?; + Ok(thresholds) +} + pub async fn delete_suspected_cheaters( conn: &mut PgConnection, id: Uuid diff --git a/services/headless-lms/models/src/user_exercise_states.rs b/services/headless-lms/models/src/user_exercise_states.rs index abe0aad755b0..7453a97eb1d4 100644 --- a/services/headless-lms/models/src/user_exercise_states.rs +++ b/services/headless-lms/models/src/user_exercise_states.rs @@ -564,59 +564,84 @@ WHERE id = $1 Ok(res) } -pub async fn get_user_exercise_state_by_user_id( +pub async fn get_user_total_course_points( conn: &mut PgConnection, user_id: Uuid, - course_instance_or_exam_id: CourseInstanceOrExamId, -) -> ModelResult< { - let (course_instance_id, exam_id) = course_instance_or_exam_id.to_instance_and_exam_ids(); - let res = sqlx::query_as!( - UserExerciseState, + course_instance_id: Uuid, +) -> ModelResult> { + let res = sqlx::query!( r#" - SELECT - id, - user_id, - exercise_id, - course_instance_id, - exam_id, - created_at, - updated_at, - deleted_at, - score_given, - grading_progress AS "grading_progress: _", - activity_progress AS "activity_progress: _", - reviewing_stage AS "reviewing_stage: _", - selected_exercise_slide_id - FROM - user_exercise_states ues - JOIN ( - SELECT - exercise_id, - MAX(updated_at) AS max_updated_at, - COUNT(exercise.id) AS exercise_count - FROM - user_exercise_states - WHERE - user_id = $1 - AND (course_instance_id = $2 OR exam_id = $3) - AND deleted_at IS NULL - GROUP BY - exercise_id - ) max_dates ON ues.exercise_id = max_dates.exercise_id AND ues.updated_at = max_dates.max_updated_at - WHERE - user_id = $1 - AND (course_instance_id = $2 OR exam_id = $3) - AND deleted_at IS NULL - "#, +SELECT SUM(score_given) AS "total_points" +FROM user_exercise_states +WHERE user_id = $1 + AND course_instance_id = $2 + AND deleted_at IS NULL + GROUP BY user_id + "#, user_id, course_instance_id, - exam_id ) - .fetch_all(conn) + .map(|x| x.total_points) + .fetch_one(conn) .await?; Ok(res) } +// TODO: why is map working here instead of just returning the value + +// pub async fn get_user_exercise_state_by_user_id( +// conn: &mut PgConnection, +// user_id: Uuid, +// course_instance_or_exam_id: CourseInstanceOrExamId, +// ) -> ModelResult< { +// let (course_instance_id, exam_id) = course_instance_or_exam_id.to_instance_and_exam_ids(); +// let res = sqlx::query_as!( +// UserExerciseState, +// r#" +// SELECT +// id, +// user_id, +// exercise_id, +// course_instance_id, +// exam_id, +// created_at, +// updated_at, +// deleted_at, +// score_given, +// grading_progress AS "grading_progress: _", +// activity_progress AS "activity_progress: _", +// reviewing_stage AS "reviewing_stage: _", +// selected_exercise_slide_id +// FROM +// user_exercise_states ues +// JOIN ( +// SELECT +// exercise_id, +// MAX(updated_at) AS max_updated_at, +// COUNT(exercise.id) AS exercise_count +// FROM +// user_exercise_states +// WHERE +// user_id = $1 +// AND (course_instance_id = $2 OR exam_id = $3) +// AND deleted_at IS NULL +// GROUP BY +// exercise_id +// ) max_dates ON ues.exercise_id = max_dates.exercise_id AND ues.updated_at = max_dates.max_updated_at +// WHERE +// user_id = $1 +// AND (course_instance_id = $2 OR exam_id = $3) +// AND deleted_at IS NULL +// "#, +// user_id, +// course_instance_id, +// exam_id +// ) +// .fetch_all(conn) +// .await?; +// Ok(res) +// } + pub async fn get_users_current_by_exercise( conn: &mut PgConnection, user_id: Uuid, From ff943d997b4457576b39607d3aab7bb7aeb3e85e Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 18 Apr 2024 17:14:15 +0300 Subject: [PATCH 19/34] [cheater-feature]: add function to get total points of student in a course --- ...6e9f531ef259a821556c742d5a60d622fe393.json | 33 +++++++++++++++++++ ...6f2003ecb5baecc29aec14f985b7f362f3b47.json | 18 ++++++++++ 2 files changed, 51 insertions(+) create mode 100644 services/headless-lms/models/.sqlx/query-16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393.json create mode 100644 services/headless-lms/models/.sqlx/query-a3c9ca6e04da53b7213792f76816f2003ecb5baecc29aec14f985b7f362f3b47.json diff --git a/services/headless-lms/models/.sqlx/query-16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393.json b/services/headless-lms/models/.sqlx/query-16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393.json new file mode 100644 index 000000000000..94ed8e0b61a6 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393.json @@ -0,0 +1,33 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT course_instance_id,\n SUM(grade)::integer AS total_points,\n AVG(grade)::REAL AS average_points,\n COUNT(DISTINCT user_id)::integer AS total_student\n FROM \n course_module_completions\n WHERE \n course_id = $1\n AND deleted_at IS NULL\n GROUP BY course_instance_id;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "course_instance_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "total_points", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "average_points", + "type_info": "Float4" + }, + { + "ordinal": 3, + "name": "total_student", + "type_info": "Int4" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, null, null, null] + }, + "hash": "16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393" +} diff --git a/services/headless-lms/models/.sqlx/query-a3c9ca6e04da53b7213792f76816f2003ecb5baecc29aec14f985b7f362f3b47.json b/services/headless-lms/models/.sqlx/query-a3c9ca6e04da53b7213792f76816f2003ecb5baecc29aec14f985b7f362f3b47.json new file mode 100644 index 000000000000..6baeacb5fe61 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-a3c9ca6e04da53b7213792f76816f2003ecb5baecc29aec14f985b7f362f3b47.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT SUM(score_given) AS \"total_points\"\nFROM user_exercise_states\nWHERE user_id = $1\n AND course_instance_id = $2\n AND deleted_at IS NULL\n GROUP BY user_id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "total_points", + "type_info": "Float4" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [null] + }, + "hash": "a3c9ca6e04da53b7213792f76816f2003ecb5baecc29aec14f985b7f362f3b47" +} From c74b4b2763b3a943cf055bfa7b2a5b2afd3468fe Mon Sep 17 00:00:00 2001 From: george-misan Date: Sun, 21 Apr 2024 14:10:51 +0300 Subject: [PATCH 20/34] [lint-job]: remove unused import --- services/headless-lms/models/src/lib.rs | 1 + .../models/src/suspected_cheaters.rs | 157 ++++++------ .../main_frontend/course_instances.rs | 39 +++ .../main_frontend/suspected_cheaters.rs | 223 ++++++++++++++++++ 4 files changed, 347 insertions(+), 73 deletions(-) create mode 100644 services/headless-lms/server/src/controllers/main_frontend/suspected_cheaters.rs diff --git a/services/headless-lms/models/src/lib.rs b/services/headless-lms/models/src/lib.rs index 70dfd9e35a42..de1697f28657 100644 --- a/services/headless-lms/models/src/lib.rs +++ b/services/headless-lms/models/src/lib.rs @@ -69,6 +69,7 @@ pub mod research_forms; pub mod roles; pub mod student_countries; pub mod study_registry_registrars; +pub mod suspected_cheaters; pub mod teacher_grading_decisions; pub mod url_redirections; pub mod user_course_instance_exercise_service_variables; diff --git a/services/headless-lms/models/src/suspected_cheaters.rs b/services/headless-lms/models/src/suspected_cheaters.rs index d0cd03874950..f40fae3e8db7 100644 --- a/services/headless-lms/models/src/suspected_cheaters.rs +++ b/services/headless-lms/models/src/suspected_cheaters.rs @@ -8,30 +8,44 @@ pub struct SuspectedCheaters { pub created_at: DateTime, pub deleted_at: Option>, pub updated_at: Option>, - pub total_duration: u64, // Represented in milliseconds - pub total_points: f32, + pub total_duration: i32, // Represented in milliseconds + pub total_points: i32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PointDurationData { + pub points: i32, + pub duration: i32, +} + +#[derive(Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct ThresholdData { + pub course_id: String, + pub url: String, + pub data: PointDurationData, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] #[cfg_attr(feature = "ts_rs", derive(TS))] pub struct Threshold { pub id: Uuid, - pub course_id: Option, + pub course_id: Uuid, pub created_at: DateTime, pub updated_at: DateTime, pub deleted_at: Option>, - pub score_threshold: f32, - pub duration_threshold: Option, + pub points: i32, + pub duration: Option, } pub async fn insert( - conn: &mut PgConnection, - user_id: Uuid, - total_duration: u64, - score_threshold: f32, + conn: &mut PgConnection, + user_id: Uuid, + total_duration: i32, + total_points: i32, ) -> ModelResult<()> { - sqlx::query!( - " + sqlx::query!( + " INSERT INTO suspected_cheaters ( user_id, total_duration, @@ -39,23 +53,23 @@ pub async fn insert( ) VALUES ($1, $2, $3) ", - user_id, - total_duration, - total_points - ) - .execute(conn) - .await?; - Ok(()) + user_id, + total_duration, + total_points + ) + .fetch_one(conn) + .await?; + Ok(()) } pub async fn insert_thresholds( - conn: &mut PgConnection, - course_id: Uuid, - duration: Option, - points: f32, + conn: &mut PgConnection, + course_id: Uuid, + duration: Option, + points: i32, ) -> ModelResult<()> { - sqlx::query!( - " + sqlx::query!( + " INSERT INTO cheater_thresholds ( course_id, duration, @@ -63,86 +77,83 @@ pub async fn insert_thresholds( ) VALUES ($1, $2, $3) ", - course_id, - duration, - points - ) - .execute(conn) - .await?; - Ok(()) + course_id, + duration, + points + ) + .execute(conn) + .await?; + Ok(()) } pub async fn update_thresholds_by_point( - conn: &mut PgConnection, - course_id: Uuid, - points: f32, + conn: &mut PgConnection, + course_id: Uuid, + points: i32, ) -> ModelResult<()> { - sqlx::query!( - " + sqlx::query!( + " UPDATE cheater_thresholds SET points = $2 WHERE course_id = $1 ", - course_id, - points - ) - .execute(conn) - .await?; - Ok(()) + course_id, + points + ) + .execute(conn) + .await?; + Ok(()) } pub async fn get_thresholds_by_id( - conn: &mut PgConnection, - course_id: Uuid, + conn: &mut PgConnection, + course_id: Uuid, ) -> ModelResult { - let thresholds = sqlx::query_as!( - Threshold, - " + let thresholds = sqlx::query_as!( + Threshold, + " SELECT * FROM cheater_thresholds WHERE course_id = $1 AND deleted_at IS NULL; ", - id - ) - .fetch_one(conn) - .await?; - Ok(thresholds) + course_id + ) + .fetch_one(conn) + .await?; + Ok(thresholds) } -pub async fn delete_suspected_cheaters( - conn: &mut PgConnection, - id: Uuid -) -> ModelResult { - let repsonse = sqlx::query!( - r#" +pub async fn delete_suspected_cheaters(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> { + sqlx::query!( + r#" UPDATE suspected_cheaters SET deleted_at = now() WHERE id = $1 RETURNING id "#, - id - ) - .fetch_one(conn) - .await?; - Ok(response.id) + id + ) + .fetch_one(conn) + .await?; + Ok(()) } pub async fn get_suspected_cheaters_by_id( - conn: &mut PgConnection, - id: Uuid, + conn: &mut PgConnection, + id: Uuid, ) -> ModelResult { - let cheaters = sqlx::query_as!( - SuspectedCheaters, - " + let cheaters = sqlx::query_as!( + SuspectedCheaters, + " SELECT * FROM suspected_cheaters WHERE id = $1 AND deleted_at IS NULL; ", - id - ) - .fetch_one(conn) - .await?; - Ok(cheaters) + id + ) + .fetch_one(conn) + .await?; + Ok(cheaters) } diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index f2c136b6fe7c..9de68a5fc45b 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -15,6 +15,7 @@ use models::{ TeacherManualCompletionRequest, }, }, + suspected_cheaters::ThresholdData, user_exercise_states::UserCourseInstanceProgress, }; @@ -442,6 +443,40 @@ async fn get_user_progress_for_course_instance( token.authorized_ok(web::Json(user_course_instance_progress)) } +/** + POST /api/v0/main-frontend/course-instance/:course_instance_id/threshold/:user_id - post course threshold information. +*/ +#[instrument(skip(pool))] +async fn insert_threshold( + pool: web::Data, + params: web::Path, + payload: web::Json, + user: AuthUser, +) -> ControllerResult> { + let mut conn = pool.acquire().await?; + + let course_instance_id = params.into_inner(); + let new_threshold = payload.0; + let duration: Option = None; + + models::suspected_cheaters::insert_thresholds( + &mut conn, + course_instance_id, + duration, + new_threshold.data.points, + ) + .await?; + + let token = authorize( + &mut conn, + Act::Edit, + Some(user.id), + Res::CourseInstance(course_instance_id), + ) + .await?; + token.authorized_ok(web::Json(())) +} + /** Add a route for each controller in this module. @@ -494,6 +529,10 @@ pub fn _add_routes(cfg: &mut ServiceConfig) { "/{course_instance_id}/progress/{user_id}", web::get().to(get_user_progress_for_course_instance), ) + .route( + "/{course_instance_id}/threshold", + web::post().to(insert_threshold), + ) .route( "/{course_instance_id}/reprocess-completions", web::post().to(post_reprocess_module_completions), diff --git a/services/headless-lms/server/src/controllers/main_frontend/suspected_cheaters.rs b/services/headless-lms/server/src/controllers/main_frontend/suspected_cheaters.rs new file mode 100644 index 000000000000..4870b62a69d7 --- /dev/null +++ b/services/headless-lms/server/src/controllers/main_frontend/suspected_cheaters.rs @@ -0,0 +1,223 @@ +//! Controllers for requests starting with `/api/v0/main-frontend/page_audio`. + +// use std::path::Path; + +// use futures::StreamExt; +// use models::page_audio_files::PageAudioFile; +// use models::suspected_cheaters::SuspectedCheaters; +// use models::suspected_cheaters::Threshold; +// use models::suspected_cheaters::SuspectedCheatersData; + +// use crate::prelude::*; + +/** +POST `/api/v0/main-frontend/suspected_cheater/threshold/:course_id` - Sets threshold for a course. + +# Example + +Request: +```http +POST /api/v0/main-frontend/suspected_cheater/threshold/65fd7f2c-09ae-52b2-806c-5649bdcb40e6 HTTP/1.1 +Content-Type: multipart/form-data + +BINARY_DATA +``` +*/ + +// #[instrument(skip(pool))] +// async fn insert_threshold( +// pool: web::Data, +// payload: web::Json, +// user: AuthUser, +// ) -> ControllerResult> { +// let mut conn = pool.acquire().await?; +// let new_threshold = payload.0; +// let points = new_threshold.data.points; +// let course_id = new_threshold.course_id; + +// let res = +// models::suspected_cheaters::insert_thresholds(&mut conn, course_id, new_threshold).await?; + +// let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Threshold).await?; +// token.authorized_ok(web::Json(res)) +// } + + +/** +POST `/api/v0/main-frontend/page_audio/:page_id` - Sets or updates the page audio. + +# Example + +Request: +```http +POST /api/v0/main-frontend/page_audio/d332f3d9-39a5-4a18-80f4-251727693c37 HTTP/1.1 +Content-Type: multipart/form-data + +BINARY_DATA +``` +*/ + +// #[instrument(skip(request, payload, pool, file_store))] +// async fn set_page_audio( +// request: HttpRequest, +// mut payload: Multipart, +// page_id: web::Path, +// pool: web::Data, +// user: AuthUser, +// file_store: web::Data, +// ) -> ControllerResult> { +// let mut conn = pool.acquire().await?; +// let page = models::pages::get_page(&mut conn, *page_id).await?; +// if let Some(course_id) = page.course_id { +// let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?; + +// let field = match payload.next().await { +// Some(Ok(field)) => field, +// Some(Err(error)) => { +// return Err(ControllerError::new( +// ControllerErrorType::InternalServerError, +// error.to_string(), +// None, +// )) +// } +// None => { +// return Err(ControllerError::new( +// ControllerErrorType::BadRequest, +// "Didn't upload any files".into(), +// None, +// )) +// } +// }; + +// let mime_type = field +// .content_type() +// .map(|ct| ct.to_string()) +// .unwrap_or_else(|| "".to_string()); +// /* +// if !matches!(mime_type.as_str(), "audio/mpeg" | "audio/ogg") { +// return Err(...) +// } +// */ +// match mime_type.as_str() { +// "audio/mpeg" | "audio/ogg" => {} +// unsupported => { +// return Err(ControllerError::new( +// ControllerErrorType::BadRequest, +// format!("Unsupported audio Mime type: {}", unsupported), +// None, +// )) +// } +// }; + +// let course = models::courses::get_course(&mut conn, page.course_id.unwrap()).await?; +// let media_path = upload_field_from_cms( +// request.headers(), +// field, +// StoreKind::Course(course.id), +// file_store.as_ref(), +// &mut conn, +// user, +// ) +// .await?; + +// models::page_audio_files::insert_page_audio( +// &mut conn, +// page.id, +// &media_path.as_path().to_string_lossy(), +// &mime_type, +// ) +// .await?; + +// token.authorized_ok(web::Json(true)) +// } else { +// Err(ControllerError::new( +// ControllerErrorType::BadRequest, +// "The page needs to be related to a course.".to_string(), +// None, +// )) +// } +// } + +/** +DELETE `/api/v0/main-frontend/page_audio/:file_id` - Removes the chapter image. + +# Example + +Request: +```http +DELETE /api/v0/main-frontend/page_audio/d332f3d9-39a5-4a18-80f4-251727693c37 HTTP/1.1 +``` +*/ + +// #[instrument(skip(pool, file_store))] +// async fn remove_page_audio( +// page_audio_id: web::Path, +// page_id: web::Path, +// pool: web::Data, +// user: AuthUser, +// file_store: web::Data, +// ) -> ControllerResult> { +// let mut conn = pool.acquire().await?; +// let audio = +// models::page_audio_files::get_page_audio_files_by_id(&mut conn, *page_audio_id).await?; +// let page = models::pages::get_page(&mut conn, audio.page_id).await?; +// if let Some(course_id) = page.course_id { +// let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?; + +// let path = models::page_audio_files::delete_page_audio(&mut conn, *page_audio_id).await?; +// file_store.delete(Path::new(&path)).await.map_err(|_| { +// ControllerError::new( +// ControllerErrorType::BadRequest, +// "Could not delete the file from the file store".to_string(), +// None, +// ) +// })?; +// token.authorized_ok(web::Json(())) +// } else { +// Err(ControllerError::new( +// ControllerErrorType::BadRequest, +// "The page needs to be related to a course.".to_string(), +// None, +// )) +// } +// } + +/** +GET `/api/v0/main-fronted/page_audio/:page_id/files` - Get a page audio files + +Request: `GET /api/v0/cms/page_audio/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02/files` +*/ +// #[instrument(skip(app_conf))] + +// async fn get_page_audio( +// page_id: web::Path, +// pool: web::Data, +// user: AuthUser, +// app_conf: web::Data, +// ) -> ControllerResult>> { +// let mut conn = pool.acquire().await?; +// let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?; + +// let mut page_audio_files = +// models::page_audio_files::get_page_audio_files(&mut conn, *page_id).await?; + +// let base_url = &app_conf.base_url; +// for audio in page_audio_files.iter_mut() { +// audio.path = format!("{base_url}/api/v0/files/{}", audio.path); +// } + +// token.authorized_ok(web::Json(page_audio_files)) +// } + +/** +Add a route for each controller in this module. + +The name starts with an underline in order to appear before other functions in the module documentation. + +We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation. +*/ +// pub fn _add_routes(cfg: &mut ServiceConfig) { +// cfg.route("/{course_instance_id}/threshold", web::post().to(insert_threshold))} +// .route("/{file_id}", web::delete().to(remove_page_audio)) +// .route("/{page_id}/files", web::get().to(get_page_audio)); +// } From 7a553e00e2dc784766ea7f968fea7580e5db9394 Mon Sep 17 00:00:00 2001 From: george-misan Date: Sun, 21 Apr 2024 16:19:17 +0300 Subject: [PATCH 21/34] [cheater-feature]: add files --- ...b3e3ae65e4005169b8ee1b20aa387d702ad58.json | 18 +++++++ ...7f95255bdebd8c8fdebbc02c8bcead9ba0da3.json | 12 +++++ ...88221698c5b12fd66650446720adbc95d1d3b.json | 12 +++++ ...a26c807a2994438e68b13886e1c555052121a.json | 12 +++++ ...7c8a3765ba5766f960c47b6b5e6cadec790b2.json | 48 +++++++++++++++++++ ...3bf2be073dc001ba654b67cf5ae4ac8ca34e0.json | 48 +++++++++++++++++++ .../models/src/user_exercise_states.rs | 14 +++--- 7 files changed, 157 insertions(+), 7 deletions(-) create mode 100644 services/headless-lms/models/.sqlx/query-4121f559b13cb44cb9245be0f5fb3e3ae65e4005169b8ee1b20aa387d702ad58.json create mode 100644 services/headless-lms/models/.sqlx/query-860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3.json create mode 100644 services/headless-lms/models/.sqlx/query-ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b.json create mode 100644 services/headless-lms/models/.sqlx/query-c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a.json create mode 100644 services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json create mode 100644 services/headless-lms/models/.sqlx/query-f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0.json diff --git a/services/headless-lms/models/.sqlx/query-4121f559b13cb44cb9245be0f5fb3e3ae65e4005169b8ee1b20aa387d702ad58.json b/services/headless-lms/models/.sqlx/query-4121f559b13cb44cb9245be0f5fb3e3ae65e4005169b8ee1b20aa387d702ad58.json new file mode 100644 index 000000000000..95e6520a408b --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-4121f559b13cb44cb9245be0f5fb3e3ae65e4005169b8ee1b20aa387d702ad58.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE suspected_cheaters\n SET deleted_at = now()\n WHERE id = $1\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false] + }, + "hash": "4121f559b13cb44cb9245be0f5fb3e3ae65e4005169b8ee1b20aa387d702ad58" +} diff --git a/services/headless-lms/models/.sqlx/query-860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3.json b/services/headless-lms/models/.sqlx/query-860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3.json new file mode 100644 index 000000000000..54fc2536f973 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO cheater_thresholds (\n course_id,\n duration,\n points\n )\n VALUES ($1, $2, $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Int4", "Int4"] + }, + "nullable": [] + }, + "hash": "860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3" +} diff --git a/services/headless-lms/models/.sqlx/query-ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b.json b/services/headless-lms/models/.sqlx/query-ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b.json new file mode 100644 index 000000000000..e1c8f9160245 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE cheater_thresholds\n SET points = $2\n WHERE course_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Int4"] + }, + "nullable": [] + }, + "hash": "ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b" +} diff --git a/services/headless-lms/models/.sqlx/query-c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a.json b/services/headless-lms/models/.sqlx/query-c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a.json new file mode 100644 index 000000000000..083ac470ab1c --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO suspected_cheaters (\n user_id,\n total_duration,\n total_points\n )\n VALUES ($1, $2, $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Int4", "Int4"] + }, + "nullable": [] + }, + "hash": "c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a" +} diff --git a/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json b/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json new file mode 100644 index 000000000000..960d3054018c --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT *\n FROM suspected_cheaters\n WHERE id = $1\n AND deleted_at IS NULL;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "user_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "total_duration", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "total_points", + "type_info": "Int4" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, false, false, false, true, false, false] + }, + "hash": "f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2" +} diff --git a/services/headless-lms/models/.sqlx/query-f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0.json b/services/headless-lms/models/.sqlx/query-f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0.json new file mode 100644 index 000000000000..2f90b1e781da --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT *\n FROM cheater_thresholds\n WHERE course_id = $1\n AND deleted_at IS NULL;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "course_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "points", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "duration", + "type_info": "Int4" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, false, false, false, true, false, false] + }, + "hash": "f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0" +} diff --git a/services/headless-lms/models/src/user_exercise_states.rs b/services/headless-lms/models/src/user_exercise_states.rs index 7453a97eb1d4..8d1264080dbf 100644 --- a/services/headless-lms/models/src/user_exercise_states.rs +++ b/services/headless-lms/models/src/user_exercise_states.rs @@ -598,7 +598,7 @@ WHERE user_id = $1 // let res = sqlx::query_as!( // UserExerciseState, // r#" -// SELECT +// SELECT // id, // user_id, // exercise_id, @@ -612,23 +612,23 @@ WHERE user_id = $1 // activity_progress AS "activity_progress: _", // reviewing_stage AS "reviewing_stage: _", // selected_exercise_slide_id -// FROM +// FROM // user_exercise_states ues // JOIN ( -// SELECT +// SELECT // exercise_id, // MAX(updated_at) AS max_updated_at, // COUNT(exercise.id) AS exercise_count -// FROM +// FROM // user_exercise_states -// WHERE +// WHERE // user_id = $1 // AND (course_instance_id = $2 OR exam_id = $3) // AND deleted_at IS NULL -// GROUP BY +// GROUP BY // exercise_id // ) max_dates ON ues.exercise_id = max_dates.exercise_id AND ues.updated_at = max_dates.max_updated_at -// WHERE +// WHERE // user_id = $1 // AND (course_instance_id = $2 OR exam_id = $3) // AND deleted_at IS NULL From c3c34205e0cb62571bc33eed8366653b4cf0a030 Mon Sep 17 00:00:00 2001 From: george-misan Date: Sun, 21 Apr 2024 17:03:48 +0300 Subject: [PATCH 22/34] [cheater-feature]: generate bindings --- services/headless-lms/models/src/suspected_cheaters.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/services/headless-lms/models/src/suspected_cheaters.rs b/services/headless-lms/models/src/suspected_cheaters.rs index f40fae3e8db7..e534c2afcb9c 100644 --- a/services/headless-lms/models/src/suspected_cheaters.rs +++ b/services/headless-lms/models/src/suspected_cheaters.rs @@ -13,6 +13,7 @@ pub struct SuspectedCheaters { } #[derive(Debug, Deserialize, Serialize)] +#[cfg_attr(feature = "ts_rs", derive(TS))] pub struct PointDurationData { pub points: i32, pub duration: i32, From d1a3240737d4978137b9f83c21b249b2364067e8 Mon Sep 17 00:00:00 2001 From: george-misan Date: Sun, 21 Apr 2024 22:53:45 +0300 Subject: [PATCH 23/34] [cheater-feature]: add suspected-cheaters route --- ...240312115600_add_suspected_cheaters.up.sql | 56 ++--- ...7f95255bdebd8c8fdebbc02c8bcead9ba0da3.json | 12 - ...88221698c5b12fd66650446720adbc95d1d3b.json | 12 - ...3bf2be073dc001ba654b67cf5ae4ac8ca34e0.json | 48 ---- .../models/src/suspected_cheaters.rs | 24 +- .../main_frontend/course_instances.rs | 56 ++++- .../main_frontend/suspected_cheaters.rs | 223 ------------------ 7 files changed, 82 insertions(+), 349 deletions(-) delete mode 100644 services/headless-lms/models/.sqlx/query-860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3.json delete mode 100644 services/headless-lms/models/.sqlx/query-ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b.json delete mode 100644 services/headless-lms/models/.sqlx/query-f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0.json delete mode 100644 services/headless-lms/server/src/controllers/main_frontend/suspected_cheaters.rs diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index e2d552023f22..7f5b4ca5c69e 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -7,12 +7,8 @@ CREATE TABLE suspected_cheaters ( total_duration INTEGER NOT NULL, total_points INTEGER NOT NULL ); - -CREATE TRIGGER set_timestamp -BEFORE UPDATE ON suspected_cheaters -FOR EACH ROW -EXECUTE PROCEDURE trigger_set_timestamp(); - +CREATE TRIGGER set_timestamp BEFORE +UPDATE ON suspected_cheaters FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE suspected_cheaters IS 'This table stores data regarding student that are suspected of cheating in a course.'; COMMENT ON COLUMN suspected_cheaters.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters.user_id IS 'The user_id of the student being suspected.'; @@ -21,30 +17,24 @@ COMMENT ON COLUMN suspected_cheaters.updated_at IS 'Timestamp when the record wa COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters.total_duration IS 'The total duration the student spend completing the course.'; COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points the student acquired in the course.'; - CREATE TABLE course_student_average ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - course_id UUID NOT NULL REFERENCES courses, + course_instance_id UUID NOT NULL REFERENCES course_instances, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, average_duration INTEGER NOT NULL, average_points INTEGER NOT NULL ); - -CREATE TRIGGER set_timestamp -BEFORE UPDATE ON course_student_average -FOR EACH ROW -EXECUTE PROCEDURE trigger_set_timestamp(); - +CREATE TRIGGER set_timestamp BEFORE +UPDATE ON course_student_average FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE course_student_average IS 'This table stores data regarding the average in a specific course.'; -COMMENT ON COLUMN course_student_average.course_id IS 'A unique, stable identifier for the record.'; +COMMENT ON COLUMN course_student_average.course_instance_id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN course_student_average.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN course_student_average.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN course_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN course_student_average.average_duration IS 'The average duration all student spent completing the course.'; COMMENT ON COLUMN course_student_average.average_points IS 'The average points all students acquired in the course.'; - -- CREATE TABLE suspected_cheaters_exercise_list ( @@ -53,18 +43,14 @@ CREATE TABLE suspected_cheaters_exercise_list ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - exercise_id UUID REFERENCES exercises NOT NULL , + exercise_id UUID REFERENCES exercises NOT NULL, duration INTEGER NOT NULL, points INTEGER NOT NULL, attempts INTEGER NOT NULL, status activity_progress NOT NULL DEFAULT 'initialized' ); - -CREATE TRIGGER set_timestamp -BEFORE UPDATE ON suspected_cheaters_exercise_list -FOR EACH ROW -EXECUTE PROCEDURE trigger_set_timestamp(); - +CREATE TRIGGER set_timestamp BEFORE +UPDATE ON suspected_cheaters_exercise_list FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE suspected_cheaters_exercise_list IS 'This table stores data regarding the list of exercises pertaining to students that have been suspected of cheating in a course.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.student_id IS 'The id of the student being suspected.'; @@ -76,7 +62,6 @@ COMMENT ON COLUMN suspected_cheaters_exercise_list.duration IS 'The duration a s COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspected student received from completing an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; - CREATE TABLE exercise_student_average ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, exercise_id UUID NOT NULL REFERENCES exercises, @@ -86,12 +71,8 @@ CREATE TABLE exercise_student_average ( average_duration INTEGER NOT NULL, average_points INTEGER NOT NULL ); - -CREATE TRIGGER set_timestamp -BEFORE UPDATE ON exercise_student_average -FOR EACH ROW -EXECUTE PROCEDURE trigger_set_timestamp(); - +CREATE TRIGGER set_timestamp BEFORE +UPDATE ON exercise_student_average FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE exercise_student_average IS 'This table stores data regarding the average in a specific course.'; COMMENT ON COLUMN exercise_student_average.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of the exercise.'; @@ -100,29 +81,22 @@ COMMENT ON COLUMN exercise_student_average.updated_at IS 'Timestamp when the rec COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN exercise_student_average.average_duration IS 'The average duration a all student used in completing an exercise.'; COMMENT ON COLUMN exercise_student_average.average_points IS 'The average points all student received from completing an exercise.'; - CREATE TABLE cheater_thresholds ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - course_id UUID NOT NULL REFERENCES courses, + course_instance_id UUID NOT NULL REFERENCES course_instances, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, points INTEGER NOT NULL, duration INTEGER NOT NULL ); - -CREATE TRIGGER set_timestamp -BEFORE UPDATE ON cheater_thresholds -FOR EACH ROW -EXECUTE PROCEDURE trigger_set_timestamp(); - +CREATE TRIGGER set_timestamp BEFORE +UPDATE ON cheater_thresholds FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); COMMENT ON TABLE cheater_thresholds IS 'This table stores threshold for measuring cheaters.'; COMMENT ON COLUMN cheater_thresholds.id IS 'A unique, stable identifier for the record.'; -COMMENT ON COLUMN cheater_thresholds.course_id IS 'The course_id of the course.'; +COMMENT ON COLUMN cheater_thresholds.course_instance_id IS 'The course_instance_id of the course.'; COMMENT ON COLUMN cheater_thresholds.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN cheater_thresholds.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN cheater_thresholds.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN cheater_thresholds.points IS 'The score threshold of the course.'; COMMENT ON COLUMN cheater_thresholds.duration IS 'The duration threshold of the course.'; - - diff --git a/services/headless-lms/models/.sqlx/query-860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3.json b/services/headless-lms/models/.sqlx/query-860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3.json deleted file mode 100644 index 54fc2536f973..000000000000 --- a/services/headless-lms/models/.sqlx/query-860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO cheater_thresholds (\n course_id,\n duration,\n points\n )\n VALUES ($1, $2, $3)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": ["Uuid", "Int4", "Int4"] - }, - "nullable": [] - }, - "hash": "860439330e36063b96d8b378f237f95255bdebd8c8fdebbc02c8bcead9ba0da3" -} diff --git a/services/headless-lms/models/.sqlx/query-ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b.json b/services/headless-lms/models/.sqlx/query-ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b.json deleted file mode 100644 index e1c8f9160245..000000000000 --- a/services/headless-lms/models/.sqlx/query-ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE cheater_thresholds\n SET points = $2\n WHERE course_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": ["Uuid", "Int4"] - }, - "nullable": [] - }, - "hash": "ae6210342e4faf2e12485fb145d88221698c5b12fd66650446720adbc95d1d3b" -} diff --git a/services/headless-lms/models/.sqlx/query-f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0.json b/services/headless-lms/models/.sqlx/query-f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0.json deleted file mode 100644 index 2f90b1e781da..000000000000 --- a/services/headless-lms/models/.sqlx/query-f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT *\n FROM cheater_thresholds\n WHERE course_id = $1\n AND deleted_at IS NULL;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "course_id", - "type_info": "Uuid" - }, - { - "ordinal": 2, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 3, - "name": "updated_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 4, - "name": "deleted_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 5, - "name": "points", - "type_info": "Int4" - }, - { - "ordinal": 6, - "name": "duration", - "type_info": "Int4" - } - ], - "parameters": { - "Left": ["Uuid"] - }, - "nullable": [false, false, false, false, true, false, false] - }, - "hash": "f9c1c406f3512b4851ed56408b03bf2be073dc001ba654b67cf5ae4ac8ca34e0" -} diff --git a/services/headless-lms/models/src/suspected_cheaters.rs b/services/headless-lms/models/src/suspected_cheaters.rs index e534c2afcb9c..544263862db1 100644 --- a/services/headless-lms/models/src/suspected_cheaters.rs +++ b/services/headless-lms/models/src/suspected_cheaters.rs @@ -22,7 +22,7 @@ pub struct PointDurationData { #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "ts_rs", derive(TS))] pub struct ThresholdData { - pub course_id: String, + pub course_instance_id: String, pub url: String, pub data: PointDurationData, } @@ -31,7 +31,7 @@ pub struct ThresholdData { #[cfg_attr(feature = "ts_rs", derive(TS))] pub struct Threshold { pub id: Uuid, - pub course_id: Uuid, + pub course_instance_id: Uuid, pub created_at: DateTime, pub updated_at: DateTime, pub deleted_at: Option>, @@ -42,7 +42,7 @@ pub struct Threshold { pub async fn insert( conn: &mut PgConnection, user_id: Uuid, - total_duration: i32, + total_duration: Option, total_points: i32, ) -> ModelResult<()> { sqlx::query!( @@ -65,20 +65,20 @@ pub async fn insert( pub async fn insert_thresholds( conn: &mut PgConnection, - course_id: Uuid, + course_instance_id: Uuid, duration: Option, points: i32, ) -> ModelResult<()> { sqlx::query!( " INSERT INTO cheater_thresholds ( - course_id, + course_instance_id, duration, points ) VALUES ($1, $2, $3) ", - course_id, + course_instance_id, duration, points ) @@ -89,16 +89,16 @@ pub async fn insert_thresholds( pub async fn update_thresholds_by_point( conn: &mut PgConnection, - course_id: Uuid, + course_instance_id: Uuid, points: i32, ) -> ModelResult<()> { sqlx::query!( " UPDATE cheater_thresholds SET points = $2 - WHERE course_id = $1 + WHERE course_instance_id = $1 ", - course_id, + course_instance_id, points ) .execute(conn) @@ -108,17 +108,17 @@ pub async fn update_thresholds_by_point( pub async fn get_thresholds_by_id( conn: &mut PgConnection, - course_id: Uuid, + course_instance_id: Uuid, ) -> ModelResult { let thresholds = sqlx::query_as!( Threshold, " SELECT * FROM cheater_thresholds - WHERE course_id = $1 + WHERE course_instance_id = $1 AND deleted_at IS NULL; ", - course_id + course_instance_id ) .fetch_one(conn) .await?; diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index 9de68a5fc45b..7c1793698035 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -444,7 +444,7 @@ async fn get_user_progress_for_course_instance( } /** - POST /api/v0/main-frontend/course-instance/:course_instance_id/threshold/:user_id - post course threshold information. + POST /api/v0/main-frontend/course-instances/:course_instance_id/threshold - post course threshold information. */ #[instrument(skip(pool))] async fn insert_threshold( @@ -477,6 +477,60 @@ async fn insert_threshold( token.authorized_ok(web::Json(())) } +/** + POST /api/v0/main-frontend/course-instances/:course_instance_id/suspected_cheaters - post course suspected cheaters information. +*/ +// #[instrument(skip(pool))] +// async fn insert_suspected_cheaters( +// pool: web::Data, +// params: web::Path, +// payload: web::Json, +// user: AuthUser, +// ) -> ControllerResult> { +// let mut conn = pool.acquire().await?; + +// let course_instance_id = params.into_inner(); + +// // Get average score +// let average_score = +// models::course_module_completions::get_course_average(&mut conn, course_instance_id) +// .await?; + +// // Get threshold +// let thresholds = +// models::suspected_cheaters::get_thresholds_by_id(&mut conn, course_instance_id).await?; + +// // Get all completions for the course instance +// let completions = models::course_module_completions::get_all_by_course_instance_id( +// &mut conn, +// course_instance_id, +// ) +// .await?; + +// // Iterate through completions, compare grades with average score and thresholds, and insert suspected cheaters +// for completion in completions { +// if completion.grade > average_score && completion.grade > thresholds?.points { +// // Insert suspected cheater +// models::suspected_cheaters::insert( +// &mut conn, +// completion.user_id, +// None, // Placeholder for duration, assuming it's optional +// completion.grade, +// ) +// .await?; +// } +// } + +// let token = authorize( +// &mut conn, +// Act::Edit, +// Some(user.id), +// Res::CourseInstance(course_instance_id), +// ) +// .await?; +// token.authorized_ok(web::Json(())) +// } + /** Add a route for each controller in this module. diff --git a/services/headless-lms/server/src/controllers/main_frontend/suspected_cheaters.rs b/services/headless-lms/server/src/controllers/main_frontend/suspected_cheaters.rs deleted file mode 100644 index 4870b62a69d7..000000000000 --- a/services/headless-lms/server/src/controllers/main_frontend/suspected_cheaters.rs +++ /dev/null @@ -1,223 +0,0 @@ -//! Controllers for requests starting with `/api/v0/main-frontend/page_audio`. - -// use std::path::Path; - -// use futures::StreamExt; -// use models::page_audio_files::PageAudioFile; -// use models::suspected_cheaters::SuspectedCheaters; -// use models::suspected_cheaters::Threshold; -// use models::suspected_cheaters::SuspectedCheatersData; - -// use crate::prelude::*; - -/** -POST `/api/v0/main-frontend/suspected_cheater/threshold/:course_id` - Sets threshold for a course. - -# Example - -Request: -```http -POST /api/v0/main-frontend/suspected_cheater/threshold/65fd7f2c-09ae-52b2-806c-5649bdcb40e6 HTTP/1.1 -Content-Type: multipart/form-data - -BINARY_DATA -``` -*/ - -// #[instrument(skip(pool))] -// async fn insert_threshold( -// pool: web::Data, -// payload: web::Json, -// user: AuthUser, -// ) -> ControllerResult> { -// let mut conn = pool.acquire().await?; -// let new_threshold = payload.0; -// let points = new_threshold.data.points; -// let course_id = new_threshold.course_id; - -// let res = -// models::suspected_cheaters::insert_thresholds(&mut conn, course_id, new_threshold).await?; - -// let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Threshold).await?; -// token.authorized_ok(web::Json(res)) -// } - - -/** -POST `/api/v0/main-frontend/page_audio/:page_id` - Sets or updates the page audio. - -# Example - -Request: -```http -POST /api/v0/main-frontend/page_audio/d332f3d9-39a5-4a18-80f4-251727693c37 HTTP/1.1 -Content-Type: multipart/form-data - -BINARY_DATA -``` -*/ - -// #[instrument(skip(request, payload, pool, file_store))] -// async fn set_page_audio( -// request: HttpRequest, -// mut payload: Multipart, -// page_id: web::Path, -// pool: web::Data, -// user: AuthUser, -// file_store: web::Data, -// ) -> ControllerResult> { -// let mut conn = pool.acquire().await?; -// let page = models::pages::get_page(&mut conn, *page_id).await?; -// if let Some(course_id) = page.course_id { -// let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?; - -// let field = match payload.next().await { -// Some(Ok(field)) => field, -// Some(Err(error)) => { -// return Err(ControllerError::new( -// ControllerErrorType::InternalServerError, -// error.to_string(), -// None, -// )) -// } -// None => { -// return Err(ControllerError::new( -// ControllerErrorType::BadRequest, -// "Didn't upload any files".into(), -// None, -// )) -// } -// }; - -// let mime_type = field -// .content_type() -// .map(|ct| ct.to_string()) -// .unwrap_or_else(|| "".to_string()); -// /* -// if !matches!(mime_type.as_str(), "audio/mpeg" | "audio/ogg") { -// return Err(...) -// } -// */ -// match mime_type.as_str() { -// "audio/mpeg" | "audio/ogg" => {} -// unsupported => { -// return Err(ControllerError::new( -// ControllerErrorType::BadRequest, -// format!("Unsupported audio Mime type: {}", unsupported), -// None, -// )) -// } -// }; - -// let course = models::courses::get_course(&mut conn, page.course_id.unwrap()).await?; -// let media_path = upload_field_from_cms( -// request.headers(), -// field, -// StoreKind::Course(course.id), -// file_store.as_ref(), -// &mut conn, -// user, -// ) -// .await?; - -// models::page_audio_files::insert_page_audio( -// &mut conn, -// page.id, -// &media_path.as_path().to_string_lossy(), -// &mime_type, -// ) -// .await?; - -// token.authorized_ok(web::Json(true)) -// } else { -// Err(ControllerError::new( -// ControllerErrorType::BadRequest, -// "The page needs to be related to a course.".to_string(), -// None, -// )) -// } -// } - -/** -DELETE `/api/v0/main-frontend/page_audio/:file_id` - Removes the chapter image. - -# Example - -Request: -```http -DELETE /api/v0/main-frontend/page_audio/d332f3d9-39a5-4a18-80f4-251727693c37 HTTP/1.1 -``` -*/ - -// #[instrument(skip(pool, file_store))] -// async fn remove_page_audio( -// page_audio_id: web::Path, -// page_id: web::Path, -// pool: web::Data, -// user: AuthUser, -// file_store: web::Data, -// ) -> ControllerResult> { -// let mut conn = pool.acquire().await?; -// let audio = -// models::page_audio_files::get_page_audio_files_by_id(&mut conn, *page_audio_id).await?; -// let page = models::pages::get_page(&mut conn, audio.page_id).await?; -// if let Some(course_id) = page.course_id { -// let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Course(course_id)).await?; - -// let path = models::page_audio_files::delete_page_audio(&mut conn, *page_audio_id).await?; -// file_store.delete(Path::new(&path)).await.map_err(|_| { -// ControllerError::new( -// ControllerErrorType::BadRequest, -// "Could not delete the file from the file store".to_string(), -// None, -// ) -// })?; -// token.authorized_ok(web::Json(())) -// } else { -// Err(ControllerError::new( -// ControllerErrorType::BadRequest, -// "The page needs to be related to a course.".to_string(), -// None, -// )) -// } -// } - -/** -GET `/api/v0/main-fronted/page_audio/:page_id/files` - Get a page audio files - -Request: `GET /api/v0/cms/page_audio/40ca9bcf-8eaa-41ba-940e-0fd5dd0c3c02/files` -*/ -// #[instrument(skip(app_conf))] - -// async fn get_page_audio( -// page_id: web::Path, -// pool: web::Data, -// user: AuthUser, -// app_conf: web::Data, -// ) -> ControllerResult>> { -// let mut conn = pool.acquire().await?; -// let token = authorize(&mut conn, Act::Edit, Some(user.id), Res::Page(*page_id)).await?; - -// let mut page_audio_files = -// models::page_audio_files::get_page_audio_files(&mut conn, *page_id).await?; - -// let base_url = &app_conf.base_url; -// for audio in page_audio_files.iter_mut() { -// audio.path = format!("{base_url}/api/v0/files/{}", audio.path); -// } - -// token.authorized_ok(web::Json(page_audio_files)) -// } - -/** -Add a route for each controller in this module. - -The name starts with an underline in order to appear before other functions in the module documentation. - -We add the routes by calling the route method instead of using the route annotations because this method preserves the function signatures for documentation. -*/ -// pub fn _add_routes(cfg: &mut ServiceConfig) { -// cfg.route("/{course_instance_id}/threshold", web::post().to(insert_threshold))} -// .route("/{file_id}", web::delete().to(remove_page_audio)) -// .route("/{page_id}/files", web::get().to(get_page_audio)); -// } From 9101911757621b998ae5bcc95c76c77fc1f9f738 Mon Sep 17 00:00:00 2001 From: george-misan Date: Wed, 24 Apr 2024 18:54:13 +0300 Subject: [PATCH 24/34] [cheater-feature]: update insert route for suspected-cheaters --- ...240312115600_add_suspected_cheaters.up.sql | 2 +- ...4ae1974f4616a6a34d589fd9c0cee9d240521.json | 12 ++ ...16a5c0b85411352d159a46b9ddaac92e5f7bc.json | 48 +++++++ ...0ff508732071d4b779cdedfa7fd9b772802b.json} | 4 +- ...ebb44b08ac5cb0f591f73a4a7914099772dab.json | 12 ++ ...7c8a3765ba5766f960c47b6b5e6cadec790b2.json | 2 +- .../models/src/course_module_completions.rs | 4 +- .../models/src/suspected_cheaters.rs | 14 +- .../main_frontend/course_instances.rs | 121 ++++++++++-------- 9 files changed, 147 insertions(+), 72 deletions(-) create mode 100644 services/headless-lms/models/.sqlx/query-75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521.json create mode 100644 services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json rename services/headless-lms/models/.sqlx/{query-16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393.json => query-895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b.json} (76%) create mode 100644 services/headless-lms/models/.sqlx/query-d293021412841c4be92eae951a0ebb44b08ac5cb0f591f73a4a7914099772dab.json diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 7f5b4ca5c69e..089dbead109b 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -4,7 +4,7 @@ CREATE TABLE suspected_cheaters ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - total_duration INTEGER NOT NULL, + total_duration INTEGER, total_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp BEFORE diff --git a/services/headless-lms/models/.sqlx/query-75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521.json b/services/headless-lms/models/.sqlx/query-75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521.json new file mode 100644 index 000000000000..a5c3372dfb01 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO cheater_thresholds (\n course_instance_id,\n duration,\n points\n )\n VALUES ($1, $2, $3)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Int4", "Int4"] + }, + "nullable": [] + }, + "hash": "75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521" +} diff --git a/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json b/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json new file mode 100644 index 000000000000..b43c36a9be36 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT *\n FROM cheater_thresholds\n WHERE course_instance_id = $1\n AND deleted_at IS NULL;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "course_instance_id", + "type_info": "Uuid" + }, + { + "ordinal": 2, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 3, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "deleted_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "points", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "duration", + "type_info": "Int4" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [false, false, false, false, true, false, false] + }, + "hash": "817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc" +} diff --git a/services/headless-lms/models/.sqlx/query-16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393.json b/services/headless-lms/models/.sqlx/query-895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b.json similarity index 76% rename from services/headless-lms/models/.sqlx/query-16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393.json rename to services/headless-lms/models/.sqlx/query-895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b.json index 94ed8e0b61a6..02270c1ad9c1 100644 --- a/services/headless-lms/models/.sqlx/query-16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393.json +++ b/services/headless-lms/models/.sqlx/query-895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT course_instance_id,\n SUM(grade)::integer AS total_points,\n AVG(grade)::REAL AS average_points,\n COUNT(DISTINCT user_id)::integer AS total_student\n FROM \n course_module_completions\n WHERE \n course_id = $1\n AND deleted_at IS NULL\n GROUP BY course_instance_id;\n ", + "query": "\n SELECT course_instance_id,\n SUM(grade)::integer AS total_points,\n AVG(grade)::REAL AS average_points,\n COUNT(DISTINCT user_id)::integer AS total_student\n FROM\n course_module_completions\n WHERE\n course_id = $1\n AND deleted_at IS NULL\n GROUP BY course_instance_id;\n ", "describe": { "columns": [ { @@ -29,5 +29,5 @@ }, "nullable": [false, null, null, null] }, - "hash": "16fa4464e024579f9b7ba18e8d76e9f531ef259a821556c742d5a60d622fe393" + "hash": "895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b" } diff --git a/services/headless-lms/models/.sqlx/query-d293021412841c4be92eae951a0ebb44b08ac5cb0f591f73a4a7914099772dab.json b/services/headless-lms/models/.sqlx/query-d293021412841c4be92eae951a0ebb44b08ac5cb0f591f73a4a7914099772dab.json new file mode 100644 index 000000000000..29c6654aee02 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-d293021412841c4be92eae951a0ebb44b08ac5cb0f591f73a4a7914099772dab.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE cheater_thresholds\n SET points = $2\n WHERE course_instance_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": ["Uuid", "Int4"] + }, + "nullable": [] + }, + "hash": "d293021412841c4be92eae951a0ebb44b08ac5cb0f591f73a4a7914099772dab" +} diff --git a/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json b/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json index 960d3054018c..87e211e526f2 100644 --- a/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json +++ b/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json @@ -42,7 +42,7 @@ "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, false, true, false, false] + "nullable": [false, false, false, false, true, true, false] }, "hash": "f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2" } diff --git a/services/headless-lms/models/src/course_module_completions.rs b/services/headless-lms/models/src/course_module_completions.rs index 344dd354f08d..a00154e03ae7 100644 --- a/services/headless-lms/models/src/course_module_completions.rs +++ b/services/headless-lms/models/src/course_module_completions.rs @@ -385,9 +385,9 @@ pub async fn get_course_average( SUM(grade)::integer AS total_points, AVG(grade)::REAL AS average_points, COUNT(DISTINCT user_id)::integer AS total_student - FROM + FROM course_module_completions - WHERE + WHERE course_id = $1 AND deleted_at IS NULL GROUP BY course_instance_id; diff --git a/services/headless-lms/models/src/suspected_cheaters.rs b/services/headless-lms/models/src/suspected_cheaters.rs index 544263862db1..c3e72af8848f 100644 --- a/services/headless-lms/models/src/suspected_cheaters.rs +++ b/services/headless-lms/models/src/suspected_cheaters.rs @@ -8,23 +8,15 @@ pub struct SuspectedCheaters { pub created_at: DateTime, pub deleted_at: Option>, pub updated_at: Option>, - pub total_duration: i32, // Represented in milliseconds + pub total_duration: Option, // Represented in milliseconds pub total_points: i32, } -#[derive(Debug, Deserialize, Serialize)] -#[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct PointDurationData { - pub points: i32, - pub duration: i32, -} - #[derive(Debug, Serialize, Deserialize)] #[cfg_attr(feature = "ts_rs", derive(TS))] pub struct ThresholdData { - pub course_instance_id: String, - pub url: String, - pub data: PointDurationData, + pub points: i32, + pub duration: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index 7c1793698035..54ed57035f87 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -459,14 +459,79 @@ async fn insert_threshold( let new_threshold = payload.0; let duration: Option = None; + let token = authorize( + &mut conn, + Act::Edit, + Some(user.id), + Res::CourseInstance(course_instance_id), + ) + .await?; + models::suspected_cheaters::insert_thresholds( &mut conn, course_instance_id, duration, - new_threshold.data.points, + new_threshold.points, ) .await?; + token.authorized_ok(web::Json(())) +} + +/** + POST /api/v0/main-frontend/course-instances/:course_instance_id/suspected_cheaters - post course suspected cheaters information. +*/ +#[instrument(skip(pool))] +async fn insert_suspected_cheaters( + pool: web::Data, + params: web::Path, + payload: web::Json, + user: AuthUser, +) -> ControllerResult> { + let mut conn = pool.acquire().await?; + + let course_instance_id = params.into_inner(); + + // Get average score + let course_average = + models::course_module_completions::get_course_average(&mut conn, course_instance_id) + .await?; + + // Get threshold + let thresholds = + models::suspected_cheaters::get_thresholds_by_id(&mut conn, course_instance_id).await?; + + // Get all completions for the course instance + let completions = models::course_module_completions::get_all_by_course_instance_id( + &mut conn, + course_instance_id, + ) + .await?; + + let average_points = course_average.average_points.unwrap_or(0.0); + + // Iterate through completions, compare grades with average score and thresholds, and insert suspected cheaters + for completion in completions { + if let Some(grade) = completion.grade { + if grade > average_points as i32 && grade > thresholds.points { + // Insert suspected cheater + models::suspected_cheaters::insert( + &mut conn, + completion.user_id, + None, // Placeholder for duration, assuming it's optional + grade, + ) + .await?; + } + } else { + return Err(ControllerError::new( + ControllerErrorType::BadRequest, + "Grade is not a numeric value".to_string(), + None, + )); + } + } + let token = authorize( &mut conn, Act::Edit, @@ -477,60 +542,6 @@ async fn insert_threshold( token.authorized_ok(web::Json(())) } -/** - POST /api/v0/main-frontend/course-instances/:course_instance_id/suspected_cheaters - post course suspected cheaters information. -*/ -// #[instrument(skip(pool))] -// async fn insert_suspected_cheaters( -// pool: web::Data, -// params: web::Path, -// payload: web::Json, -// user: AuthUser, -// ) -> ControllerResult> { -// let mut conn = pool.acquire().await?; - -// let course_instance_id = params.into_inner(); - -// // Get average score -// let average_score = -// models::course_module_completions::get_course_average(&mut conn, course_instance_id) -// .await?; - -// // Get threshold -// let thresholds = -// models::suspected_cheaters::get_thresholds_by_id(&mut conn, course_instance_id).await?; - -// // Get all completions for the course instance -// let completions = models::course_module_completions::get_all_by_course_instance_id( -// &mut conn, -// course_instance_id, -// ) -// .await?; - -// // Iterate through completions, compare grades with average score and thresholds, and insert suspected cheaters -// for completion in completions { -// if completion.grade > average_score && completion.grade > thresholds?.points { -// // Insert suspected cheater -// models::suspected_cheaters::insert( -// &mut conn, -// completion.user_id, -// None, // Placeholder for duration, assuming it's optional -// completion.grade, -// ) -// .await?; -// } -// } - -// let token = authorize( -// &mut conn, -// Act::Edit, -// Some(user.id), -// Res::CourseInstance(course_instance_id), -// ) -// .await?; -// token.authorized_ok(web::Json(())) -// } - /** Add a route for each controller in this module. From 72ef938d2c25e4ad42e3b8820a49dfafb0118e84 Mon Sep 17 00:00:00 2001 From: george-misan Date: Wed, 24 Apr 2024 19:07:47 +0300 Subject: [PATCH 25/34] [migration-job]: run migration --- .../20240312115600_add_suspected_cheaters.up.sql | 8 ++++---- ...802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 089dbead109b..1357a09e12aa 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -23,7 +23,7 @@ CREATE TABLE course_student_average ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - average_duration INTEGER NOT NULL, + average_duration INTEGER, average_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp BEFORE @@ -44,7 +44,7 @@ CREATE TABLE suspected_cheaters_exercise_list ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, exercise_id UUID REFERENCES exercises NOT NULL, - duration INTEGER NOT NULL, + duration INTEGER, points INTEGER NOT NULL, attempts INTEGER NOT NULL, status activity_progress NOT NULL DEFAULT 'initialized' @@ -68,7 +68,7 @@ CREATE TABLE exercise_student_average ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - average_duration INTEGER NOT NULL, + average_duration INTEGER, average_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp BEFORE @@ -88,7 +88,7 @@ CREATE TABLE cheater_thresholds ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, points INTEGER NOT NULL, - duration INTEGER NOT NULL + duration INTEGER ); CREATE TRIGGER set_timestamp BEFORE UPDATE ON cheater_thresholds FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); diff --git a/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json b/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json index b43c36a9be36..04f78b12271a 100644 --- a/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json +++ b/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json @@ -42,7 +42,7 @@ "parameters": { "Left": ["Uuid"] }, - "nullable": [false, false, false, false, true, false, false] + "nullable": [false, false, false, false, true, false, true] }, "hash": "817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc" } From 82d5b2525788b2509cce788c81bacde91f4573b0 Mon Sep 17 00:00:00 2001 From: george-misan Date: Thu, 25 Apr 2024 11:04:34 +0300 Subject: [PATCH 26/34] [cleanup]: update and test suspected -cheaters route --- ...40480cf6490b12aab24d085e62cd45fd36a78a.json} | 4 ++-- .../models/src/course_module_completions.rs | 2 +- .../main_frontend/course_instances.rs | 17 ++++++++--------- 3 files changed, 11 insertions(+), 12 deletions(-) rename services/headless-lms/models/.sqlx/{query-895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b.json => query-60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a.json} (81%) diff --git a/services/headless-lms/models/.sqlx/query-895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b.json b/services/headless-lms/models/.sqlx/query-60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a.json similarity index 81% rename from services/headless-lms/models/.sqlx/query-895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b.json rename to services/headless-lms/models/.sqlx/query-60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a.json index 02270c1ad9c1..9be3edf05493 100644 --- a/services/headless-lms/models/.sqlx/query-895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b.json +++ b/services/headless-lms/models/.sqlx/query-60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT course_instance_id,\n SUM(grade)::integer AS total_points,\n AVG(grade)::REAL AS average_points,\n COUNT(DISTINCT user_id)::integer AS total_student\n FROM\n course_module_completions\n WHERE\n course_id = $1\n AND deleted_at IS NULL\n GROUP BY course_instance_id;\n ", + "query": "\n SELECT course_instance_id,\n SUM(grade)::integer AS total_points,\n AVG(grade)::REAL AS average_points,\n COUNT(DISTINCT user_id)::integer AS total_student\n FROM\n course_module_completions\n WHERE\n course_instance_id = $1\n AND deleted_at IS NULL\n GROUP BY course_instance_id;\n ", "describe": { "columns": [ { @@ -29,5 +29,5 @@ }, "nullable": [false, null, null, null] }, - "hash": "895b2dfdee0ed62de79454bd7fe50ff508732071d4b779cdedfa7fd9b772802b" + "hash": "60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a" } diff --git a/services/headless-lms/models/src/course_module_completions.rs b/services/headless-lms/models/src/course_module_completions.rs index a00154e03ae7..40b6d32db5bf 100644 --- a/services/headless-lms/models/src/course_module_completions.rs +++ b/services/headless-lms/models/src/course_module_completions.rs @@ -388,7 +388,7 @@ pub async fn get_course_average( FROM course_module_completions WHERE - course_id = $1 + course_instance_id = $1 AND deleted_at IS NULL GROUP BY course_instance_id; "#, diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index 54ed57035f87..5d79e04e9179 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -485,7 +485,7 @@ async fn insert_threshold( async fn insert_suspected_cheaters( pool: web::Data, params: web::Path, - payload: web::Json, + // payload: web::Json, user: AuthUser, ) -> ControllerResult> { let mut conn = pool.acquire().await?; @@ -513,15 +513,10 @@ async fn insert_suspected_cheaters( // Iterate through completions, compare grades with average score and thresholds, and insert suspected cheaters for completion in completions { if let Some(grade) = completion.grade { - if grade > average_points as i32 && grade > thresholds.points { + if grade > thresholds.points || grade > average_points as i32 { // Insert suspected cheater - models::suspected_cheaters::insert( - &mut conn, - completion.user_id, - None, // Placeholder for duration, assuming it's optional - grade, - ) - .await?; + models::suspected_cheaters::insert(&mut conn, completion.user_id, None, grade) + .await?; } } else { return Err(ControllerError::new( @@ -598,6 +593,10 @@ pub fn _add_routes(cfg: &mut ServiceConfig) { "/{course_instance_id}/threshold", web::post().to(insert_threshold), ) + .route( + "/{course_instance_id}/suspected-cheaters", + web::post().to(insert_suspected_cheaters), + ) .route( "/{course_instance_id}/reprocess-completions", web::post().to(post_reprocess_module_completions), From ef89e6c8961f6d7820a921cc5963560f742a6d2a Mon Sep 17 00:00:00 2001 From: george-misan Date: Fri, 26 Apr 2024 09:47:29 +0300 Subject: [PATCH 27/34] [cheater-feature]: update suspected-cheaters route to use average duration as a metric --- ...0480cf6490b12aab24d085e62cd45fd36a78a.json | 33 ----------- .../models/src/course_instances.rs | 54 ++++++++++++++++++ .../models/src/course_module_completions.rs | 26 --------- .../models/src/user_exercise_states.rs | 55 ------------------- .../main_frontend/course_instances.rs | 43 +++++++++++---- 5 files changed, 87 insertions(+), 124 deletions(-) delete mode 100644 services/headless-lms/models/.sqlx/query-60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a.json diff --git a/services/headless-lms/models/.sqlx/query-60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a.json b/services/headless-lms/models/.sqlx/query-60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a.json deleted file mode 100644 index 9be3edf05493..000000000000 --- a/services/headless-lms/models/.sqlx/query-60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n SELECT course_instance_id,\n SUM(grade)::integer AS total_points,\n AVG(grade)::REAL AS average_points,\n COUNT(DISTINCT user_id)::integer AS total_student\n FROM\n course_module_completions\n WHERE\n course_instance_id = $1\n AND deleted_at IS NULL\n GROUP BY course_instance_id;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "course_instance_id", - "type_info": "Uuid" - }, - { - "ordinal": 1, - "name": "total_points", - "type_info": "Int4" - }, - { - "ordinal": 2, - "name": "average_points", - "type_info": "Float4" - }, - { - "ordinal": 3, - "name": "total_student", - "type_info": "Int4" - } - ], - "parameters": { - "Left": ["Uuid"] - }, - "nullable": [false, null, null, null] - }, - "hash": "60debf52c4ef7892dd78863fb340480cf6490b12aab24d085e62cd45fd36a78a" -} diff --git a/services/headless-lms/models/src/course_instances.rs b/services/headless-lms/models/src/course_instances.rs index 3491ca8ba40b..6907cb7679f0 100644 --- a/services/headless-lms/models/src/course_instances.rs +++ b/services/headless-lms/models/src/course_instances.rs @@ -26,6 +26,12 @@ pub struct CourseInstance { pub support_email: Option, } +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[cfg_attr(feature = "ts_rs", derive(TS))] +pub struct CourseAverageDuration { + pub average_duration: Option, +} + impl CourseInstance { pub fn is_open(&self) -> bool { self.starts_at.map(|sa| sa < Utc::now()).unwrap_or_default() @@ -746,6 +752,54 @@ WHERE user_id = $1 Ok(()) } +pub async fn get_course_average_duration( + conn: &mut PgConnection, + course_instance_id: Uuid, +) -> ModelResult { + let res = sqlx::query_as!( + CourseAverageDuration, + r#" +SELECT + AVG(EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at))::int8 AS average_duration +FROM course_instance_enrollments ce +JOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id +WHERE ce.course_instance_id = $1 + AND ce.deleted_at IS NULL + AND cmc.deleted_at IS NULL; + "#, + course_instance_id + ) + .fetch_one(conn) + .await?; + Ok(res) +} + +pub async fn get_student_duration( + conn: &mut PgConnection, + user_id: Uuid, + course_instance_id: Uuid, +) -> ModelResult { + let res = sqlx::query_as!( + CourseAverageDuration, + r#" +SELECT + EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at)::int8 AS average_duration +FROM course_instance_enrollments ce +JOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id +AND cmc.user_id = ce.user_id +WHERE ce.course_instance_id = $1 + AND ce.user_id = $2 + AND ce.deleted_at IS NULL + AND cmc.deleted_at IS NULL; + "#, + course_instance_id, + user_id + ) + .fetch_one(conn) + .await?; + Ok(res) +} + #[cfg(test)] mod test { use super::*; diff --git a/services/headless-lms/models/src/course_module_completions.rs b/services/headless-lms/models/src/course_module_completions.rs index 40b6d32db5bf..feac101331b1 100644 --- a/services/headless-lms/models/src/course_module_completions.rs +++ b/services/headless-lms/models/src/course_module_completions.rs @@ -373,32 +373,6 @@ WHERE course_id = $1 Ok(res.count.unwrap_or(0)) } -// Get the student average in the course -pub async fn get_course_average( - conn: &mut PgConnection, - course_instance_id: Uuid, -) -> ModelResult { - let res = sqlx::query_as!( - CourseModulePointsAverage, - r#" - SELECT course_instance_id, - SUM(grade)::integer AS total_points, - AVG(grade)::REAL AS average_points, - COUNT(DISTINCT user_id)::integer AS total_student - FROM - course_module_completions - WHERE - course_instance_id = $1 - AND deleted_at IS NULL - GROUP BY course_instance_id; - "#, - course_instance_id - ) - .fetch_one(conn) - .await?; - Ok(res) -} - /// Gets automatically granted course module completion for the given user on the specified course /// instance. This entry is quaranteed to be unique in database by the index /// `course_module_automatic_completion_uniqueness`. diff --git a/services/headless-lms/models/src/user_exercise_states.rs b/services/headless-lms/models/src/user_exercise_states.rs index 8d1264080dbf..f8e116683375 100644 --- a/services/headless-lms/models/src/user_exercise_states.rs +++ b/services/headless-lms/models/src/user_exercise_states.rs @@ -587,61 +587,6 @@ WHERE user_id = $1 Ok(res) } -// TODO: why is map working here instead of just returning the value - -// pub async fn get_user_exercise_state_by_user_id( -// conn: &mut PgConnection, -// user_id: Uuid, -// course_instance_or_exam_id: CourseInstanceOrExamId, -// ) -> ModelResult< { -// let (course_instance_id, exam_id) = course_instance_or_exam_id.to_instance_and_exam_ids(); -// let res = sqlx::query_as!( -// UserExerciseState, -// r#" -// SELECT -// id, -// user_id, -// exercise_id, -// course_instance_id, -// exam_id, -// created_at, -// updated_at, -// deleted_at, -// score_given, -// grading_progress AS "grading_progress: _", -// activity_progress AS "activity_progress: _", -// reviewing_stage AS "reviewing_stage: _", -// selected_exercise_slide_id -// FROM -// user_exercise_states ues -// JOIN ( -// SELECT -// exercise_id, -// MAX(updated_at) AS max_updated_at, -// COUNT(exercise.id) AS exercise_count -// FROM -// user_exercise_states -// WHERE -// user_id = $1 -// AND (course_instance_id = $2 OR exam_id = $3) -// AND deleted_at IS NULL -// GROUP BY -// exercise_id -// ) max_dates ON ues.exercise_id = max_dates.exercise_id AND ues.updated_at = max_dates.max_updated_at -// WHERE -// user_id = $1 -// AND (course_instance_id = $2 OR exam_id = $3) -// AND deleted_at IS NULL -// "#, -// user_id, -// course_instance_id, -// exam_id -// ) -// .fetch_all(conn) -// .await?; -// Ok(res) -// } - pub async fn get_users_current_by_exercise( conn: &mut PgConnection, user_id: Uuid, diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index 5d79e04e9179..028d64d29d2f 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -492,11 +492,6 @@ async fn insert_suspected_cheaters( let course_instance_id = params.into_inner(); - // Get average score - let course_average = - models::course_module_completions::get_course_average(&mut conn, course_instance_id) - .await?; - // Get threshold let thresholds = models::suspected_cheaters::get_thresholds_by_id(&mut conn, course_instance_id).await?; @@ -508,15 +503,43 @@ async fn insert_suspected_cheaters( ) .await?; - let average_points = course_average.average_points.unwrap_or(0.0); + // all do this for a single student and compare + + let average_duration = + models::course_instances::get_course_average_duration(&mut conn, course_instance_id) + .await?; + + // let average_points = course_average.average_points.unwrap_or(0.0); // Iterate through completions, compare grades with average score and thresholds, and insert suspected cheaters for completion in completions { - if let Some(grade) = completion.grade { - if grade > thresholds.points || grade > average_points as i32 { + if let Some(_grade) = completion.grade { + let total_points = models::user_exercise_states::get_user_total_course_points( + &mut conn, + user.id, + course_instance_id, + ) + .await? + .unwrap_or(0.0); + + let student_duration = models::course_instances::get_student_duration( + &mut conn, + completion.user_id, + course_instance_id, + ) + .await?; + + if total_points as i32 > thresholds.points + && student_duration.average_duration > average_duration.average_duration + { // Insert suspected cheater - models::suspected_cheaters::insert(&mut conn, completion.user_id, None, grade) - .await?; + models::suspected_cheaters::insert( + &mut conn, + completion.user_id, + None, + total_points as i32, + ) + .await?; } } else { return Err(ControllerError::new( From 7774e5527b33bf33e3f27baf0383362cd12f1a7e Mon Sep 17 00:00:00 2001 From: george-misan Date: Fri, 26 Apr 2024 10:46:29 +0300 Subject: [PATCH 28/34] [cheater-feature]: update suspected-cheaters route to use average duration as a metric --- ...2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json | 18 ++++ ...173a08e17b2041fe3e2973df81bc488e3da71.json | 18 ++++ .../main_frontend/course_instances.rs | 99 +++++++++++++------ 3 files changed, 106 insertions(+), 29 deletions(-) create mode 100644 services/headless-lms/models/.sqlx/query-54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json create mode 100644 services/headless-lms/models/.sqlx/query-62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71.json diff --git a/services/headless-lms/models/.sqlx/query-54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json b/services/headless-lms/models/.sqlx/query-54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json new file mode 100644 index 000000000000..bfc03a23b640 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT\n EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at)::int8 AS average_duration\nFROM course_instance_enrollments ce\nJOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id\nAND cmc.user_id = ce.user_id\nWHERE ce.course_instance_id = $1\n AND ce.user_id = $2\n AND ce.deleted_at IS NULL\n AND cmc.deleted_at IS NULL;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "average_duration", + "type_info": "Int8" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [null] + }, + "hash": "54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2" +} diff --git a/services/headless-lms/models/.sqlx/query-62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71.json b/services/headless-lms/models/.sqlx/query-62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71.json new file mode 100644 index 000000000000..fd188c0a2f35 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT\n AVG(EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at))::int8 AS average_duration\nFROM course_instance_enrollments ce\nJOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id\nWHERE ce.course_instance_id = $1\n AND ce.deleted_at IS NULL\n AND cmc.deleted_at IS NULL;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "average_duration", + "type_info": "Int8" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [null] + }, + "hash": "62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71" +} diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index 028d64d29d2f..309e3424569e 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -512,42 +512,83 @@ async fn insert_suspected_cheaters( // let average_points = course_average.average_points.unwrap_or(0.0); // Iterate through completions, compare grades with average score and thresholds, and insert suspected cheaters + // for completion in completions { + // if let Some(_grade) = completion.grade { + // let total_points = models::user_exercise_states::get_user_total_course_points( + // &mut conn, + // user.id, + // course_instance_id, + // ) + // .await? + // .unwrap_or(0.0); + + // let student_duration = models::course_instances::get_student_duration( + // &mut conn, + // completion.user_id, + // course_instance_id, + // ) + // .await?; + + // if total_points as i32 > thresholds.points + // && student_duration.average_duration > average_duration.average_duration + // { + // // Insert suspected cheater + // models::suspected_cheaters::insert( + // &mut conn, + // completion.user_id, + // None, + // total_points as i32, + // ) + // .await?; + // } + // } else { + // return Err(ControllerError::new( + // ControllerErrorType::BadRequest, + // "Grade is not a numeric value".to_string(), + // None, + // )); + // } + // } for completion in completions { - if let Some(_grade) = completion.grade { - let total_points = models::user_exercise_states::get_user_total_course_points( - &mut conn, - user.id, - course_instance_id, - ) - .await? - .unwrap_or(0.0); - - let student_duration = models::course_instances::get_student_duration( - &mut conn, - completion.user_id, - course_instance_id, - ) - .await?; - - if total_points as i32 > thresholds.points - && student_duration.average_duration > average_duration.average_duration - { - // Insert suspected cheater - models::suspected_cheaters::insert( - &mut conn, - completion.user_id, - None, - total_points as i32, - ) - .await?; - } - } else { + if completion.grade.is_none() { return Err(ControllerError::new( ControllerErrorType::BadRequest, "Grade is not a numeric value".to_string(), None, )); } + + let total_points = models::user_exercise_states::get_user_total_course_points( + &mut conn, + user.id, + course_instance_id, + ) + .await? + .unwrap_or(0.0); + + let student_duration = models::course_instances::get_student_duration( + &mut conn, + completion.user_id, + course_instance_id, + ) + .await?; + + if total_points as i32 <= thresholds.points { + continue; + } + + let completion_average_duration = student_duration.average_duration; + let average_duration_value = average_duration.average_duration; + + if completion_average_duration > average_duration_value { + models::suspected_cheaters::insert( + &mut conn, + completion.user_id, + None, + total_points as i32, + ) + .await?; + } } let token = authorize( From 0faa462f01e744819320ff8f00beaf22b538d408 Mon Sep 17 00:00:00 2001 From: george-misan Date: Fri, 26 Apr 2024 13:01:00 +0300 Subject: [PATCH 29/34] [cheater-feature]: update suspected-cheaters route to use average duration as a metric --- ...240312115600_add_suspected_cheaters.up.sql | 20 +++++----- ...2908170cb898da785bf97730456fbd4007f74.json | 18 +++++++++ ...2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json | 18 --------- ...173a08e17b2041fe3e2973df81bc488e3da71.json | 18 --------- ...16a5c0b85411352d159a46b9ddaac92e5f7bc.json | 2 +- ...24f54d206fee3ecaef449808df6239abe6fa.json} | 4 +- ...335858425e23c56115c28ecb3ef7e51c5169f.json | 18 +++++++++ ...0ae10b6e85f445179051dbabb8dcacdc3c73.json} | 4 +- ...7c8a3765ba5766f960c47b6b5e6cadec790b2.json | 2 +- .../models/src/course_instances.rs | 38 +++++++++---------- .../models/src/suspected_cheaters.rs | 18 ++++----- .../main_frontend/course_instances.rs | 9 ++--- 12 files changed, 83 insertions(+), 86 deletions(-) create mode 100644 services/headless-lms/models/.sqlx/query-3eeaf95c1f900086e5d592a8abb2908170cb898da785bf97730456fbd4007f74.json delete mode 100644 services/headless-lms/models/.sqlx/query-54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json delete mode 100644 services/headless-lms/models/.sqlx/query-62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71.json rename services/headless-lms/models/.sqlx/{query-75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521.json => query-9840145a8c388e39c73aa7d7763124f54d206fee3ecaef449808df6239abe6fa.json} (56%) create mode 100644 services/headless-lms/models/.sqlx/query-a2683028ae0a7755641284b8156335858425e23c56115c28ecb3ef7e51c5169f.json rename services/headless-lms/models/.sqlx/{query-c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a.json => query-c2f2aa72ce142e7784325d9127d70ae10b6e85f445179051dbabb8dcacdc3c73.json} (55%) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index 1357a09e12aa..aa36844346d4 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -4,7 +4,7 @@ CREATE TABLE suspected_cheaters ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - total_duration INTEGER, + total_duration_seconds INTEGER, total_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp BEFORE @@ -15,7 +15,7 @@ COMMENT ON COLUMN suspected_cheaters.user_id IS 'The user_id of the student bein COMMENT ON COLUMN suspected_cheaters.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN suspected_cheaters.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN suspected_cheaters.total_duration IS 'The total duration the student spend completing the course.'; +COMMENT ON COLUMN suspected_cheaters.total_duration_seconds IS 'The total duration the student spend completing the course.'; COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points the student acquired in the course.'; CREATE TABLE course_student_average ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, @@ -23,7 +23,7 @@ CREATE TABLE course_student_average ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - average_duration INTEGER, + average_duration_seconds INTEGER, average_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp BEFORE @@ -33,7 +33,7 @@ COMMENT ON COLUMN course_student_average.course_instance_id IS 'A unique, stable COMMENT ON COLUMN course_student_average.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN course_student_average.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN course_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN course_student_average.average_duration IS 'The average duration all student spent completing the course.'; +COMMENT ON COLUMN course_student_average.average_duration_seconds IS 'The average duration all student spent completing the course.'; COMMENT ON COLUMN course_student_average.average_points IS 'The average points all students acquired in the course.'; -- @@ -44,7 +44,7 @@ CREATE TABLE suspected_cheaters_exercise_list ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, exercise_id UUID REFERENCES exercises NOT NULL, - duration INTEGER, + duration_seconds INTEGER, points INTEGER NOT NULL, attempts INTEGER NOT NULL, status activity_progress NOT NULL DEFAULT 'initialized' @@ -58,7 +58,7 @@ COMMENT ON COLUMN suspected_cheaters_exercise_list.updated_at IS 'Timestamp when COMMENT ON COLUMN suspected_cheaters_exercise_list.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.exercise_id IS 'Exercise Id of an exercise completed by the suspected student.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.duration IS 'The duration a suspected student used in completing an exercise.'; +COMMENT ON COLUMN suspected_cheaters_exercise_list.duration_seconds IS 'The duration a suspected student used in completing an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspected student received from completing an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; @@ -68,7 +68,7 @@ CREATE TABLE exercise_student_average ( created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, - average_duration INTEGER, + average_duration_seconds INTEGER, average_points INTEGER NOT NULL ); CREATE TRIGGER set_timestamp BEFORE @@ -79,7 +79,7 @@ COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of th COMMENT ON COLUMN exercise_student_average.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN exercise_student_average.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN exercise_student_average.average_duration IS 'The average duration a all student used in completing an exercise.'; +COMMENT ON COLUMN exercise_student_average.average_duration_seconds IS 'The average duration a all student used in completing an exercise.'; COMMENT ON COLUMN exercise_student_average.average_points IS 'The average points all student received from completing an exercise.'; CREATE TABLE cheater_thresholds ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, @@ -88,7 +88,7 @@ CREATE TABLE cheater_thresholds ( updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), deleted_at TIMESTAMP WITH TIME ZONE, points INTEGER NOT NULL, - duration INTEGER + duration_seconds INTEGER ); CREATE TRIGGER set_timestamp BEFORE UPDATE ON cheater_thresholds FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); @@ -99,4 +99,4 @@ COMMENT ON COLUMN cheater_thresholds.created_at IS 'Timestamp when the record wa COMMENT ON COLUMN cheater_thresholds.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN cheater_thresholds.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN cheater_thresholds.points IS 'The score threshold of the course.'; -COMMENT ON COLUMN cheater_thresholds.duration IS 'The duration threshold of the course.'; +COMMENT ON COLUMN cheater_thresholds.duration_seconds IS 'The duration threshold of the course.'; diff --git a/services/headless-lms/models/.sqlx/query-3eeaf95c1f900086e5d592a8abb2908170cb898da785bf97730456fbd4007f74.json b/services/headless-lms/models/.sqlx/query-3eeaf95c1f900086e5d592a8abb2908170cb898da785bf97730456fbd4007f74.json new file mode 100644 index 000000000000..6bcdf6e6b5e0 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-3eeaf95c1f900086e5d592a8abb2908170cb898da785bf97730456fbd4007f74.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT\n COALESCE(EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at)::int8, 0) AS student_duration_seconds\nFROM course_instance_enrollments ce\nJOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id\nAND cmc.user_id = ce.user_id\nWHERE ce.course_instance_id = $1\n AND ce.user_id = $2\n AND ce.deleted_at IS NULL\n AND cmc.deleted_at IS NULL;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "student_duration_seconds", + "type_info": "Int8" + } + ], + "parameters": { + "Left": ["Uuid", "Uuid"] + }, + "nullable": [null] + }, + "hash": "3eeaf95c1f900086e5d592a8abb2908170cb898da785bf97730456fbd4007f74" +} diff --git a/services/headless-lms/models/.sqlx/query-54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json b/services/headless-lms/models/.sqlx/query-54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json deleted file mode 100644 index bfc03a23b640..000000000000 --- a/services/headless-lms/models/.sqlx/query-54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nSELECT\n EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at)::int8 AS average_duration\nFROM course_instance_enrollments ce\nJOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id\nAND cmc.user_id = ce.user_id\nWHERE ce.course_instance_id = $1\n AND ce.user_id = $2\n AND ce.deleted_at IS NULL\n AND cmc.deleted_at IS NULL;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "average_duration", - "type_info": "Int8" - } - ], - "parameters": { - "Left": ["Uuid", "Uuid"] - }, - "nullable": [null] - }, - "hash": "54d031df6e8591ec062ebe741fd2f45aa2426eca46df1ab3a7e4d83ff5ddafe2" -} diff --git a/services/headless-lms/models/.sqlx/query-62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71.json b/services/headless-lms/models/.sqlx/query-62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71.json deleted file mode 100644 index fd188c0a2f35..000000000000 --- a/services/headless-lms/models/.sqlx/query-62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\nSELECT\n AVG(EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at))::int8 AS average_duration\nFROM course_instance_enrollments ce\nJOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id\nWHERE ce.course_instance_id = $1\n AND ce.deleted_at IS NULL\n AND cmc.deleted_at IS NULL;\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "average_duration", - "type_info": "Int8" - } - ], - "parameters": { - "Left": ["Uuid"] - }, - "nullable": [null] - }, - "hash": "62049b4e97be627a5081126e649173a08e17b2041fe3e2973df81bc488e3da71" -} diff --git a/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json b/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json index 04f78b12271a..6af6203bc101 100644 --- a/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json +++ b/services/headless-lms/models/.sqlx/query-817b5fa25f0aed6802f3bf5d64116a5c0b85411352d159a46b9ddaac92e5f7bc.json @@ -35,7 +35,7 @@ }, { "ordinal": 6, - "name": "duration", + "name": "duration_seconds", "type_info": "Int4" } ], diff --git a/services/headless-lms/models/.sqlx/query-75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521.json b/services/headless-lms/models/.sqlx/query-9840145a8c388e39c73aa7d7763124f54d206fee3ecaef449808df6239abe6fa.json similarity index 56% rename from services/headless-lms/models/.sqlx/query-75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521.json rename to services/headless-lms/models/.sqlx/query-9840145a8c388e39c73aa7d7763124f54d206fee3ecaef449808df6239abe6fa.json index a5c3372dfb01..fd07c12be65b 100644 --- a/services/headless-lms/models/.sqlx/query-75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521.json +++ b/services/headless-lms/models/.sqlx/query-9840145a8c388e39c73aa7d7763124f54d206fee3ecaef449808df6239abe6fa.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO cheater_thresholds (\n course_instance_id,\n duration,\n points\n )\n VALUES ($1, $2, $3)\n ", + "query": "\n INSERT INTO cheater_thresholds (\n course_instance_id,\n duration_seconds,\n points\n )\n VALUES ($1, $2, $3)\n ", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "75b25ababe27e1558d4681ba2cf4ae1974f4616a6a34d589fd9c0cee9d240521" + "hash": "9840145a8c388e39c73aa7d7763124f54d206fee3ecaef449808df6239abe6fa" } diff --git a/services/headless-lms/models/.sqlx/query-a2683028ae0a7755641284b8156335858425e23c56115c28ecb3ef7e51c5169f.json b/services/headless-lms/models/.sqlx/query-a2683028ae0a7755641284b8156335858425e23c56115c28ecb3ef7e51c5169f.json new file mode 100644 index 000000000000..6fc9c72be120 --- /dev/null +++ b/services/headless-lms/models/.sqlx/query-a2683028ae0a7755641284b8156335858425e23c56115c28ecb3ef7e51c5169f.json @@ -0,0 +1,18 @@ +{ + "db_name": "PostgreSQL", + "query": "\nSELECT\n AVG(EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at))::int8 AS average_duration_seconds\nFROM course_instance_enrollments ce\nJOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id\nWHERE ce.course_instance_id = $1\n AND ce.deleted_at IS NULL\n AND cmc.deleted_at IS NULL;\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "average_duration_seconds", + "type_info": "Int8" + } + ], + "parameters": { + "Left": ["Uuid"] + }, + "nullable": [null] + }, + "hash": "a2683028ae0a7755641284b8156335858425e23c56115c28ecb3ef7e51c5169f" +} diff --git a/services/headless-lms/models/.sqlx/query-c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a.json b/services/headless-lms/models/.sqlx/query-c2f2aa72ce142e7784325d9127d70ae10b6e85f445179051dbabb8dcacdc3c73.json similarity index 55% rename from services/headless-lms/models/.sqlx/query-c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a.json rename to services/headless-lms/models/.sqlx/query-c2f2aa72ce142e7784325d9127d70ae10b6e85f445179051dbabb8dcacdc3c73.json index 083ac470ab1c..e50bac69ef99 100644 --- a/services/headless-lms/models/.sqlx/query-c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a.json +++ b/services/headless-lms/models/.sqlx/query-c2f2aa72ce142e7784325d9127d70ae10b6e85f445179051dbabb8dcacdc3c73.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n INSERT INTO suspected_cheaters (\n user_id,\n total_duration,\n total_points\n )\n VALUES ($1, $2, $3)\n ", + "query": "\n INSERT INTO suspected_cheaters (\n user_id,\n total_duration_seconds,\n total_points\n )\n VALUES ($1, $2, $3)\n ", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "c9fb34a08eb2bb74c107e3a1beca26c807a2994438e68b13886e1c555052121a" + "hash": "c2f2aa72ce142e7784325d9127d70ae10b6e85f445179051dbabb8dcacdc3c73" } diff --git a/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json b/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json index 87e211e526f2..bc50b0f87be3 100644 --- a/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json +++ b/services/headless-lms/models/.sqlx/query-f604ff0b17b848a9a5b4eec08857c8a3765ba5766f960c47b6b5e6cadec790b2.json @@ -30,7 +30,7 @@ }, { "ordinal": 5, - "name": "total_duration", + "name": "total_duration_seconds", "type_info": "Int4" }, { diff --git a/services/headless-lms/models/src/course_instances.rs b/services/headless-lms/models/src/course_instances.rs index 6907cb7679f0..c7833198bfa6 100644 --- a/services/headless-lms/models/src/course_instances.rs +++ b/services/headless-lms/models/src/course_instances.rs @@ -26,11 +26,11 @@ pub struct CourseInstance { pub support_email: Option, } -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -#[cfg_attr(feature = "ts_rs", derive(TS))] -pub struct CourseAverageDuration { - pub average_duration: Option, -} +// #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +// #[cfg_attr(feature = "ts_rs", derive(TS))] +// pub struct CourseAverageDuration { +// pub average_duration_seconds: Option, +// } impl CourseInstance { pub fn is_open(&self) -> bool { @@ -755,35 +755,34 @@ WHERE user_id = $1 pub async fn get_course_average_duration( conn: &mut PgConnection, course_instance_id: Uuid, -) -> ModelResult { - let res = sqlx::query_as!( - CourseAverageDuration, - r#" +) -> ModelResult> { + let res = sqlx::query!( + " SELECT - AVG(EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at))::int8 AS average_duration + AVG(EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at))::int8 AS average_duration_seconds FROM course_instance_enrollments ce JOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id WHERE ce.course_instance_id = $1 AND ce.deleted_at IS NULL AND cmc.deleted_at IS NULL; - "#, + ", course_instance_id ) .fetch_one(conn) .await?; - Ok(res) + + Ok(res.average_duration_seconds) } pub async fn get_student_duration( conn: &mut PgConnection, user_id: Uuid, course_instance_id: Uuid, -) -> ModelResult { - let res = sqlx::query_as!( - CourseAverageDuration, - r#" +) -> ModelResult> { + let res = sqlx::query!( + " SELECT - EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at)::int8 AS average_duration + COALESCE(EXTRACT(EPOCH FROM cmc.completion_date - ce.created_at)::int8, 0) AS student_duration_seconds FROM course_instance_enrollments ce JOIN course_module_completions cmc ON cmc.course_instance_id = ce.course_instance_id AND cmc.user_id = ce.user_id @@ -791,13 +790,14 @@ WHERE ce.course_instance_id = $1 AND ce.user_id = $2 AND ce.deleted_at IS NULL AND cmc.deleted_at IS NULL; - "#, + ", course_instance_id, user_id ) .fetch_one(conn) .await?; - Ok(res) + + Ok(res.student_duration_seconds) } #[cfg(test)] diff --git a/services/headless-lms/models/src/suspected_cheaters.rs b/services/headless-lms/models/src/suspected_cheaters.rs index c3e72af8848f..b83769bba1ef 100644 --- a/services/headless-lms/models/src/suspected_cheaters.rs +++ b/services/headless-lms/models/src/suspected_cheaters.rs @@ -8,7 +8,7 @@ pub struct SuspectedCheaters { pub created_at: DateTime, pub deleted_at: Option>, pub updated_at: Option>, - pub total_duration: Option, // Represented in milliseconds + pub total_duration_seconds: Option, // Represented in seconds pub total_points: i32, } @@ -16,7 +16,7 @@ pub struct SuspectedCheaters { #[cfg_attr(feature = "ts_rs", derive(TS))] pub struct ThresholdData { pub points: i32, - pub duration: Option, + pub duration_seconds: Option, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -28,26 +28,26 @@ pub struct Threshold { pub updated_at: DateTime, pub deleted_at: Option>, pub points: i32, - pub duration: Option, + pub duration_seconds: Option, } pub async fn insert( conn: &mut PgConnection, user_id: Uuid, - total_duration: Option, + total_duration_seconds: Option, total_points: i32, ) -> ModelResult<()> { sqlx::query!( " INSERT INTO suspected_cheaters ( user_id, - total_duration, + total_duration_seconds, total_points ) VALUES ($1, $2, $3) ", user_id, - total_duration, + total_duration_seconds, total_points ) .fetch_one(conn) @@ -58,20 +58,20 @@ pub async fn insert( pub async fn insert_thresholds( conn: &mut PgConnection, course_instance_id: Uuid, - duration: Option, + duration_seconds: Option, points: i32, ) -> ModelResult<()> { sqlx::query!( " INSERT INTO cheater_thresholds ( course_instance_id, - duration, + duration_seconds, points ) VALUES ($1, $2, $3) ", course_instance_id, - duration, + duration_seconds, points ) .execute(conn) diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index 309e3424569e..d54452d8e689 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -505,7 +505,7 @@ async fn insert_suspected_cheaters( // all do this for a single student and compare - let average_duration = + let average_duration_seconds = models::course_instances::get_course_average_duration(&mut conn, course_instance_id) .await?; @@ -566,7 +566,7 @@ async fn insert_suspected_cheaters( .await? .unwrap_or(0.0); - let student_duration = models::course_instances::get_student_duration( + let student_duration_seconds = models::course_instances::get_student_duration( &mut conn, completion.user_id, course_instance_id, @@ -577,10 +577,7 @@ async fn insert_suspected_cheaters( continue; } - let completion_average_duration = student_duration.average_duration; - let average_duration_value = average_duration.average_duration; - - if completion_average_duration > average_duration_value { + if student_duration_seconds > average_duration_seconds { models::suspected_cheaters::insert( &mut conn, completion.user_id, From d1ccf991c84ab7e728d14e9b5130daa25f9a5302 Mon Sep 17 00:00:00 2001 From: george-misan Date: Sat, 27 Apr 2024 15:12:15 +0300 Subject: [PATCH 30/34] [migration-job]: update migaration file --- ...0312115600_add_suspected_cheaters.down.sql | 6 -- ...240312115600_add_suspected_cheaters.up.sql | 74 ++----------------- 2 files changed, 6 insertions(+), 74 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql index c96d05823b98..9f922e7c06c6 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.down.sql @@ -1,8 +1,2 @@ DROP TABLE suspected_cheaters; -DROP TABLE course_student_average; - --- - -DROP TABLE suspected_cheaters_exercise_list; -DROP TABLE exercise_student_average; DROP TABLE cheater_thresholds; diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index aa36844346d4..c09fd133be85 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -9,78 +9,16 @@ CREATE TABLE suspected_cheaters ( ); CREATE TRIGGER set_timestamp BEFORE UPDATE ON suspected_cheaters FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); -COMMENT ON TABLE suspected_cheaters IS 'This table stores data regarding student that are suspected of cheating in a course.'; +-- The suspected_cheaters table is a that contains a student that are suspected of cheating because they meet the cheating requirement (i.e. score > threshold && duration > average_duration) +COMMENT ON TABLE suspected_cheaters IS 'This table stores data regarding student that are suspected of cheating in a course instance.'; COMMENT ON COLUMN suspected_cheaters.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters.user_id IS 'The user_id of the student being suspected.'; COMMENT ON COLUMN suspected_cheaters.created_at IS 'Timestamp when the record was created.'; COMMENT ON COLUMN suspected_cheaters.updated_at IS 'Timestamp when the record was updated.'; COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN suspected_cheaters.total_duration_seconds IS 'The total duration the student spend completing the course.'; -COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points the student acquired in the course.'; -CREATE TABLE course_student_average ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - course_instance_id UUID NOT NULL REFERENCES course_instances, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - deleted_at TIMESTAMP WITH TIME ZONE, - average_duration_seconds INTEGER, - average_points INTEGER NOT NULL -); -CREATE TRIGGER set_timestamp BEFORE -UPDATE ON course_student_average FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); -COMMENT ON TABLE course_student_average IS 'This table stores data regarding the average in a specific course.'; -COMMENT ON COLUMN course_student_average.course_instance_id IS 'A unique, stable identifier for the record.'; -COMMENT ON COLUMN course_student_average.created_at IS 'Timestamp when the record was created.'; -COMMENT ON COLUMN course_student_average.updated_at IS 'Timestamp when the record was updated.'; -COMMENT ON COLUMN course_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN course_student_average.average_duration_seconds IS 'The average duration all student spent completing the course.'; -COMMENT ON COLUMN course_student_average.average_points IS 'The average points all students acquired in the course.'; --- - -CREATE TABLE suspected_cheaters_exercise_list ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - student_id UUID NOT NULL REFERENCES course_module_completions, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - deleted_at TIMESTAMP WITH TIME ZONE, - exercise_id UUID REFERENCES exercises NOT NULL, - duration_seconds INTEGER, - points INTEGER NOT NULL, - attempts INTEGER NOT NULL, - status activity_progress NOT NULL DEFAULT 'initialized' -); -CREATE TRIGGER set_timestamp BEFORE -UPDATE ON suspected_cheaters_exercise_list FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); -COMMENT ON TABLE suspected_cheaters_exercise_list IS 'This table stores data regarding the list of exercises pertaining to students that have been suspected of cheating in a course.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.id IS 'A unique, stable identifier for the record.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.student_id IS 'The id of the student being suspected.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.updated_at IS 'Timestamp when the record was updated.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.created_at IS 'Timestamp when the record was created.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.exercise_id IS 'Exercise Id of an exercise completed by the suspected student.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.duration_seconds IS 'The duration a suspected student used in completing an exercise.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.points IS 'The points a suspected student received from completing an exercise.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.attempts IS 'The number of times a student attempt an exercise.'; -COMMENT ON COLUMN suspected_cheaters_exercise_list.status IS 'The status of an exercise.'; -CREATE TABLE exercise_student_average ( - id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, - exercise_id UUID NOT NULL REFERENCES exercises, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), - deleted_at TIMESTAMP WITH TIME ZONE, - average_duration_seconds INTEGER, - average_points INTEGER NOT NULL -); -CREATE TRIGGER set_timestamp BEFORE -UPDATE ON exercise_student_average FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); -COMMENT ON TABLE exercise_student_average IS 'This table stores data regarding the average in a specific course.'; -COMMENT ON COLUMN exercise_student_average.id IS 'A unique, stable identifier for the record.'; -COMMENT ON COLUMN exercise_student_average.exercise_id IS 'The exercise_id of the exercise.'; -COMMENT ON COLUMN exercise_student_average.created_at IS 'Timestamp when the record was created.'; -COMMENT ON COLUMN exercise_student_average.updated_at IS 'Timestamp when the record was updated.'; -COMMENT ON COLUMN exercise_student_average.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; -COMMENT ON COLUMN exercise_student_average.average_duration_seconds IS 'The average duration a all student used in completing an exercise.'; -COMMENT ON COLUMN exercise_student_average.average_points IS 'The average points all student received from completing an exercise.'; +COMMENT ON COLUMN suspected_cheaters.total_duration_seconds IS 'The total duration the student spent completing the course.'; +COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points earned by the student in the course.'; +-- The cheater_thresholds table contains thresholds set by the instructor, representing the maximum score or duration a student can surpass before being suspected of cheating. CREATE TABLE cheater_thresholds ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, course_instance_id UUID NOT NULL REFERENCES course_instances, @@ -92,7 +30,7 @@ CREATE TABLE cheater_thresholds ( ); CREATE TRIGGER set_timestamp BEFORE UPDATE ON cheater_thresholds FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); -COMMENT ON TABLE cheater_thresholds IS 'This table stores threshold for measuring cheaters.'; +COMMENT ON TABLE cheater_thresholds IS 'This table stores thresholds set by the instructor, representing the maximum score or duration a student can surpass before being suspected of cheating cheaters.'; COMMENT ON COLUMN cheater_thresholds.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN cheater_thresholds.course_instance_id IS 'The course_instance_id of the course.'; COMMENT ON COLUMN cheater_thresholds.created_at IS 'Timestamp when the record was created.'; From fae2fdc05a3eb0754e572ba918e8de7a9154ad2c Mon Sep 17 00:00:00 2001 From: george-misan Date: Sat, 27 Apr 2024 15:18:16 +0300 Subject: [PATCH 31/34] [cleanup]: remove unused code --- .../main_frontend/course_instances.rs | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index d54452d8e689..4e4530d8338d 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -509,46 +509,6 @@ async fn insert_suspected_cheaters( models::course_instances::get_course_average_duration(&mut conn, course_instance_id) .await?; - // let average_points = course_average.average_points.unwrap_or(0.0); - - // Iterate through completions, compare grades with average score and thresholds, and insert suspected cheaters - // for completion in completions { - // if let Some(_grade) = completion.grade { - // let total_points = models::user_exercise_states::get_user_total_course_points( - // &mut conn, - // user.id, - // course_instance_id, - // ) - // .await? - // .unwrap_or(0.0); - - // let student_duration = models::course_instances::get_student_duration( - // &mut conn, - // completion.user_id, - // course_instance_id, - // ) - // .await?; - - // if total_points as i32 > thresholds.points - // && student_duration.average_duration > average_duration.average_duration - // { - // // Insert suspected cheater - // models::suspected_cheaters::insert( - // &mut conn, - // completion.user_id, - // None, - // total_points as i32, - // ) - // .await?; - // } - // } else { - // return Err(ControllerError::new( - // ControllerErrorType::BadRequest, - // "Grade is not a numeric value".to_string(), - // None, - // )); - // } - // } for completion in completions { if completion.grade.is_none() { return Err(ControllerError::new( From 387fbad3c9b3f6e78b0e6ed2db75ba93993315f3 Mon Sep 17 00:00:00 2001 From: george-misan Date: Sat, 27 Apr 2024 15:20:36 +0300 Subject: [PATCH 32/34] [cleanup]: remove unused code --- services/headless-lms/models/src/course_instances.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/services/headless-lms/models/src/course_instances.rs b/services/headless-lms/models/src/course_instances.rs index c7833198bfa6..dc99057f351e 100644 --- a/services/headless-lms/models/src/course_instances.rs +++ b/services/headless-lms/models/src/course_instances.rs @@ -26,12 +26,6 @@ pub struct CourseInstance { pub support_email: Option, } -// #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -// #[cfg_attr(feature = "ts_rs", derive(TS))] -// pub struct CourseAverageDuration { -// pub average_duration_seconds: Option, -// } - impl CourseInstance { pub fn is_open(&self) -> bool { self.starts_at.map(|sa| sa < Utc::now()).unwrap_or_default() From 6c7b340ab1d5fdf6c9395c488af879ec525da16b Mon Sep 17 00:00:00 2001 From: george-misan Date: Sat, 27 Apr 2024 15:22:02 +0300 Subject: [PATCH 33/34] [cleanup]: remove redundant commment --- services/headless-lms/models/src/suspected_cheaters.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/headless-lms/models/src/suspected_cheaters.rs b/services/headless-lms/models/src/suspected_cheaters.rs index b83769bba1ef..86fa31980693 100644 --- a/services/headless-lms/models/src/suspected_cheaters.rs +++ b/services/headless-lms/models/src/suspected_cheaters.rs @@ -8,7 +8,7 @@ pub struct SuspectedCheaters { pub created_at: DateTime, pub deleted_at: Option>, pub updated_at: Option>, - pub total_duration_seconds: Option, // Represented in seconds + pub total_duration_seconds: Option, pub total_points: i32, } From f7d499ba397079de81be90e45ef8cb18c3d05b7c Mon Sep 17 00:00:00 2001 From: george-misan Date: Tue, 7 May 2024 08:47:00 +0300 Subject: [PATCH 34/34] [code-review]: update migration & insert_suspected_cheaters route --- ...240312115600_add_suspected_cheaters.up.sql | 5 +- .../main_frontend/course_instances.rs | 80 ++++++++----------- 2 files changed, 37 insertions(+), 48 deletions(-) diff --git a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql index c09fd133be85..4b5820c3a99c 100644 --- a/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql +++ b/services/headless-lms/migrations/20240312115600_add_suspected_cheaters.up.sql @@ -9,8 +9,7 @@ CREATE TABLE suspected_cheaters ( ); CREATE TRIGGER set_timestamp BEFORE UPDATE ON suspected_cheaters FOR EACH ROW EXECUTE PROCEDURE trigger_set_timestamp(); --- The suspected_cheaters table is a that contains a student that are suspected of cheating because they meet the cheating requirement (i.e. score > threshold && duration > average_duration) -COMMENT ON TABLE suspected_cheaters IS 'This table stores data regarding student that are suspected of cheating in a course instance.'; +COMMENT ON TABLE suspected_cheaters IS 'Contains a student that are suspected of cheating because they meet the cheating requirement (i.e. score > threshold && duration > average_duration).'; COMMENT ON COLUMN suspected_cheaters.id IS 'A unique, stable identifier for the record.'; COMMENT ON COLUMN suspected_cheaters.user_id IS 'The user_id of the student being suspected.'; COMMENT ON COLUMN suspected_cheaters.created_at IS 'Timestamp when the record was created.'; @@ -18,7 +17,7 @@ COMMENT ON COLUMN suspected_cheaters.updated_at IS 'Timestamp when the record wa COMMENT ON COLUMN suspected_cheaters.deleted_at IS 'Timestamp when the record was deleted. If null, the record is not deleted.'; COMMENT ON COLUMN suspected_cheaters.total_duration_seconds IS 'The total duration the student spent completing the course.'; COMMENT ON COLUMN suspected_cheaters.total_points IS 'The total points earned by the student in the course.'; --- The cheater_thresholds table contains thresholds set by the instructor, representing the maximum score or duration a student can surpass before being suspected of cheating. +-- The cheater_thresholds table starts here. CREATE TABLE cheater_thresholds ( id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, course_instance_id UUID NOT NULL REFERENCES course_instances, diff --git a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs index 4e4530d8338d..925bd0713728 100644 --- a/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs +++ b/services/headless-lms/server/src/controllers/main_frontend/course_instances.rs @@ -484,69 +484,59 @@ async fn insert_threshold( #[instrument(skip(pool))] async fn insert_suspected_cheaters( pool: web::Data, - params: web::Path, - // payload: web::Json, + params: web::Path<(Uuid, Uuid, Uuid)>, user: AuthUser, ) -> ControllerResult> { let mut conn = pool.acquire().await?; - let course_instance_id = params.into_inner(); + let (course_instance_id, user_id, course_module_completion_id) = params.into_inner(); + + let average_duration_seconds = + models::course_instances::get_course_average_duration(&mut conn, course_instance_id) + .await?; - // Get threshold + // Get threshold for a specific course instance let thresholds = models::suspected_cheaters::get_thresholds_by_id(&mut conn, course_instance_id).await?; - // Get all completions for the course instance - let completions = models::course_module_completions::get_all_by_course_instance_id( + // Get all completions for the a course module completion + + let completion = + models::course_module_completions::get_by_id(&mut conn, course_module_completion_id) + .await?; + + if completion.grade.is_none() { + return Err(ControllerError::new( + ControllerErrorType::BadRequest, + "Grade is not a numeric value".to_string(), + None, + )); + } + + let total_points = models::user_exercise_states::get_user_total_course_points( &mut conn, + user_id, course_instance_id, ) - .await?; - - // all do this for a single student and compare + .await? + .unwrap_or(0.0); - let average_duration_seconds = - models::course_instances::get_course_average_duration(&mut conn, course_instance_id) + let student_duration_seconds = + models::course_instances::get_student_duration(&mut conn, user_id, course_instance_id) .await?; - for completion in completions { - if completion.grade.is_none() { - return Err(ControllerError::new( - ControllerErrorType::BadRequest, - "Grade is not a numeric value".to_string(), - None, - )); - } - - let total_points = models::user_exercise_states::get_user_total_course_points( - &mut conn, - user.id, - course_instance_id, - ) - .await? - .unwrap_or(0.0); - - let student_duration_seconds = models::course_instances::get_student_duration( + if student_duration_seconds > average_duration_seconds + && total_points as i32 <= thresholds.points + { + models::suspected_cheaters::insert( &mut conn, completion.user_id, - course_instance_id, + None, + total_points as i32, ) .await?; - - if total_points as i32 <= thresholds.points { - continue; - } - - if student_duration_seconds > average_duration_seconds { - models::suspected_cheaters::insert( - &mut conn, - completion.user_id, - None, - total_points as i32, - ) - .await?; - } } + // } let token = authorize( &mut conn, @@ -615,7 +605,7 @@ pub fn _add_routes(cfg: &mut ServiceConfig) { web::post().to(insert_threshold), ) .route( - "/{course_instance_id}/suspected-cheaters", + "/{course_instance_id}/suspected-cheaters/{user_id}/course-module-completion/{course_module_completion_id}", web::post().to(insert_suspected_cheaters), ) .route(