From 8329ea81c3669a5b9ca4f10859e221b35f3c6fbe Mon Sep 17 00:00:00 2001 From: 41abku1mst <41abku1mst@hft-stuttgart.de> Date: Tue, 22 Jul 2025 12:02:16 +0200 Subject: [PATCH 1/2] student submission code added --- newupdatedFile/Dockerfile | 41 ++ newupdatedFile/docker-compose.yml | 58 ++ newupdatedFile/docker-entrypoint.sh | 7 + newupdatedFile/dta.zip | Bin 0 -> 45970 bytes newupdatedFile/full_output.log | 119 ++++ newupdatedFile/moodle.ini | 1 + newupdatedFile/nginx.conf | 34 ++ newupdatedFile/student-dir.txt | 1 + .../teacher-dta-dir-test-runner-jdk21.txt | 1 + newupdatedFile/test-moodle.sh | 524 ++++++++++++++++++ 10 files changed, 786 insertions(+) create mode 100644 newupdatedFile/Dockerfile create mode 100644 newupdatedFile/docker-compose.yml create mode 100644 newupdatedFile/docker-entrypoint.sh create mode 100644 newupdatedFile/dta.zip create mode 100644 newupdatedFile/full_output.log create mode 100644 newupdatedFile/moodle.ini create mode 100644 newupdatedFile/nginx.conf create mode 100644 newupdatedFile/student-dir.txt create mode 100644 newupdatedFile/teacher-dta-dir-test-runner-jdk21.txt create mode 100644 newupdatedFile/test-moodle.sh diff --git a/newupdatedFile/Dockerfile b/newupdatedFile/Dockerfile new file mode 100644 index 0000000..769692c --- /dev/null +++ b/newupdatedFile/Dockerfile @@ -0,0 +1,41 @@ +FROM debian:bookworm + +# Install Apache, PHP, and other required extensions +RUN apt-get update && apt-get install -y \ + apache2 \ + php \ + php-cli \ + php-gd \ + php-mysql \ + php-intl \ + php-curl \ + php-xml \ + php-xmlrpc \ + php-soap \ + php-zip \ + php-mbstring \ + unzip \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Add custom PHP settings for Moodle +# We copy it to both the apache2 and cli config directories +COPY moodle.ini /etc/php/8.2/apache2/conf.d/99-moodle.ini +COPY moodle.ini /etc/php/8.2/cli/conf.d/99-moodle.ini + +# Download and unpack Moodle +ARG MOODLE_VERSION=403 +RUN rm -rf /var/www/html +RUN git clone --depth 1 --branch MOODLE_${MOODLE_VERSION}_STABLE https://github.com/moodle/moodle.git /var/www/html/ + +# Create the moodledata directory +RUN mkdir /var/www/moodledata && chown -R www-data:www-data /var/www/moodledata + +# Set correct permissions +WORKDIR /var/www/html +RUN chown -R www-data:www-data /var/www/html + +# Copy a startup script +COPY --chown=root:root docker-entrypoint.sh / +ENTRYPOINT ["/docker-entrypoint.sh"] +CMD ["apache2ctl", "-D", "FOREGROUND"] diff --git a/newupdatedFile/docker-compose.yml b/newupdatedFile/docker-compose.yml new file mode 100644 index 0000000..828f0b3 --- /dev/null +++ b/newupdatedFile/docker-compose.yml @@ -0,0 +1,58 @@ +version: '3.8' + +services: + db: + image: mariadb:10.6 + container_name: moodle-db-official + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + volumes: + - db_data:/var/lib/mysql + environment: + - MARIADB_ROOT_PASSWORD=verysecretpassword + - MARIADB_DATABASE=moodle + - MARIADB_USER=moodleuser + - MARIADB_PASSWORD=moodlepassword + restart: unless-stopped + + web: + build: . # This tells it to use the Dockerfile in this directory + container_name: moodle-web-official + volumes: + # Mount our local plugin for easy installation + - ./dta.zip:/tmp/dta.zip + # Mount the moodledata directory as a named volume + - moodledata:/var/www/moodledata + ports: + - "8080:80" + depends_on: + - db + restart: unless-stopped + + backend: + container_name: backend + image: hftstuttgart/cota-backend:latest + user: "${EUSER}:${EGID}" + ports: # Add this section + - "8081:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - '/tmp/dta-tests:/tmp/dta-tests' + restart: unless-stopped + # This service can be reached by the 'web' container at http://backend:[port] + + cache: + container_name: cachedta + image: nginx:alpine + ports: + - '82:8080' + volumes: + - './conf:/etc/nginx/conf.d' # Nginx configs go here + depends_on: + - backend + restart: unless-stopped + +volumes: + db_data: + moodledata: diff --git a/newupdatedFile/docker-entrypoint.sh b/newupdatedFile/docker-entrypoint.sh new file mode 100644 index 0000000..49e734c --- /dev/null +++ b/newupdatedFile/docker-entrypoint.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +# Apache gets grumpy about PID files pre-existing +rm -f /var/run/apache2/apache2.pid + +exec "$@" diff --git a/newupdatedFile/dta.zip b/newupdatedFile/dta.zip new file mode 100644 index 0000000000000000000000000000000000000000..8de309aa041c025b22d6260e77ee494e9282ea4d GIT binary patch literal 45970 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 Z=Kn$=fxD3(n*@B2YgiDF;Nl 4 + [modulename] => assign + [name] => Fully Automated DTA Assignment + [intro] => This assignment was created by the final automation script. + [introformat] => 1 + [section] => 1 + [visible] => 1 + [assignsubmission_dta_enabled] => 1 + [assignsubmission_file_enabled] => 1 + [dta] => 1 + [tests_draft_dta] => 121728303 + [assignsubmission_file_maxsizebytes] => 0 + [cmidnumber] => + [assignsubmission_file_maxfiles] => 20 + [assignsubmission_onlinetext_enabled] => 0 + [submissiondrafts] => 0 + [requiresubmissionstatement] => 0 + [requireallteammemberssubmit] => 0 + [teamsubmission] => 0 + [sendnotifications] => 1 + [sendlatenotifications] => 0 + [sendstudentnotifications] => 1 + [grade] => 100 + [blindmarking] => 0 + [markingworkflow] => 0 + [markingallocation] => 0 + [attemptreopenmethod] => none + [allowsubmissionsfromdate] => 1753175949 + [duedate] => 1753780749 + [cutoffdate] => 0 + [gradingduedate] => 0 + [module] => 1 + [timemodified] => 1753175949 + [alwaysshowdescription] => 0 + [completionsubmit] => 0 +) + +DEBUG: Returned object from add_moduleinfo(): +stdClass Object +( + [course] => 4 + [modulename] => assign + [name] => Fully Automated DTA Assignment + [intro] => This assignment was created by the final automation script. + [introformat] => 1 + [section] => 1 + [visible] => 1 + [assignsubmission_dta_enabled] => 1 + [assignsubmission_file_enabled] => 1 + [dta] => 1 + [tests_draft_dta] => 121728303 + [assignsubmission_file_maxsizebytes] => 0 + [cmidnumber] => + [assignsubmission_file_maxfiles] => 20 + [assignsubmission_onlinetext_enabled] => 0 + [submissiondrafts] => 0 + [requiresubmissionstatement] => 0 + [requireallteammemberssubmit] => 0 + [teamsubmission] => 0 + [sendnotifications] => 1 + [sendlatenotifications] => 0 + [sendstudentnotifications] => 1 + [grade] => 100 + [blindmarking] => 0 + [markingworkflow] => 0 + [markingallocation] => 0 + [attemptreopenmethod] => none + [allowsubmissionsfromdate] => 1753175949 + [duedate] => 1753780749 + [cutoffdate] => 0 + [gradingduedate] => 0 + [module] => 1 + [timemodified] => 1753175949 + [alwaysshowdescription] => 0 + [completionsubmit] => 0 + [instance] => 3 + [coursemodule] => 6 + [groupingid] => 0 + [completion] => 0 + [completionview] => 0 + [completionexpected] => 0 + [completionpassgrade] => 0 + [completiongradeitemnumber] => + [conditiongradegroup] => Array + ( + ) + + [conditionfieldgroup] => Array + ( + ) + + [visibleoncoursepage] => 1 + [downloadcontent] => 1 + [groupmode] => 0 + [beforemod] => + [gradingman] => grading_manager Object + ( + [context:protected] => core\context\module Object + ( + [_id:protected] => 19 + [_contextlevel:protected] => 70 + [_instanceid:protected] => 6 + [_path:protected] => /1/3/18/19 + [_depth:protected] => 4 + [_locked:protected] => 0 + ) + + [component:protected] => mod_assign + [area:protected] => + [areacache:grading_manager:private] => + ) + + [showgradingmanagement] => +) + +3 diff --git a/newupdatedFile/moodle.ini b/newupdatedFile/moodle.ini new file mode 100644 index 0000000..cc0cce8 --- /dev/null +++ b/newupdatedFile/moodle.ini @@ -0,0 +1 @@ +max_input_vars = 5000 diff --git a/newupdatedFile/nginx.conf b/newupdatedFile/nginx.conf new file mode 100644 index 0000000..7d7cb39 --- /dev/null +++ b/newupdatedFile/nginx.conf @@ -0,0 +1,34 @@ +worker_processes 5; +worker_rlimit_nofile 8192; + +events { + worker_connections 4096; +} + +http { + default_type application/octet-stream; + sendfile on; + tcp_nopush on; + server_names_hash_bucket_size 128; + + proxy_cache_path /opt/cache levels=1:2 keys_zone=dta_cache:10m max_size=10g inactive=60m use_temp_path=off; + + server { + listen 8080; + + location /v1/task { + proxy_cache dta_cache; + proxy_cache_background_update on; + proxy_cache_lock on; + proxy_cache_key "$scheme$request_uri"; + proxy_cache_valid 200 302 1d; + proxy_cache_methods GET HEAD POST; + + proxy_pass http://backendcomposedta:8080/v1/task; + } + + location /v1/unittest { + proxy_pass http://backendcomposedta:8080/v1/unittest; + } + } +} diff --git a/newupdatedFile/student-dir.txt b/newupdatedFile/student-dir.txt new file mode 100644 index 0000000..50f42b1 --- /dev/null +++ b/newupdatedFile/student-dir.txt @@ -0,0 +1 @@ +dta::https://transfer.hft-stuttgart.de/gitlab/cota/examples/cota-submission-calc-jdk.git::none::none::/ diff --git a/newupdatedFile/teacher-dta-dir-test-runner-jdk21.txt b/newupdatedFile/teacher-dta-dir-test-runner-jdk21.txt new file mode 100644 index 0000000..f5c9b2f --- /dev/null +++ b/newupdatedFile/teacher-dta-dir-test-runner-jdk21.txt @@ -0,0 +1 @@ +dta::https://transfer.hft-stuttgart.de/gitlab/cota/examples/cota-test-course-jdk.git::none::none::/topic02/exercise04::cota-runner-jdk21-junit5 \ No newline at end of file diff --git a/newupdatedFile/test-moodle.sh b/newupdatedFile/test-moodle.sh new file mode 100644 index 0000000..8e94cd0 --- /dev/null +++ b/newupdatedFile/test-moodle.sh @@ -0,0 +1,524 @@ +#!/bin/bash +set -e + +# ============================================================================== +# DEFINITIVE Moodle CI/CD AUTOMATION SCRIPT V10 (ROBUST ERROR CHECKING) +# ============================================================================== + +# --- Configuration (UPDATE THESE!) --- +MOODLE_URL="http://localhost:8080" +BACKEND_URL="http://localhost:8081" +MOODLE_ADMIN_USER="user" +MOODLE_ADMIN_PASS="QAYwsx@12345" +# Automation User (Course creator) +AUTOMATION_USER_NAME="maaeruser" +AUTOMATION_USER_PASS="AutomationPassword123" +# Student User (created by script) +STUDENT_USER_NAME="tstudent" +STUDENT_USER_PASS="StudentPassword123" +DTA_CONFIG_FILE="teacher-dta-dir-test-runner-jdk21.txt" +DTA_STUDENT_SUBMISSION_FILE="student-dir.txt" +# --- Static Configuration --- +MOODLE_CONTAINER="moodle-web-official" +MOODLE_SYSTEM_USER="www-data" +MOODLE_ROOT_IN_CONTAINER="/var/www/html" +PLUGIN_ZIP_NAME="dta.zip" +CUSTOM_SERVICE_NAME="dta_automation_service" +DB_CONTAINER_NAME="moodle-db-official" +DB_ROOT_PASSWORD="verysecretpassword" +DB_NAME="moodle" + +# ============================================================================== +# SCRIPT EXECUTION +# ============================================================================== + +echo "🚀 Starting Definitive Moodle Automation Script..." + +# --- Pre-flight Checks & Plugin Install (Step 1) --- +if ! docker ps --format '{{.Names}}' | grep -qw "$MOODLE_CONTAINER"; then echo "❌ Moodle container not running." && exit 1; fi +if [ ! -f "$PLUGIN_ZIP_NAME" ]; then echo "❌ Plugin ZIP file not found." && exit 1; fi +if [ ! -f "$DTA_CONFIG_FILE" ]; then echo "❌ DTA config file not found." && exit 1; fi +echo "✅ Pre-flight checks passed." + +echo "📦 Step 1: Installing custom plugin..." +# docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" bash -c "set -e; mkdir -p '$MOODLE_ROOT_IN_CONTAINER/mod/assign/submission'; unzip -o '/tmp/$PLUGIN_ZIP_NAME' -d '$MOODLE_ROOT_IN_CONTAINER/mod/assign/submission'; php '$MOODLE_ROOT_IN_CONTAINER/admin/cli/upgrade.php' --non-interactive; php '$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php'" +echo "✅ Plugin installed." + +echo " -> Enabling Web Service " +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=enablemobilewebservice --set=1 + +# --- Step 2: Create and Configure a Custom Web Service --- +echo " -> Clearing cURL security settings..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=curlsecurityblockedhosts --set="" +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=curlsecurityallowedport --set="" + +echo " -> Enabling Web Service File Uploads (System-wide)..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=webserviceuploaddisabled --set=0 +# --- THIS IS THE FINAL FIX --- +# Enable username/password authentication for web service token generation +echo " -> Enabling password authentication for web services..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=enablewsauthpassword --set=1 + +# --- Step 4: Create Dedicated API Role and Automation User --- +echo "🧑‍💻 Step 4: Creating dedicated API Role and Automation User..." +# This is a more advanced version of the user creation script +PHP_USER_SCRIPT_PATH="/tmp/create_api_user.php" +LOCAL_PHP_USER_SCRIPT_PATH="/tmp/local_create_api_user.php" +cat > "$LOCAL_PHP_USER_SCRIPT_PATH" <libdir.'/clilib.php'); +require_once(\$CFG->dirroot.'/user/lib.php'); +global \$DB; + +\$username = \$argv[1]; \$password = \$argv[2]; +if (!\$DB->record_exists('user', ['username' => \$username])) { + \$user = new stdClass(); + \$user->username = \$username; \$user->firstname = 'API'; \$user->lastname = 'User'; + \$user->email = 'api.user@example.com'; \$user->confirmed = 1; + \$user->mnethostid = \$CFG->mnet_localhost_id; \$user->auth = 'manual'; + \$userid = user_create_user(\$user, false, false); + \$user = \$DB->get_record('user', ['id' => \$userid]); + \update_internal_user_password(\$user, \$password); + echo "Created user '\$username' with ID: \$userid\n"; + + // Create a new role for API users if it doesn't exist + \$rolename = 'API User'; + \$apicapability = 'moodle/webservice:createtoken'; + \$restapicapability = 'webservice/rest:use'; + \$coursecapability = 'moodle/course:create'; + \$enrolcapability = 'enrol/manual:enrol'; + if (!\$apirole = \$DB->get_record('role', ['shortname' => 'apiuser'])) { + \$apiroleid = create_role(\$rolename, 'apiuser', 'Role for API access'); + \$apirole = \$DB->get_record('role', ['id' => \$apiroleid]); + echo "Created role '\$rolename' with ID: \$apiroleid\n"; + } + // Ensure the capability is assigned to the role + \$context = \core\context\system::instance(); + assign_capability(\$apicapability, CAP_ALLOW, \$apirole->id, \$context->id); + assign_capability(\$restapicapability, CAP_ALLOW, \$apirole->id, \$context->id); + assign_capability(\$coursecapability, CAP_ALLOW, \$apirole->id, \$context->id); + assign_capability(\$enrolcapability, CAP_ALLOW, \$apirole->id, \$context->id); + echo "Assigned capability '\$apicapability' to role '\$rolename'\n"; + + // Assign our automation user to this new role at the system level + role_assign(\$apirole->id, \$userid, \$context->id); +// Get the IDs for the roles we need to assign +\$managerroleid = \$DB->get_field('role', 'id', ['shortname' => 'manager']); +\$coursecreatorroleid = \$DB->get_field('role', 'id', ['shortname' => 'coursecreator']); +\$systemcontext = \core\context\system::instance(); + +// Assign the 'Manager' and 'Course creator' roles to the new user at the system context +role_assign(\$managerroleid, \$userid, \$systemcontext->id); +role_assign(\$coursecreatorroleid, \$userid, \$systemcontext->id); + echo "Assigned user '\$username' to role '\$rolename'\n"; +} else { + \$userid = \$DB->get_field('user', 'id', ['username' => \$username]); + echo "User '\$username' already exists with ID: \$userid\n"; +} + +// Create Student User +\$student_username = \$argv[3]; \$student_password = \$argv[4]; +if (!\$DB->record_exists('user', ['username' => \$student_username])) { + \$student = new stdClass(); \$student->username = \$student_username; \$student->firstname = 'Test'; \$student->lastname = 'Student'; + \$student->email = 'student@example.com'; \$student->confirmed = 1; \$student->mnethostid = \$CFG->mnet_localhost_id; \$student->auth = 'manual'; + \$studentid = user_create_user(\$student, false, false); + \$student = \$DB->get_record('user', ['id' => \$studentid]); + \update_internal_user_password(\$student, \$student_password); + echo "Created user '\$student_username' with ID: \$studentid\n"; + + // Create a new role for API users if it doesn't exist + \$rolename = 'API Student user'; + \$apicapability = 'moodle/webservice:createtoken'; + \$restapicapability = 'webservice/rest:use'; + if (!\$apirole = \$DB->get_record('role', ['shortname' => 'apistudentuser'])) { + \$apiroleid = create_role(\$rolename, 'apistudentuser', 'Role for API access'); + \$apirole = \$DB->get_record('role', ['id' => \$apiroleid]); + echo "Created role '\$rolename' with ID: \$apiroleid\n"; + } + // Ensure the capability is assigned to the role + \$context = \core\context\system::instance(); + assign_capability(\$apicapability, CAP_ALLOW, \$apirole->id, \$context->id); + assign_capability(\$restapicapability, CAP_ALLOW, \$apirole->id, \$context->id); + echo "Assigned capability '\$apicapability' to role '\$rolename'\n"; + + // Assign our automation user to this new role at the system level + role_assign(\$apirole->id, \$studentid, \$context->id); + echo "Assigned user '\$username' to role '\$rolename'\n"; +} +EOF +docker cp "$LOCAL_PHP_USER_SCRIPT_PATH" "$MOODLE_CONTAINER:$PHP_USER_SCRIPT_PATH" +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$PHP_USER_SCRIPT_PATH" "$AUTOMATION_USER_NAME" "$AUTOMATION_USER_PASS" "$STUDENT_USER_NAME" "$STUDENT_USER_PASS" +docker exec "$MOODLE_CONTAINER" rm "$PHP_USER_SCRIPT_PATH" +rm "$LOCAL_PHP_USER_SCRIPT_PATH" +echo "✅ Automation and student users are configured." + +echo "⚙️ Step 2: Creating and configuring a dedicated web service..." +docker exec -i "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" < Purging Moodle caches.." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" +sleep 15 + +# --- Step 6: Assign System Roles via API (The Definitive Fix) --- +echo "🛡️ Step 6: Assigning System Roles to Automation User..." +# We use the main site admin's token for this one-time setup task, as it's guaranteed to have permissions. +ADMIN_SETUP_TOKEN_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/login/token.php" --data-urlencode "username=$MOODLE_ADMIN_USER" --data-urlencode "password=$MOODLE_ADMIN_PASS" --data-urlencode "service=moodle_mobile_app") +ADMIN_SETUP_TOKEN=$(echo "$ADMIN_SETUP_TOKEN_RESPONSE" | jq -r '.token') + +# Get IDs needed for role assignment +AUTOMATION_USER_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_user WHERE username='$AUTOMATION_USER_NAME';") +MANAGER_ROLE_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_role WHERE shortname='manager';") +COURSE_CREATOR_ROLE_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_role WHERE shortname='coursecreator';") +SYSTEM_CONTEXT_ID=10 # The ID for the System context is always 1 + +# Assign Manager and Course Creator roles to the automation user +curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$ADMIN_SETUP_TOKEN" -d "wsfunction=core_role_assign_roles" -d "moodlewsrestformat=json" \ + -d "assignments[0][roleid]=$MANAGER_ROLE_ID" -d "assignments[0][userid]=$AUTOMATION_USER_ID" -d "assignments[0][contextid]=$SYSTEM_CONTEXT_ID" \ + -d "assignments[1][roleid]=$COURSE_CREATOR_ROLE_ID" -d "assignments[1][userid]=$AUTOMATION_USER_ID" -d "assignments[1][contextid]=$SYSTEM_CONTEXT_ID" > /dev/null +echo "✅ Assigned System Roles to Automation User." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" +sleep 15 + +# --- THIS IS THE CRITICAL FIX --- +echo " -> Purging Moodle caches to apply service changes..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" +echo "✅ Caches purged." +sleep 5 + + +echo "🔐 Step 3: Obtaining API Token...$AUTOMATION_USER_NAME..$AUTOMATION_USER_PASS" +TOKEN_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/login/token.php" \ + --data-urlencode "username=$AUTOMATION_USER_NAME" \ + --data-urlencode "password=$AUTOMATION_USER_PASS" \ + --data-urlencode "service=$CUSTOM_SERVICE_NAME") + +# Check for a Moodle error in the response FIRST +if echo "$TOKEN_RESPONSE" | jq -e 'if type=="object" and .error then true else false end' > /dev/null; then + echo "❌ Moodle API returned an error when requesting a token:" + echo "$TOKEN_RESPONSE" | jq . + exit 1 +fi + +TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.token') +echo "✅ Successfully obtained API token. $TOKEN" + + +# --- Step 4: Create a Course --- +echo "📚 Step 4: Creating a new course..." +COURSE_SHORTNAME="DTA-Course-$(date +%s)" +COURSE_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$TOKEN" -d "wsfunction=core_course_create_courses" -d "moodlewsrestformat=json" \ + -d "courses[0][fullname]=DTA Test Course" -d "courses[0][shortname]=$COURSE_SHORTNAME" -d "courses[0][categoryid]=1") + +# --- THIS IS THE ROBUST ERROR CHECK --- +# First, check if the response is a Moodle exception object. +# The `jq -e` command sets an exit code, which works perfectly with `if`. +if echo "$COURSE_RESPONSE" | jq -e 'if type=="object" and .exception then true else false end' > /dev/null; then + echo "❌ Moodle API returned an exception during course creation:" + echo "$COURSE_RESPONSE" | jq . # Pretty-print the JSON error + exit 1 +fi + +# If not an exception, try to parse the ID. This handles both single-object and array responses. +NEW_COURSE_ID=$(echo "$COURSE_RESPONSE" | jq -r 'if type=="array" then .[0].id else .id end') + +if [ -z "$NEW_COURSE_ID" ] || [ "$NEW_COURSE_ID" == "null" ]; then + echo "❌ Failed to parse a valid Course ID from the API response." + echo "Full API Response:" + echo "$COURSE_RESPONSE" | jq . + exit 1 +fi +echo "✅ Successfully created course '$COURSE_SHORTNAME' with ID: $NEW_COURSE_ID" +# --- END OF ROBUST ERROR CHECK --- + +# --- Step 5: Enroll Admin User into the New Course --- +echo "🧑‍🏫 Step 5: Enrolling you as a teacher in the new course..." +# First, get the User ID for your admin user +AUTOMATION_USER_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_user WHERE username='$AUTOMATION_USER_NAME';") +# The roleid for a Teacher is typically 3 (editingteacher) or 4 (teacher). We'll use 3. +TEACHER_ROLE_ID=3 + +# Add the enrol_manual_enrol_users function to our service +docker exec -i "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -e "INSERT IGNORE INTO mdl_external_services_functions (externalserviceid, functionname) VALUES ((SELECT id FROM mdl_external_services WHERE shortname = '$CUSTOM_SERVICE_NAME'), 'enrol_manual_enrol_users');" +# Purge cache to recognize the new function +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" > /dev/null + +ENROL_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$TOKEN" -d "wsfunction=enrol_manual_enrol_users" -d "moodlewsrestformat=json" \ + -d "enrolments[0][roleid]=$TEACHER_ROLE_ID" \ + -d "enrolments[0][userid]=$AUTOMATION_USER_ID" \ + -d "enrolments[0][courseid]=$NEW_COURSE_ID") + +# A successful response is null, so we check for an exception instead. +if echo "$ENROL_RESPONSE" | jq -e 'if type=="object" and .exception then true else false end' > /dev/null; then + echo "❌ Moodle API returned an exception during user enrollment:" + echo "$ENROL_RESPONSE" | jq . + exit 1 +fi +echo "✅ Successfully enrolled you in the course." + +# --- Step 5: Upload Configuration File for DTA Plugin --- +echo "📤 Step 5: Uploading config file for DTA plugin..." +FILE_UPLOAD_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/upload.php" \ + -F "token=$TOKEN" -F "filearea=draft" -F "itemid=0" -F "file=@$DTA_CONFIG_FILE") +DTA_FILE_ITEMID=$(echo "$FILE_UPLOAD_RESPONSE" | jq -r 'if type=="array" then .[0].itemid else .itemid end') + +if [ -z "$DTA_FILE_ITEMID" ] || [ "$DTA_FILE_ITEMID" == "null" ]; then + echo "❌ Failed to upload DTA config file. Response:" + echo "$FILE_UPLOAD_RESPONSE" | jq . + exit 1 +fi +echo "✅ DTA config file uploaded. Received Item ID: $DTA_FILE_ITEMID" + +# --- Step 6: Create Assignment via Custom PHP Script --- +echo "📝 Step 6: Creating assignment with detailed error logging..." +PHP_ASSIGN_SCRIPT_PATH="/tmp/create_assignment.php" +LOCAL_PHP_ASSIGN_SCRIPT_PATH="/tmp/local_create_assignment.php" + +# --- This PHP script now has a try/catch block for detailed errors --- +cat > "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" <dirroot.'/course/modlib.php'); +global \$DB, \$USER; + +// programmatically "log in" as the admin user +// This gives file_save_draft_area_files the user context it needs. +\$adminusername = "$AUTOMATION_USER_NAME"; +\$adminuser = \$DB->get_record('user', ['username' => \$adminusername, 'deleted' => 0], '*', MUST_EXIST); +\core\session\manager::set_user(\$adminuser); +// The global $USER object is now populated. + +if (count(\$argv) < 3) { exit("Error: Course ID and DTA Item ID are required.\n"); } +\$courseid = (int)\$argv[1]; +\$dta_itemid = (int)\$argv[2]; + +\$course = \$DB->get_record('course', ['id' => \$courseid], '*', MUST_EXIST); + +\$data = new stdClass(); + +// --- Core Module Details --- +\$data->course = \$courseid; +\$data->modulename = 'assign'; +\$data->name = 'Fully Automated DTA Assignment'; +\$data->intro = 'This assignment was created by the final automation script.'; +\$data->introformat = FORMAT_HTML; +\$data->section = 1; +\$data->visible = 1; + +// --- Submission Settings --- +\$data->assignsubmission_dta_enabled = 1; +\$data->assignsubmission_file_enabled = 1; + +// --- THIS IS FIX #1: Correct property name for the DTA file manager --- +// This assumes the form element for your file manager is named 'package_filemanager' +// The final property name becomes assignsubmission_dta_package_filemanager +\$data->dta = 1; // Explicitly enable the dta group of settings +\$data->tests_draft_dta = \$dta_itemid; +\$data->assignsubmission_file_maxsizebytes = 0; +\$data->cmidnumber = ''; +\$data->assignsubmission_file_maxfiles = 20; + +\$data->assignsubmission_onlinetext_enabled = 0; +\$data->submissiondrafts = 0; // Final submissions cannot be edited +\$data->requiresubmissionstatement = 0; // The direct fix for the error +\$data->requireallteammemberssubmit = 0; +\$data->teamsubmission = 0; + +// --- Notification Settings --- +\$data->sendnotifications = 1; +\$data->sendlatenotifications = 0; +\$data->sendstudentnotifications = 1; + +// --- Grading Settings --- +\$data->grade = 100; // Max grade +\$data->blindmarking = 0; +\$data->markingworkflow = 0; +\$data->markingallocation = 0; +\$data->attemptreopenmethod = 'none'; + +// --- Date Settings --- +\$data->allowsubmissionsfromdate = time(); +\$data->duedate = time() + (7 * 24 * 60 * 60); // Due in 7 days +\$data->cutoffdate = 0; // No hard cutoff +\$data->gradingduedate = 0; + +// --- Moodle Magic - these fields are necessary but we don't need to change them --- +\$data->module = \$DB->get_field('modules', 'id', ['name' => 'assign'], MUST_EXIST); +\$data->timemodified = time(); +\$data->alwaysshowdescription = 0; +\$data->completionsubmit = 0; + +// --- DEBUG: Print the exact data object we are sending --- +fwrite(STDERR, "DEBUG: Data object being sent to add_moduleinfo():\n"); +fwrite(STDERR, print_r(\$data, true) . "\n"); + +try { + // Attempt to create the assignment + \$cm = add_moduleinfo(\$data, \$course); + fwrite(STDERR, "DEBUG: Returned object from add_moduleinfo():\n"); + fwrite(STDERR, print_r(\$cm, true) . "\n"); + if (!\$cm || !is_object(\$cm) || !\$cm->coursemodule) { + fwrite(STDERR, "Error: add_moduleinfo() returned invalid object\n"); + exit(1); + } + // Some versions: id = course module id, instance = assignment id + \$assignmentid = property_exists(\$cm, 'instance') ? \$cm->instance : null; + \$coursemoduleid = property_exists(\$cm, 'coursemodule') ? \$cm->coursemodule : null; + + if (!\$assignmentid) { + fwrite(STDERR, "Error: Could not extract assignment ID\n"); + exit(1); + } + + rebuild_course_cache(\$course->id, true); + + // ✅ Echo only the assignment ID (for Bash) + echo \$assignmentid . "\n"; + +} catch (Exception \$e) { + // If anything goes wrong, catch the exception and print all details + echo "\n!!!!!! SCRIPT FAILED: An Exception was caught! !!!!!!\n\n"; + echo "Error Type: " . get_class(\$e) . "\n"; + echo "Error Message: " . \$e->getMessage() . "\n\n"; + + // Moodle's database exceptions often have extra useful debug info + if (!empty(\$e->debuginfo)) { + echo "Moodle Debug Info:\n" . \$e->debuginfo . "\n\n"; + } + + echo "Stack Trace:\n" . \$e->getTraceAsString() . "\n"; + exit(1); // Exit with an error code +} +EOF + +docker cp "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" "$MOODLE_CONTAINER:$PHP_ASSIGN_SCRIPT_PATH" +NEW_ASSIGNMENT_ID=$(docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$PHP_ASSIGN_SCRIPT_PATH" "$NEW_COURSE_ID" "$DTA_FILE_ITEMID" 2>&1 | tee full_output.log | tail -n1) +docker exec "$MOODLE_CONTAINER" rm "$PHP_ASSIGN_SCRIPT_PATH" +rm "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" +echo " -> Created assignment with ID: $NEW_ASSIGNMENT_ID" +echo "✅ Test scenario created." + +# --- Step 8: Student Submission Workflow --- +echo "🧑‍🎓 Step 8: Beginning student submission workflow..." +STUDENT_USER_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_user WHERE username='$STUDENT_USER_NAME';") +# Role 5 is the standard 'Student' role +STUDENT_ROLE_ID=5 +STUDENT_ENROL_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$TOKEN" -d "wsfunction=enrol_manual_enrol_users" -d "moodlewsrestformat=json" \ + -d "enrolments[0][roleid]=$STUDENT_ROLE_ID" \ + -d "enrolments[0][userid]=$STUDENT_USER_ID" \ + -d "enrolments[0][courseid]=$NEW_COURSE_ID") +echo " -> Enrolled student in course." + +echo " -> Getting token for student..." +STUDENT_TOKEN_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/login/token.php" \ + --data-urlencode "username=$STUDENT_USER_NAME" --data-urlencode "password=$STUDENT_USER_PASS" --data-urlencode "service=$CUSTOM_SERVICE_NAME") +STUDENT_TOKEN=$(echo "$STUDENT_TOKEN_RESPONSE" | jq -r '.token') +if [ -z "$STUDENT_TOKEN" ] || [ "$STUDENT_TOKEN" == "null" ]; then echo "❌ Failed to get API token for student. Response: $STUDENT_TOKEN_RESPONSE" && exit 1; fi +echo " -> Successfully obtained token for student. $STUDENT_TOKEN" + +echo " -> Uploading student submission file..." +STUDENT_FILE_UPLOAD_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/upload.php" -F "token=$STUDENT_TOKEN" -F "filearea=draft" -F "itemid=0" -F "file=@$DTA_STUDENT_SUBMISSION_FILE") +STUDENT_FILE_ITEMID=$(echo "$STUDENT_FILE_UPLOAD_RESPONSE" | jq -r 'if type=="array" then .[0].itemid else .itemid end') +echo " -> Student file uploaded. Item ID: $STUDENT_FILE_ITEMID" +echo " -> Saving final submission..." + +# ============================================================================== +# CORRECTED SUBMISSION STEP (V12 - USER SPECIFIED) +# ============================================================================== +# We call the external backend directly and pass the student's wstoken as a form +# field, as required by this specific custom plugin's workflow. + +echo " -> Sending student file directly to the DTA Backend for testing..." +BACKEND_RESPONSE=$(curl --fail -sS -L -X POST "$BACKEND_URL/v1/task/$SUBMISSION_ID" \ + --form "taskFile=@\"$DTA_STUDENT_SUBMISSION_FILE\"" \ + --form "assignmentId=\"$NEW_ASSIGNMENT_ID\"" \ + --form "wstoken=\"$STUDENT_TOKEN\"") + +# Check if the backend returned a valid JSON test report +if ! echo "$BACKEND_RESPONSE" | jq -e '.results' > /dev/null; then + echo "❌ The DTA backend did not return a valid test report. Response:" + echo "$BACKEND_RESPONSE" + exit 1 +fi +echo "✅ File successfully sent to backend for grading." +echo " -> Backend Test Report Summary:" +echo "$BACKEND_RESPONSE" | jq '.results[] | .name + ": " + .failureType' + +# --- Step 9: Verify Grade and Feedback in Moodle Database --- +echo "🧐 Step 9: Verifying grade and feedback has been posted back to Moodle..." +echo " -> Waiting 15 seconds for DTA backend to make the callback..." +sleep 15 + +# Query the database directly to see if the summary record was created by the backend +SUMMARY_COUNT=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT COUNT(*) FROM mdl_assignsubmission_dta_summary WHERE submission_id = '$SUBMISSION_ID';") + +if [ "$SUMMARY_COUNT" -eq 0 ]; then + echo "❌ ERROR: No feedback summary found in the database for submission ID $SUBMISSION_ID." + echo "The backend callback likely failed. Check the backend logs:" + docker logs backend + exit 1 +fi + +echo "--- FINAL MOODLE DATABASE STATUS ---" +echo "✅ Found DTA summary record in the database." +echo "Detailed Test Results from mdl_assignsubmission_dta_result:" +docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -e "SELECT name, state, failure_type, failure_reason FROM mdl_assignsubmission_dta_result WHERE submission_id = '$SUBMISSION_ID';" + +# Final check on the core Moodle grading status +STATUS_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$TOKEN" \ + -d "wsfunction=mod_assign_get_submission_status" \ + -d "moodlewsrestformat=json" \ + -d "assignid=$NEW_ASSIGNMENT_ID" \ + -d "userid=$STUDENT_USER_ID") +GRADE_STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.lastattempt.gradingstatus') + +if [ "$GRADE_STATUS" == "graded" ]; then + echo "🎉 SUCCESS! The submission is marked as 'graded' in the Moodle GUI." +else + echo "⚠️ WARNING: The submission status is '$GRADE_STATUS', not 'graded'. The backend wrote to its custom tables but may have failed to update the core gradebook." + exit 1 +fi + + +echo "🎉🎉 Moodle automation complete. The environment is fully configured. 🎉🎉" -- GitLab From 84b4e0e48183582cb592033b62d7f99f5716ab55 Mon Sep 17 00:00:00 2001 From: 41abku1mst <41abku1mst@hft-stuttgart.de> Date: Thu, 24 Jul 2025 15:09:36 +0200 Subject: [PATCH 2/2] final moodle script with dta updated --- docker-compose.yml | 2 + dta.zip | Bin 45968 -> 48387 bytes full_output.log | 119 +++++++++++ student-dir.txt | 1 + test-moodle.sh | 518 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 640 insertions(+) create mode 100644 full_output.log create mode 100644 student-dir.txt create mode 100644 test-moodle.sh diff --git a/docker-compose.yml b/docker-compose.yml index 0082d78..828f0b3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,8 @@ services: container_name: backend image: hftstuttgart/cota-backend:latest user: "${EUSER}:${EGID}" + ports: # Add this section + - "8081:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock - '/tmp/dta-tests:/tmp/dta-tests' diff --git a/dta.zip b/dta.zip index c08bd0c7c7f91938434f5c0bfa89edd9c97f7ddf..aa1fa02bf36a4da6627733fd48885718556fec2e 100644 GIT binary patch delta 8438 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 Q)Q?-~qe-Pu`}FJo0hxoCLjV8( delta 6057 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 U7WSW4qWgVb;DJEVs6S8t51Dl@0ssI2 diff --git a/full_output.log b/full_output.log new file mode 100644 index 0000000..67a9e7d --- /dev/null +++ b/full_output.log @@ -0,0 +1,119 @@ +DEBUG: Data object being sent to add_moduleinfo(): +stdClass Object +( + [course] => 6 + [modulename] => assign + [name] => Fully Automated DTA Assignment + [intro] => This assignment was created by the final automation script. + [introformat] => 1 + [section] => 1 + [visible] => 1 + [assignsubmission_dta_enabled] => 1 + [assignsubmission_file_enabled] => 1 + [dta] => 1 + [tests_draft_dta] => 432698970 + [assignsubmission_file_maxsizebytes] => 0 + [cmidnumber] => + [assignsubmission_file_maxfiles] => 20 + [assignsubmission_onlinetext_enabled] => 0 + [submissiondrafts] => 0 + [requiresubmissionstatement] => 0 + [requireallteammemberssubmit] => 0 + [teamsubmission] => 0 + [sendnotifications] => 1 + [sendlatenotifications] => 0 + [sendstudentnotifications] => 1 + [grade] => 100 + [blindmarking] => 0 + [markingworkflow] => 0 + [markingallocation] => 0 + [attemptreopenmethod] => none + [allowsubmissionsfromdate] => 1753362310 + [duedate] => 1753967110 + [cutoffdate] => 0 + [gradingduedate] => 0 + [module] => 1 + [timemodified] => 1753362310 + [alwaysshowdescription] => 0 + [completionsubmit] => 0 +) + +DEBUG: Returned object from add_moduleinfo(): +stdClass Object +( + [course] => 6 + [modulename] => assign + [name] => Fully Automated DTA Assignment + [intro] => This assignment was created by the final automation script. + [introformat] => 1 + [section] => 1 + [visible] => 1 + [assignsubmission_dta_enabled] => 1 + [assignsubmission_file_enabled] => 1 + [dta] => 1 + [tests_draft_dta] => 432698970 + [assignsubmission_file_maxsizebytes] => 0 + [cmidnumber] => + [assignsubmission_file_maxfiles] => 20 + [assignsubmission_onlinetext_enabled] => 0 + [submissiondrafts] => 0 + [requiresubmissionstatement] => 0 + [requireallteammemberssubmit] => 0 + [teamsubmission] => 0 + [sendnotifications] => 1 + [sendlatenotifications] => 0 + [sendstudentnotifications] => 1 + [grade] => 100 + [blindmarking] => 0 + [markingworkflow] => 0 + [markingallocation] => 0 + [attemptreopenmethod] => none + [allowsubmissionsfromdate] => 1753362310 + [duedate] => 1753967110 + [cutoffdate] => 0 + [gradingduedate] => 0 + [module] => 1 + [timemodified] => 1753362310 + [alwaysshowdescription] => 0 + [completionsubmit] => 0 + [instance] => 3 + [coursemodule] => 10 + [groupingid] => 0 + [completion] => 0 + [completionview] => 0 + [completionexpected] => 0 + [completionpassgrade] => 0 + [completiongradeitemnumber] => + [conditiongradegroup] => Array + ( + ) + + [conditionfieldgroup] => Array + ( + ) + + [visibleoncoursepage] => 1 + [downloadcontent] => 1 + [groupmode] => 0 + [beforemod] => + [gradingman] => grading_manager Object + ( + [context:protected] => core\context\module Object + ( + [_id:protected] => 28 + [_contextlevel:protected] => 70 + [_instanceid:protected] => 10 + [_path:protected] => /1/3/27/28 + [_depth:protected] => 4 + [_locked:protected] => 0 + ) + + [component:protected] => mod_assign + [area:protected] => + [areacache:grading_manager:private] => + ) + + [showgradingmanagement] => +) + +3 diff --git a/student-dir.txt b/student-dir.txt new file mode 100644 index 0000000..50f42b1 --- /dev/null +++ b/student-dir.txt @@ -0,0 +1 @@ +dta::https://transfer.hft-stuttgart.de/gitlab/cota/examples/cota-submission-calc-jdk.git::none::none::/ diff --git a/test-moodle.sh b/test-moodle.sh new file mode 100644 index 0000000..d186b87 --- /dev/null +++ b/test-moodle.sh @@ -0,0 +1,518 @@ +#!/bin/bash +set -e + +# ============================================================================== +# DEFINITIVE Moodle CI/CD AUTOMATION SCRIPT V10 (ROBUST ERROR CHECKING) +# ============================================================================== + +# --- Configuration (UPDATE THESE!) --- +MOODLE_URL="http://localhost:8080" +BACKEND_URL="http://localhost:8081" +MOODLE_ADMIN_USER="user" +MOODLE_ADMIN_PASS="QAYwsx@12345" +# Automation User (Course creator) +AUTOMATION_USER_NAME="maaeruser" +AUTOMATION_USER_PASS="AutomationPassword123" +# Student User (created by script) +STUDENT_USER_NAME="tstudent" +STUDENT_USER_PASS="StudentPassword123" +DTA_CONFIG_FILE="teacher-dta-dir-test-runner-jdk21.txt" +DTA_STUDENT_SUBMISSION_FILE="student-dir.txt" +# --- Static Configuration --- +MOODLE_CONTAINER="moodle-web-official" +MOODLE_SYSTEM_USER="www-data" +MOODLE_ROOT_IN_CONTAINER="/var/www/html" +PLUGIN_ZIP_NAME="dta.zip" +CUSTOM_SERVICE_NAME="dta_automation_service" +DB_CONTAINER_NAME="moodle-db-official" +DB_ROOT_PASSWORD="verysecretpassword" +DB_NAME="moodle" + +# ============================================================================== +# SCRIPT EXECUTION +# ============================================================================== + +echo "🚀 Starting Definitive Moodle Automation Script..." + +# --- Pre-flight Checks & Plugin Install (Step 1) --- +if ! docker ps --format '{{.Names}}' | grep -qw "$MOODLE_CONTAINER"; then echo "❌ Moodle container not running." && exit 1; fi +if [ ! -f "$PLUGIN_ZIP_NAME" ]; then echo "❌ Plugin ZIP file not found." && exit 1; fi +if [ ! -f "$DTA_CONFIG_FILE" ]; then echo "❌ DTA config file not found." && exit 1; fi +echo "✅ Pre-flight checks passed." + +echo "📦 Step 1: Installing custom plugin..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" bash -c "set -e; mkdir -p '$MOODLE_ROOT_IN_CONTAINER/mod/assign/submission'; unzip -o '/tmp/$PLUGIN_ZIP_NAME' -d '$MOODLE_ROOT_IN_CONTAINER/mod/assign/submission'; php '$MOODLE_ROOT_IN_CONTAINER/admin/cli/upgrade.php' --non-interactive; php '$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php'" +echo "✅ Plugin installed." + +echo " -> Enabling Web Service " +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=enablemobilewebservice --set=1 + +# --- Step 2: Create and Configure a Custom Web Service --- +echo " -> Clearing cURL security settings..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=curlsecurityblockedhosts --set="" +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=curlsecurityallowedport --set="" + +echo " -> Enabling Web Service File Uploads (System-wide)..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=webserviceuploaddisabled --set=0 +# --- THIS IS THE FINAL FIX --- +# Enable username/password authentication for web service token generation +echo " -> Enabling password authentication for web services..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=enablewsauthpassword --set=1 + +# --- Step 4: Create Dedicated API Role and Automation User --- +echo "🧑‍💻 Step 4: Creating dedicated API Role and Automation User..." +# This is a more advanced version of the user creation script +PHP_USER_SCRIPT_PATH="/tmp/create_api_user.php" +LOCAL_PHP_USER_SCRIPT_PATH="/tmp/local_create_api_user.php" +cat > "$LOCAL_PHP_USER_SCRIPT_PATH" <libdir.'/clilib.php'); +require_once(\$CFG->dirroot.'/user/lib.php'); +global \$DB; + +\$username = \$argv[1]; \$password = \$argv[2]; +if (!\$DB->record_exists('user', ['username' => \$username])) { + \$user = new stdClass(); + \$user->username = \$username; \$user->firstname = 'API'; \$user->lastname = 'User'; + \$user->email = 'api.user@example.com'; \$user->confirmed = 1; + \$user->mnethostid = \$CFG->mnet_localhost_id; \$user->auth = 'manual'; + \$userid = user_create_user(\$user, false, false); + \$user = \$DB->get_record('user', ['id' => \$userid]); + \update_internal_user_password(\$user, \$password); + echo "Created user '\$username' with ID: \$userid\n"; + + // Create a new role for API users if it doesn't exist + \$rolename = 'API User'; + \$apicapability = 'moodle/webservice:createtoken'; + \$restapicapability = 'webservice/rest:use'; + \$coursecapability = 'moodle/course:create'; + \$enrolcapability = 'enrol/manual:enrol'; + if (!\$apirole = \$DB->get_record('role', ['shortname' => 'apiuser'])) { + \$apiroleid = create_role(\$rolename, 'apiuser', 'Role for API access'); + \$apirole = \$DB->get_record('role', ['id' => \$apiroleid]); + echo "Created role '\$rolename' with ID: \$apiroleid\n"; + } + // Ensure the capability is assigned to the role + \$context = \core\context\system::instance(); + assign_capability(\$apicapability, CAP_ALLOW, \$apirole->id, \$context->id); + assign_capability(\$restapicapability, CAP_ALLOW, \$apirole->id, \$context->id); + assign_capability(\$coursecapability, CAP_ALLOW, \$apirole->id, \$context->id); + assign_capability(\$enrolcapability, CAP_ALLOW, \$apirole->id, \$context->id); + echo "Assigned capability '\$apicapability' to role '\$rolename'\n"; + + // Assign our automation user to this new role at the system level + role_assign(\$apirole->id, \$userid, \$context->id); +// Get the IDs for the roles we need to assign +\$managerroleid = \$DB->get_field('role', 'id', ['shortname' => 'manager']); +\$coursecreatorroleid = \$DB->get_field('role', 'id', ['shortname' => 'coursecreator']); +\$systemcontext = \core\context\system::instance(); + +// Assign the 'Manager' and 'Course creator' roles to the new user at the system context +role_assign(\$managerroleid, \$userid, \$systemcontext->id); +role_assign(\$coursecreatorroleid, \$userid, \$systemcontext->id); + echo "Assigned user '\$username' to role '\$rolename'\n"; +} else { + \$userid = \$DB->get_field('user', 'id', ['username' => \$username]); + echo "User '\$username' already exists with ID: \$userid\n"; +} + +// Create Student User +\$student_username = \$argv[3]; \$student_password = \$argv[4]; +if (!\$DB->record_exists('user', ['username' => \$student_username])) { + \$student = new stdClass(); \$student->username = \$student_username; \$student->firstname = 'Test'; \$student->lastname = 'Student'; + \$student->email = 'student@example.com'; \$student->confirmed = 1; \$student->mnethostid = \$CFG->mnet_localhost_id; \$student->auth = 'manual'; + \$studentid = user_create_user(\$student, false, false); + \$student = \$DB->get_record('user', ['id' => \$studentid]); + \update_internal_user_password(\$student, \$student_password); + echo "Created user '\$student_username' with ID: \$studentid\n"; + + // Create a new role for API users if it doesn't exist + \$rolename = 'API Student user'; + \$apicapability = 'moodle/webservice:createtoken'; + \$restapicapability = 'webservice/rest:use'; + if (!\$apirole = \$DB->get_record('role', ['shortname' => 'apistudentuser'])) { + \$apiroleid = create_role(\$rolename, 'apistudentuser', 'Role for API access'); + \$apirole = \$DB->get_record('role', ['id' => \$apiroleid]); + echo "Created role '\$rolename' with ID: \$apiroleid\n"; + } + // Ensure the capability is assigned to the role + \$context = \core\context\system::instance(); + assign_capability(\$apicapability, CAP_ALLOW, \$apirole->id, \$context->id); + assign_capability(\$restapicapability, CAP_ALLOW, \$apirole->id, \$context->id); + echo "Assigned capability '\$apicapability' to role '\$rolename'\n"; + + // Assign our automation user to this new role at the system level + role_assign(\$apirole->id, \$studentid, \$context->id); + echo "Assigned user '\$username' to role '\$rolename'\n"; +} +EOF +docker cp "$LOCAL_PHP_USER_SCRIPT_PATH" "$MOODLE_CONTAINER:$PHP_USER_SCRIPT_PATH" +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$PHP_USER_SCRIPT_PATH" "$AUTOMATION_USER_NAME" "$AUTOMATION_USER_PASS" "$STUDENT_USER_NAME" "$STUDENT_USER_PASS" +docker exec "$MOODLE_CONTAINER" rm "$PHP_USER_SCRIPT_PATH" +rm "$LOCAL_PHP_USER_SCRIPT_PATH" +echo "✅ Automation and student users are configured." + +echo "⚙️ Step 2: Creating and configuring a dedicated web service..." +docker exec -i "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" < Purging Moodle caches.." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" +sleep 15 + +# --- Step 6: Assign System Roles via API (The Definitive Fix) --- +echo "🛡️ Step 6: Assigning System Roles to Automation User..." +# We use the main site admin's token for this one-time setup task, as it's guaranteed to have permissions. +ADMIN_SETUP_TOKEN_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/login/token.php" --data-urlencode "username=$MOODLE_ADMIN_USER" --data-urlencode "password=$MOODLE_ADMIN_PASS" --data-urlencode "service=moodle_mobile_app") +ADMIN_SETUP_TOKEN=$(echo "$ADMIN_SETUP_TOKEN_RESPONSE" | jq -r '.token') + +# Get IDs needed for role assignment +AUTOMATION_USER_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_user WHERE username='$AUTOMATION_USER_NAME';") +MANAGER_ROLE_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_role WHERE shortname='manager';") +COURSE_CREATOR_ROLE_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_role WHERE shortname='coursecreator';") +SYSTEM_CONTEXT_ID=10 # The ID for the System context is always 1 + +# Assign Manager and Course Creator roles to the automation user +curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$ADMIN_SETUP_TOKEN" -d "wsfunction=core_role_assign_roles" -d "moodlewsrestformat=json" \ + -d "assignments[0][roleid]=$MANAGER_ROLE_ID" -d "assignments[0][userid]=$AUTOMATION_USER_ID" -d "assignments[0][contextid]=$SYSTEM_CONTEXT_ID" \ + -d "assignments[1][roleid]=$COURSE_CREATOR_ROLE_ID" -d "assignments[1][userid]=$AUTOMATION_USER_ID" -d "assignments[1][contextid]=$SYSTEM_CONTEXT_ID" > /dev/null +echo "✅ Assigned System Roles to Automation User." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" +sleep 15 + +# --- THIS IS THE CRITICAL FIX --- +echo " -> Purging Moodle caches to apply service changes..." +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" +echo "✅ Caches purged." +sleep 5 + + +echo "🔐 Step 3: Obtaining API Token...$AUTOMATION_USER_NAME..$AUTOMATION_USER_PASS" +TOKEN_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/login/token.php" \ + --data-urlencode "username=$AUTOMATION_USER_NAME" \ + --data-urlencode "password=$AUTOMATION_USER_PASS" \ + --data-urlencode "service=$CUSTOM_SERVICE_NAME") + +# Check for a Moodle error in the response FIRST +if echo "$TOKEN_RESPONSE" | jq -e 'if type=="object" and .error then true else false end' > /dev/null; then + echo "❌ Moodle API returned an error when requesting a token:" + echo "$TOKEN_RESPONSE" | jq . + exit 1 +fi + +TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.token') +echo "✅ Successfully obtained API token. $TOKEN" + + +# --- Step 4: Create a Course --- +echo "📚 Step 4: Creating a new course..." +COURSE_SHORTNAME="DTA-Course-$(date +%s)" +COURSE_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$TOKEN" -d "wsfunction=core_course_create_courses" -d "moodlewsrestformat=json" \ + -d "courses[0][fullname]=DTA Test Course" -d "courses[0][shortname]=$COURSE_SHORTNAME" -d "courses[0][categoryid]=1") + +# --- THIS IS THE ROBUST ERROR CHECK --- +# First, check if the response is a Moodle exception object. +# The `jq -e` command sets an exit code, which works perfectly with `if`. +if echo "$COURSE_RESPONSE" | jq -e 'if type=="object" and .exception then true else false end' > /dev/null; then + echo "❌ Moodle API returned an exception during course creation:" + echo "$COURSE_RESPONSE" | jq . # Pretty-print the JSON error + exit 1 +fi + +# If not an exception, try to parse the ID. This handles both single-object and array responses. +NEW_COURSE_ID=$(echo "$COURSE_RESPONSE" | jq -r 'if type=="array" then .[0].id else .id end') + +if [ -z "$NEW_COURSE_ID" ] || [ "$NEW_COURSE_ID" == "null" ]; then + echo "❌ Failed to parse a valid Course ID from the API response." + echo "Full API Response:" + echo "$COURSE_RESPONSE" | jq . + exit 1 +fi +echo "✅ Successfully created course '$COURSE_SHORTNAME' with ID: $NEW_COURSE_ID" +# --- END OF ROBUST ERROR CHECK --- + +# --- Step 5: Enroll Admin User into the New Course --- +echo "🧑‍🏫 Step 5: Enrolling you as a teacher in the new course..." +# First, get the User ID for your admin user +AUTOMATION_USER_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_user WHERE username='$AUTOMATION_USER_NAME';") +# The roleid for a Teacher is typically 3 (editingteacher) or 4 (teacher). We'll use 3. +TEACHER_ROLE_ID=3 + +# Add the enrol_manual_enrol_users function to our service +docker exec -i "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -e "INSERT IGNORE INTO mdl_external_services_functions (externalserviceid, functionname) VALUES ((SELECT id FROM mdl_external_services WHERE shortname = '$CUSTOM_SERVICE_NAME'), 'enrol_manual_enrol_users');" +# Purge cache to recognize the new function +docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" > /dev/null + +ENROL_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$TOKEN" -d "wsfunction=enrol_manual_enrol_users" -d "moodlewsrestformat=json" \ + -d "enrolments[0][roleid]=$TEACHER_ROLE_ID" \ + -d "enrolments[0][userid]=$AUTOMATION_USER_ID" \ + -d "enrolments[0][courseid]=$NEW_COURSE_ID") + +# A successful response is null, so we check for an exception instead. +if echo "$ENROL_RESPONSE" | jq -e 'if type=="object" and .exception then true else false end' > /dev/null; then + echo "❌ Moodle API returned an exception during user enrollment:" + echo "$ENROL_RESPONSE" | jq . + exit 1 +fi +echo "✅ Successfully enrolled you in the course." + +# --- Step 5: Upload Configuration File for DTA Plugin --- +echo "📤 Step 5: Uploading config file for DTA plugin..." +FILE_UPLOAD_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/upload.php" \ + -F "token=$TOKEN" -F "filearea=draft" -F "itemid=0" -F "file=@$DTA_CONFIG_FILE") +DTA_FILE_ITEMID=$(echo "$FILE_UPLOAD_RESPONSE" | jq -r 'if type=="array" then .[0].itemid else .itemid end') + +if [ -z "$DTA_FILE_ITEMID" ] || [ "$DTA_FILE_ITEMID" == "null" ]; then + echo "❌ Failed to upload DTA config file. Response:" + echo "$FILE_UPLOAD_RESPONSE" | jq . + exit 1 +fi +echo "✅ DTA config file uploaded. Received Item ID: $DTA_FILE_ITEMID" + +# --- Step 6: Create Assignment via Custom PHP Script --- +echo "📝 Step 6: Creating assignment with detailed error logging..." +PHP_ASSIGN_SCRIPT_PATH="/tmp/create_assignment.php" +LOCAL_PHP_ASSIGN_SCRIPT_PATH="/tmp/local_create_assignment.php" + +# --- This PHP script now has a try/catch block for detailed errors --- +cat > "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" <dirroot.'/course/modlib.php'); +global \$DB, \$USER; + +// programmatically "log in" as the admin user +// This gives file_save_draft_area_files the user context it needs. +\$adminusername = "$AUTOMATION_USER_NAME"; +\$adminuser = \$DB->get_record('user', ['username' => \$adminusername, 'deleted' => 0], '*', MUST_EXIST); +\core\session\manager::set_user(\$adminuser); +// The global $USER object is now populated. + +if (count(\$argv) < 3) { exit("Error: Course ID and DTA Item ID are required.\n"); } +\$courseid = (int)\$argv[1]; +\$dta_itemid = (int)\$argv[2]; + +\$course = \$DB->get_record('course', ['id' => \$courseid], '*', MUST_EXIST); + +\$data = new stdClass(); + +// --- Core Module Details --- +\$data->course = \$courseid; +\$data->modulename = 'assign'; +\$data->name = 'Fully Automated DTA Assignment'; +\$data->intro = 'This assignment was created by the final automation script.'; +\$data->introformat = FORMAT_HTML; +\$data->section = 1; +\$data->visible = 1; + +// --- Submission Settings --- +\$data->assignsubmission_dta_enabled = 1; +\$data->assignsubmission_file_enabled = 1; + +// --- THIS IS FIX #1: Correct property name for the DTA file manager --- +// This assumes the form element for your file manager is named 'package_filemanager' +// The final property name becomes assignsubmission_dta_package_filemanager +\$data->dta = 1; // Explicitly enable the dta group of settings +\$data->tests_draft_dta = \$dta_itemid; +\$data->assignsubmission_file_maxsizebytes = 0; +\$data->cmidnumber = ''; +\$data->assignsubmission_file_maxfiles = 20; + +\$data->assignsubmission_onlinetext_enabled = 0; +\$data->submissiondrafts = 0; // Final submissions cannot be edited +\$data->requiresubmissionstatement = 0; // The direct fix for the error +\$data->requireallteammemberssubmit = 0; +\$data->teamsubmission = 0; + +// --- Notification Settings --- +\$data->sendnotifications = 1; +\$data->sendlatenotifications = 0; +\$data->sendstudentnotifications = 1; + +// --- Grading Settings --- +\$data->grade = 100; // Max grade +\$data->blindmarking = 0; +\$data->markingworkflow = 0; +\$data->markingallocation = 0; +\$data->attemptreopenmethod = 'none'; + +// --- Date Settings --- +\$data->allowsubmissionsfromdate = time(); +\$data->duedate = time() + (7 * 24 * 60 * 60); // Due in 7 days +\$data->cutoffdate = 0; // No hard cutoff +\$data->gradingduedate = 0; + +// --- Moodle Magic - these fields are necessary but we don't need to change them --- +\$data->module = \$DB->get_field('modules', 'id', ['name' => 'assign'], MUST_EXIST); +\$data->timemodified = time(); +\$data->alwaysshowdescription = 0; +\$data->completionsubmit = 0; + +// --- DEBUG: Print the exact data object we are sending --- +fwrite(STDERR, "DEBUG: Data object being sent to add_moduleinfo():\n"); +fwrite(STDERR, print_r(\$data, true) . "\n"); + +try { + // Attempt to create the assignment + \$cm = add_moduleinfo(\$data, \$course); + fwrite(STDERR, "DEBUG: Returned object from add_moduleinfo():\n"); + fwrite(STDERR, print_r(\$cm, true) . "\n"); + if (!\$cm || !is_object(\$cm) || !\$cm->coursemodule) { + fwrite(STDERR, "Error: add_moduleinfo() returned invalid object\n"); + exit(1); + } + // Some versions: id = course module id, instance = assignment id + \$assignmentid = property_exists(\$cm, 'instance') ? \$cm->instance : null; + \$coursemoduleid = property_exists(\$cm, 'coursemodule') ? \$cm->coursemodule : null; + + if (!\$assignmentid) { + fwrite(STDERR, "Error: Could not extract assignment ID\n"); + exit(1); + } + + rebuild_course_cache(\$course->id, true); + + // ✅ Echo only the assignment ID (for Bash) + echo \$assignmentid . "\n"; + +} catch (Exception \$e) { + // If anything goes wrong, catch the exception and print all details + echo "\n!!!!!! SCRIPT FAILED: An Exception was caught! !!!!!!\n\n"; + echo "Error Type: " . get_class(\$e) . "\n"; + echo "Error Message: " . \$e->getMessage() . "\n\n"; + + // Moodle's database exceptions often have extra useful debug info + if (!empty(\$e->debuginfo)) { + echo "Moodle Debug Info:\n" . \$e->debuginfo . "\n\n"; + } + + echo "Stack Trace:\n" . \$e->getTraceAsString() . "\n"; + exit(1); // Exit with an error code +} +EOF + +docker cp "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" "$MOODLE_CONTAINER:$PHP_ASSIGN_SCRIPT_PATH" +NEW_ASSIGNMENT_ID=$(docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$PHP_ASSIGN_SCRIPT_PATH" "$NEW_COURSE_ID" "$DTA_FILE_ITEMID" 2>&1 | tee full_output.log | tail -n1) +docker exec "$MOODLE_CONTAINER" rm "$PHP_ASSIGN_SCRIPT_PATH" +rm "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" +echo " -> Created assignment with ID: $NEW_ASSIGNMENT_ID" +echo "✅ Test scenario created." + +# --- Step 8: Student Submission Workflow --- +echo "🧑‍🎓 Step 8: Beginning student submission workflow..." +STUDENT_USER_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_user WHERE username='$STUDENT_USER_NAME';") +# Role 5 is the standard 'Student' role +STUDENT_ROLE_ID=5 +STUDENT_ENROL_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$TOKEN" -d "wsfunction=enrol_manual_enrol_users" -d "moodlewsrestformat=json" \ + -d "enrolments[0][roleid]=$STUDENT_ROLE_ID" \ + -d "enrolments[0][userid]=$STUDENT_USER_ID" \ + -d "enrolments[0][courseid]=$NEW_COURSE_ID") +echo " -> Enrolled student in course." + +echo " -> Getting token for student..." +STUDENT_TOKEN_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/login/token.php" \ + --data-urlencode "username=$STUDENT_USER_NAME" --data-urlencode "password=$STUDENT_USER_PASS" --data-urlencode "service=$CUSTOM_SERVICE_NAME") +STUDENT_TOKEN=$(echo "$STUDENT_TOKEN_RESPONSE" | jq -r '.token') +if [ -z "$STUDENT_TOKEN" ] || [ "$STUDENT_TOKEN" == "null" ]; then echo "❌ Failed to get API token for student. Response: $STUDENT_TOKEN_RESPONSE" && exit 1; fi +echo " -> Successfully obtained token for student. $STUDENT_TOKEN" + +echo " -> Uploading student submission file..." +STUDENT_FILE_UPLOAD_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/upload.php" -F "token=$STUDENT_TOKEN" -F "filearea=draft" -F "itemid=0" -F "file=@$DTA_STUDENT_SUBMISSION_FILE") +STUDENT_FILE_ITEMID=$(echo "$STUDENT_FILE_UPLOAD_RESPONSE" | jq -r 'if type=="array" then .[0].itemid else .itemid end') +echo " -> Student file uploaded. Item ID: $STUDENT_FILE_ITEMID" + + +echo " -> Saving final submission using our custom web service..." +SAVE_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$STUDENT_TOKEN" \ + -d "wsfunction=assignsubmission_dta_save_submission" \ + -d "moodlewsrestformat=json" \ + -d "assignmentid=$NEW_ASSIGNMENT_ID" \ + -d "userid=$STUDENT_USER_ID" \ + -d "itemid=$STUDENT_FILE_ITEMID") + +if echo "$SAVE_RESPONSE" | jq -e 'if type=="object" and .exception then true else false end' > /dev/null; then + echo "❌ Moodle API returned an exception during submission save:" + echo "$SAVE_RESPONSE" | jq . + exit 1 +fi +echo "✅ Submission successfully sent to Moodle, which has triggered the backend grading." + +# --- Step 7: Verify Grade and Feedback in Moodle Database --- +echo "🧐 Step 7: Verifying grade and feedback has been posted back to Moodle..." +echo " -> Waiting 20 seconds for DTA backend to make the callback..." +sleep 20 + +SUBMISSION_ID=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT id FROM mdl_assign_submission WHERE assignment = '$NEW_ASSIGNMENT_ID' AND userid = '$STUDENT_USER_ID';") +SUMMARY_COUNT=$(docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -se "SELECT COUNT(*) FROM mdl_assignsubmission_dta_summary WHERE submission_id = '$SUBMISSION_ID';") + +if [ "$SUMMARY_COUNT" -eq 0 ]; then + echo "❌ ERROR: No feedback summary found in the database for submission ID $SUBMISSION_ID." + echo "The backend callback likely failed. Check the backend logs:" + docker logs backend + exit 1 +fi + +echo "--- FINAL MOODLE DATABASE STATUS ---" +echo "✅ Found DTA summary record in the database." +echo "Detailed Test Results from mdl_assignsubmission_dta_result:" +docker exec "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" -e "SELECT name, state, failure_type, failure_reason FROM mdl_assignsubmission_dta_result WHERE submission_id = '$SUBMISSION_ID';" + +STATUS_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ + -d "wstoken=$TOKEN" \ + -d "wsfunction=mod_assign_get_submission_status" \ + -d "moodlewsrestformat=json" \ + -d "assignid=$NEW_ASSIGNMENT_ID" \ + -d "userid=$STUDENT_USER_ID") +GRADE_STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.lastattempt.gradingstatus') + +if [ "$GRADE_STATUS" == "graded" ]; then + echo "🎉 SUCCESS! The submission is marked as 'graded' in the Moodle GUI." +else + echo "⚠️ WARNING: The submission status is '$GRADE_STATUS', not 'graded'. The backend wrote to its custom tables but may have failed to update the core gradebook." + exit 1 +fi + + +echo "🎉🎉 Moodle automation complete. The environment is fully configured. 🎉🎉" \ No newline at end of file -- GitLab