diff --git a/docker-compose.yml b/docker-compose.yml index 0082d7804016f591ad75184294ec32b90ea51810..828f0b310bbb84f6449313ccaace3e4fcb19119c 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 Binary files a/dta.zip and b/dta.zip differ diff --git a/full_output.log b/full_output.log new file mode 100644 index 0000000000000000000000000000000000000000..67a9e7ddf447266938097a0d9823788963e71ac7 --- /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/newupdatedFile/Dockerfile b/newupdatedFile/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..769692cacaafff370cf1d82b518c74135b7acd3e --- /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 0000000000000000000000000000000000000000..828f0b310bbb84f6449313ccaace3e4fcb19119c --- /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 0000000000000000000000000000000000000000..49e734c7b77a07fadf4dc11fb19c7e095ad9f34d --- /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 Binary files /dev/null and b/newupdatedFile/dta.zip differ diff --git a/newupdatedFile/full_output.log b/newupdatedFile/full_output.log new file mode 100644 index 0000000000000000000000000000000000000000..5cf34a51f1b64f9e2046357be328ef23a82875df --- /dev/null +++ b/newupdatedFile/full_output.log @@ -0,0 +1,119 @@ +DEBUG: Data object being sent to 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 +) + +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 0000000000000000000000000000000000000000..cc0cce84fe925de7cd0e8cb446e2adbd59e172e2 --- /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 0000000000000000000000000000000000000000..7d7cb391ec0adbe3d85d0229c2eb75998e69490b --- /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 0000000000000000000000000000000000000000..50f42b17fa8843ac424912a4f87c1fd0359107ca --- /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 0000000000000000000000000000000000000000..f5c9b2fa0e96d4ce2a5ed278de1938068677dc83 --- /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 0000000000000000000000000000000000000000..8e94cd020e4d6d15c16fe6cbd8033d6060bd3fe9 --- /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. 🎉🎉" diff --git a/student-dir.txt b/student-dir.txt new file mode 100644 index 0000000000000000000000000000000000000000..50f42b17fa8843ac424912a4f87c1fd0359107ca --- /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 0000000000000000000000000000000000000000..d186b87ce64b27246b37cb747f982176c429673b --- /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