From 5e88c1871bfc89f6e567fa498ac02f7703d57101 Mon Sep 17 00:00:00 2001 From: 41abku1mst <41abku1mst@hft-stuttgart.de> Date: Tue, 26 Aug 2025 00:58:02 +0200 Subject: [PATCH] installation part added to docker-entrypoint.sh file --- Dockerfile | 16 +- docker-compose.yml | 4 + docker-entrypoint.sh | 55 ++- full_output.log | 32 +- test-moodle.sh | 1036 +++++++++++++++++++++--------------------- 5 files changed, 604 insertions(+), 539 deletions(-) diff --git a/Dockerfile b/Dockerfile index 769692c..3d9a003 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM debian:bookworm # Install Apache, PHP, and other required extensions +# ADDED: sudo and mariadb-client for the entrypoint script RUN apt-get update && apt-get install -y \ apache2 \ php \ @@ -16,10 +17,11 @@ RUN apt-get update && apt-get install -y \ php-mbstring \ unzip \ git \ + sudo \ + mariadb-client \ && 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 @@ -35,7 +37,13 @@ RUN mkdir /var/www/moodledata && chown -R www-data:www-data /var/www/moodledata 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"] +# --- UPDATED SECTION --- +# Copy the entrypoint script to a standard location and make it executable +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +# Set the entrypoint +ENTRYPOINT ["docker-entrypoint.sh"] + +# command to start Apache on Debian CMD ["apache2ctl", "-D", "FOREGROUND"] diff --git a/docker-compose.yml b/docker-compose.yml index 828f0b3..73a265e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,10 @@ services: depends_on: - db restart: unless-stopped + environment: + - MOODLE_WWWROOT=http://localhost:8080 + - MOODLE_ADMIN_USER=user + - MOODLE_ADMIN_PASS=QAYwsx@12345 backend: container_name: backend diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 49e734c..0b1090a 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,7 +1,60 @@ #!/bin/bash set -e -# Apache gets grumpy about PID files pre-existing +# Define the path to Moodle's config file +MOODLE_CONFIG_FILE="/var/www/html/config.php" + +# Check if Moodle is already installed by looking for config.php +if [ ! -f "$MOODLE_CONFIG_FILE" ]; then + echo " Moodle config.php not found. Starting first-time installation..." + + # Wait for the database to be ready + echo "-> Waiting for database connection at 'db'..." + while ! mysqladmin ping -h"db" --silent; do + sleep 1 + done + echo "-> Database is ready." + + # --- FIX: Read wwwroot from an environment variable --- + # This makes the script flexible for any environment (local or CI/CD). + if [ -z "$MOODLE_WWWROOT" ]; then + echo "❌ ERROR: MOODLE_WWWROOT environment variable is not set." + exit 1 + fi + + # Run the Moodle installation command as the www-data user + # It gets admin credentials and wwwroot from environment variables. + sudo -u www-data php /var/www/html/admin/cli/install.php \ + --lang=en \ + --wwwroot="$MOODLE_WWWROOT" \ + --dataroot="/var/www/moodledata" \ + --dbtype="mariadb" \ + --dbhost="db" \ + --dbname="moodle" \ + --dbuser="moodleuser" \ + --dbpass="moodlepassword" \ + --fullname="CI Test Site" \ + --shortname="CITest" \ + --adminuser="$MOODLE_ADMIN_USER" \ + --adminpass="$MOODLE_ADMIN_PASS" \ + --adminemail="admin@example.com" \ + --non-interactive \ + --agree-license + + echo "-> Moodle installation complete." + + echo "-> Setting no-reply email address..." + sudo -u www-data php /var/www/html/admin/cli/cfg.php --name=noreplyaddress --set="noreply@example.com" + + echo "-> Installation finished. Moodle is ready." +else + echo " Moodle config.php found. Skipping installation." +fi + +# Clean up any old PID files to ensure Apache starts correctly. +echo "-> Cleaning up pre-existing PID files..." rm -f /var/run/apache2/apache2.pid +# This is the final command that starts the main process of the container (Apache). +echo "-> Starting Apache..." exec "$@" diff --git a/full_output.log b/full_output.log index 67a9e7d..07daeb5 100644 --- a/full_output.log +++ b/full_output.log @@ -1,7 +1,7 @@ DEBUG: Data object being sent to add_moduleinfo(): stdClass Object ( - [course] => 6 + [course] => 2 [modulename] => assign [name] => Fully Automated DTA Assignment [intro] => This assignment was created by the final automation script. @@ -11,7 +11,7 @@ stdClass Object [assignsubmission_dta_enabled] => 1 [assignsubmission_file_enabled] => 1 [dta] => 1 - [tests_draft_dta] => 432698970 + [tests_draft_dta] => 879454372 [assignsubmission_file_maxsizebytes] => 0 [cmidnumber] => [assignsubmission_file_maxfiles] => 20 @@ -28,12 +28,12 @@ stdClass Object [markingworkflow] => 0 [markingallocation] => 0 [attemptreopenmethod] => none - [allowsubmissionsfromdate] => 1753362310 - [duedate] => 1753967110 + [allowsubmissionsfromdate] => 1756162043 + [duedate] => 1756766843 [cutoffdate] => 0 [gradingduedate] => 0 [module] => 1 - [timemodified] => 1753362310 + [timemodified] => 1756162043 [alwaysshowdescription] => 0 [completionsubmit] => 0 ) @@ -41,7 +41,7 @@ stdClass Object DEBUG: Returned object from add_moduleinfo(): stdClass Object ( - [course] => 6 + [course] => 2 [modulename] => assign [name] => Fully Automated DTA Assignment [intro] => This assignment was created by the final automation script. @@ -51,7 +51,7 @@ stdClass Object [assignsubmission_dta_enabled] => 1 [assignsubmission_file_enabled] => 1 [dta] => 1 - [tests_draft_dta] => 432698970 + [tests_draft_dta] => 879454372 [assignsubmission_file_maxsizebytes] => 0 [cmidnumber] => [assignsubmission_file_maxfiles] => 20 @@ -68,16 +68,16 @@ stdClass Object [markingworkflow] => 0 [markingallocation] => 0 [attemptreopenmethod] => none - [allowsubmissionsfromdate] => 1753362310 - [duedate] => 1753967110 + [allowsubmissionsfromdate] => 1756162043 + [duedate] => 1756766843 [cutoffdate] => 0 [gradingduedate] => 0 [module] => 1 - [timemodified] => 1753362310 + [timemodified] => 1756162043 [alwaysshowdescription] => 0 [completionsubmit] => 0 - [instance] => 3 - [coursemodule] => 10 + [instance] => 1 + [coursemodule] => 2 [groupingid] => 0 [completion] => 0 [completionview] => 0 @@ -100,10 +100,10 @@ stdClass Object ( [context:protected] => core\context\module Object ( - [_id:protected] => 28 + [_id:protected] => 14 [_contextlevel:protected] => 70 - [_instanceid:protected] => 10 - [_path:protected] => /1/3/27/28 + [_instanceid:protected] => 2 + [_path:protected] => /1/3/13/14 [_depth:protected] => 4 [_locked:protected] => 0 ) @@ -116,4 +116,4 @@ stdClass Object [showgradingmanagement] => ) -3 +1 diff --git a/test-moodle.sh b/test-moodle.sh index d186b87..7bcfd47 100644 --- a/test-moodle.sh +++ b/test-moodle.sh @@ -1,518 +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 +#!/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. 🎉🎉" -- GitLab