Commit e2a6616d authored by Abhishek's avatar Abhishek
Browse files

Merge branch 'new_moodle_file_abhishek' into 'master'

installation part added to docker-entrypoint.sh file

See merge request !6
parents 02ab583d 5e88c187
Pipeline #11791 skipped with stage
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"]
......@@ -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
......
#!/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 "$@"
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
#!/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" <<EOF
<?php
define('CLI_SCRIPT', true);
require('/var/www/html/config.php');
require_once(\$CFG->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" <<EOF
-- Enable Web Services globally in Moodle's config
INSERT INTO mdl_config (name, value) VALUES ('enablewebservices', '1') ON DUPLICATE KEY UPDATE value = '1';
INSERT INTO mdl_config (name, value) VALUES ('webserviceprotocols', 'rest') ON DUPLICATE KEY UPDATE value = 'rest';
# --- End of addition --
-- Create our custom service, now with a requiredcapability set for robustness
INSERT IGNORE INTO mdl_external_services (name, shortname, enabled, requiredcapability, restrictedusers, uploadfiles, downloadfiles)
VALUES ('DTA Automation Service', '$CUSTOM_SERVICE_NAME', 1, '', 0, 1, 1); -- <-- The capability fix
-- Get the ID of our custom service
SET @serviceid = (SELECT id FROM mdl_external_services WHERE shortname = '$CUSTOM_SERVICE_NAME');
-- Add the required functions to OUR service
INSERT IGNORE INTO mdl_external_services_functions (externalserviceid, functionname) VALUES
(@serviceid, 'core_user_create_users'),
(@serviceid, 'core_course_create_courses'),
(@serviceid, 'enrol_manual_enrol_users'),
(@serviceid, 'core_files_upload'),
(@serviceid, 'mod_assign_save_submission'),
(@serviceid, 'mod_assign_get_submission_status'),
(@serviceid, 'mod_assign_get_submissions'),
(@serviceid, 'mod_assign_save_grade'),
(@serviceid, 'assignsubmission_dta_save_submission');
# -- *** THIS IS THE CRITICAL FIX ***
# -- Authorize the admin user to use this service.
SET @automationuserid = (SELECT id FROM mdl_user WHERE username = '$AUTOMATION_USER_NAME' AND deleted = 0 ORDER BY id ASC LIMIT 1);
INSERT IGNORE INTO mdl_external_services_users (externalserviceid, userid, iprestriction, validuntil, timecreated)
VALUES (@serviceid, @automationuserid , NULL, NULL, UNIX_TIMESTAMP());
-- Also authorize the student user to use the service to get a token
SET @studentid = (SELECT id FROM mdl_user WHERE username = '$STUDENT_USER_NAME');
INSERT IGNORE INTO mdl_external_services_users (externalserviceid, userid) VALUES (@serviceid, @studentid);
EOF
echo "✅ Custom web service created and user authorized."
echo " -> 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" <<EOF
<?php
define('CLI_SCRIPT', true);
require('/var/www/html/config.php');
require_once(\$CFG->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" <<EOF
<?php
define('CLI_SCRIPT', true);
require('/var/www/html/config.php');
require_once(\$CFG->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" <<EOF
-- Enable Web Services globally in Moodle's config
INSERT INTO mdl_config (name, value) VALUES ('enablewebservices', '1') ON DUPLICATE KEY UPDATE value = '1';
INSERT INTO mdl_config (name, value) VALUES ('webserviceprotocols', 'rest') ON DUPLICATE KEY UPDATE value = 'rest';
# --- End of addition --
-- Create our custom service, now with a requiredcapability set for robustness
INSERT IGNORE INTO mdl_external_services (name, shortname, enabled, requiredcapability, restrictedusers, uploadfiles, downloadfiles)
VALUES ('DTA Automation Service', '$CUSTOM_SERVICE_NAME', 1, '', 0, 1, 1); -- <-- The capability fix
-- Get the ID of our custom service
SET @serviceid = (SELECT id FROM mdl_external_services WHERE shortname = '$CUSTOM_SERVICE_NAME');
-- Add the required functions to OUR service
INSERT IGNORE INTO mdl_external_services_functions (externalserviceid, functionname) VALUES
(@serviceid, 'core_user_create_users'),
(@serviceid, 'core_course_create_courses'),
(@serviceid, 'enrol_manual_enrol_users'),
(@serviceid, 'core_files_upload'),
(@serviceid, 'mod_assign_save_submission'),
(@serviceid, 'mod_assign_get_submission_status'),
(@serviceid, 'mod_assign_get_submissions'),
(@serviceid, 'mod_assign_save_grade'),
(@serviceid, 'assignsubmission_dta_save_submission');
# -- *** THIS IS THE CRITICAL FIX ***
# -- Authorize the admin user to use this service.
SET @automationuserid = (SELECT id FROM mdl_user WHERE username = '$AUTOMATION_USER_NAME' AND deleted = 0 ORDER BY id ASC LIMIT 1);
INSERT IGNORE INTO mdl_external_services_users (externalserviceid, userid, iprestriction, validuntil, timecreated)
VALUES (@serviceid, @automationuserid , NULL, NULL, UNIX_TIMESTAMP());
-- Also authorize the student user to use the service to get a token
SET @studentid = (SELECT id FROM mdl_user WHERE username = '$STUDENT_USER_NAME');
INSERT IGNORE INTO mdl_external_services_users (externalserviceid, userid) VALUES (@serviceid, @studentid);
EOF
echo "✅ Custom web service created and user authorized."
echo " -> 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" <<EOF
<?php
define('CLI_SCRIPT', true);
require('/var/www/html/config.php');
require_once(\$CFG->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. 🎉🎉"
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment