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 FROM debian:bookworm
# Install Apache, PHP, and other required extensions # Install Apache, PHP, and other required extensions
# ADDED: sudo and mariadb-client for the entrypoint script
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
apache2 \ apache2 \
php \ php \
...@@ -16,10 +17,11 @@ RUN apt-get update && apt-get install -y \ ...@@ -16,10 +17,11 @@ RUN apt-get update && apt-get install -y \
php-mbstring \ php-mbstring \
unzip \ unzip \
git \ git \
sudo \
mariadb-client \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Add custom PHP settings for Moodle # 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/apache2/conf.d/99-moodle.ini
COPY moodle.ini /etc/php/8.2/cli/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 ...@@ -35,7 +37,13 @@ RUN mkdir /var/www/moodledata && chown -R www-data:www-data /var/www/moodledata
WORKDIR /var/www/html WORKDIR /var/www/html
RUN chown -R www-data:www-data /var/www/html RUN chown -R www-data:www-data /var/www/html
# Copy a startup script # --- UPDATED SECTION ---
COPY --chown=root:root docker-entrypoint.sh / # Copy the entrypoint script to a standard location and make it executable
ENTRYPOINT ["/docker-entrypoint.sh"] 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"] CMD ["apache2ctl", "-D", "FOREGROUND"]
...@@ -29,6 +29,10 @@ services: ...@@ -29,6 +29,10 @@ services:
depends_on: depends_on:
- db - db
restart: unless-stopped restart: unless-stopped
environment:
- MOODLE_WWWROOT=http://localhost:8080
- MOODLE_ADMIN_USER=user
- MOODLE_ADMIN_PASS=QAYwsx@12345
backend: backend:
container_name: backend container_name: backend
......
#!/bin/bash #!/bin/bash
set -e 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 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 "$@" exec "$@"
DEBUG: Data object being sent to add_moduleinfo(): DEBUG: Data object being sent to add_moduleinfo():
stdClass Object stdClass Object
( (
[course] => 6 [course] => 2
[modulename] => assign [modulename] => assign
[name] => Fully Automated DTA Assignment [name] => Fully Automated DTA Assignment
[intro] => This assignment was created by the final automation script. [intro] => This assignment was created by the final automation script.
...@@ -11,7 +11,7 @@ stdClass Object ...@@ -11,7 +11,7 @@ stdClass Object
[assignsubmission_dta_enabled] => 1 [assignsubmission_dta_enabled] => 1
[assignsubmission_file_enabled] => 1 [assignsubmission_file_enabled] => 1
[dta] => 1 [dta] => 1
[tests_draft_dta] => 432698970 [tests_draft_dta] => 879454372
[assignsubmission_file_maxsizebytes] => 0 [assignsubmission_file_maxsizebytes] => 0
[cmidnumber] => [cmidnumber] =>
[assignsubmission_file_maxfiles] => 20 [assignsubmission_file_maxfiles] => 20
...@@ -28,12 +28,12 @@ stdClass Object ...@@ -28,12 +28,12 @@ stdClass Object
[markingworkflow] => 0 [markingworkflow] => 0
[markingallocation] => 0 [markingallocation] => 0
[attemptreopenmethod] => none [attemptreopenmethod] => none
[allowsubmissionsfromdate] => 1753362310 [allowsubmissionsfromdate] => 1756162043
[duedate] => 1753967110 [duedate] => 1756766843
[cutoffdate] => 0 [cutoffdate] => 0
[gradingduedate] => 0 [gradingduedate] => 0
[module] => 1 [module] => 1
[timemodified] => 1753362310 [timemodified] => 1756162043
[alwaysshowdescription] => 0 [alwaysshowdescription] => 0
[completionsubmit] => 0 [completionsubmit] => 0
) )
...@@ -41,7 +41,7 @@ stdClass Object ...@@ -41,7 +41,7 @@ stdClass Object
DEBUG: Returned object from add_moduleinfo(): DEBUG: Returned object from add_moduleinfo():
stdClass Object stdClass Object
( (
[course] => 6 [course] => 2
[modulename] => assign [modulename] => assign
[name] => Fully Automated DTA Assignment [name] => Fully Automated DTA Assignment
[intro] => This assignment was created by the final automation script. [intro] => This assignment was created by the final automation script.
...@@ -51,7 +51,7 @@ stdClass Object ...@@ -51,7 +51,7 @@ stdClass Object
[assignsubmission_dta_enabled] => 1 [assignsubmission_dta_enabled] => 1
[assignsubmission_file_enabled] => 1 [assignsubmission_file_enabled] => 1
[dta] => 1 [dta] => 1
[tests_draft_dta] => 432698970 [tests_draft_dta] => 879454372
[assignsubmission_file_maxsizebytes] => 0 [assignsubmission_file_maxsizebytes] => 0
[cmidnumber] => [cmidnumber] =>
[assignsubmission_file_maxfiles] => 20 [assignsubmission_file_maxfiles] => 20
...@@ -68,16 +68,16 @@ stdClass Object ...@@ -68,16 +68,16 @@ stdClass Object
[markingworkflow] => 0 [markingworkflow] => 0
[markingallocation] => 0 [markingallocation] => 0
[attemptreopenmethod] => none [attemptreopenmethod] => none
[allowsubmissionsfromdate] => 1753362310 [allowsubmissionsfromdate] => 1756162043
[duedate] => 1753967110 [duedate] => 1756766843
[cutoffdate] => 0 [cutoffdate] => 0
[gradingduedate] => 0 [gradingduedate] => 0
[module] => 1 [module] => 1
[timemodified] => 1753362310 [timemodified] => 1756162043
[alwaysshowdescription] => 0 [alwaysshowdescription] => 0
[completionsubmit] => 0 [completionsubmit] => 0
[instance] => 3 [instance] => 1
[coursemodule] => 10 [coursemodule] => 2
[groupingid] => 0 [groupingid] => 0
[completion] => 0 [completion] => 0
[completionview] => 0 [completionview] => 0
...@@ -100,10 +100,10 @@ stdClass Object ...@@ -100,10 +100,10 @@ stdClass Object
( (
[context:protected] => core\context\module Object [context:protected] => core\context\module Object
( (
[_id:protected] => 28 [_id:protected] => 14
[_contextlevel:protected] => 70 [_contextlevel:protected] => 70
[_instanceid:protected] => 10 [_instanceid:protected] => 2
[_path:protected] => /1/3/27/28 [_path:protected] => /1/3/13/14
[_depth:protected] => 4 [_depth:protected] => 4
[_locked:protected] => 0 [_locked:protected] => 0
) )
...@@ -116,4 +116,4 @@ stdClass Object ...@@ -116,4 +116,4 @@ stdClass Object
[showgradingmanagement] => [showgradingmanagement] =>
) )
3 1
#!/bin/bash #!/bin/bash
set -e set -e
# ============================================================================== # ==============================================================================
# DEFINITIVE Moodle CI/CD AUTOMATION SCRIPT V10 (ROBUST ERROR CHECKING) # DEFINITIVE Moodle CI/CD AUTOMATION SCRIPT V10 (ROBUST ERROR CHECKING)
# ============================================================================== # ==============================================================================
# --- Configuration (UPDATE THESE!) --- # --- Configuration (UPDATE THESE!) ---
MOODLE_URL="http://localhost:8080" MOODLE_URL="http://localhost:8080"
BACKEND_URL="http://localhost:8081" BACKEND_URL="http://localhost:8081"
MOODLE_ADMIN_USER="user" MOODLE_ADMIN_USER="user"
MOODLE_ADMIN_PASS="QAYwsx@12345" MOODLE_ADMIN_PASS="QAYwsx@12345"
# Automation User (Course creator) # Automation User (Course creator)
AUTOMATION_USER_NAME="maaeruser" AUTOMATION_USER_NAME="maaeruser"
AUTOMATION_USER_PASS="AutomationPassword123" AUTOMATION_USER_PASS="AutomationPassword123"
# Student User (created by script) # Student User (created by script)
STUDENT_USER_NAME="tstudent" STUDENT_USER_NAME="tstudent"
STUDENT_USER_PASS="StudentPassword123" STUDENT_USER_PASS="StudentPassword123"
DTA_CONFIG_FILE="teacher-dta-dir-test-runner-jdk21.txt" DTA_CONFIG_FILE="teacher-dta-dir-test-runner-jdk21.txt"
DTA_STUDENT_SUBMISSION_FILE="student-dir.txt" DTA_STUDENT_SUBMISSION_FILE="student-dir.txt"
# --- Static Configuration --- # --- Static Configuration ---
MOODLE_CONTAINER="moodle-web-official" MOODLE_CONTAINER="moodle-web-official"
MOODLE_SYSTEM_USER="www-data" MOODLE_SYSTEM_USER="www-data"
MOODLE_ROOT_IN_CONTAINER="/var/www/html" MOODLE_ROOT_IN_CONTAINER="/var/www/html"
PLUGIN_ZIP_NAME="dta.zip" PLUGIN_ZIP_NAME="dta.zip"
CUSTOM_SERVICE_NAME="dta_automation_service" CUSTOM_SERVICE_NAME="dta_automation_service"
DB_CONTAINER_NAME="moodle-db-official" DB_CONTAINER_NAME="moodle-db-official"
DB_ROOT_PASSWORD="verysecretpassword" DB_ROOT_PASSWORD="verysecretpassword"
DB_NAME="moodle" DB_NAME="moodle"
# ============================================================================== # ==============================================================================
# SCRIPT EXECUTION # SCRIPT EXECUTION
# ============================================================================== # ==============================================================================
echo "🚀 Starting Definitive Moodle Automation Script..." echo "🚀 Starting Definitive Moodle Automation Script..."
# --- Pre-flight Checks & Plugin Install (Step 1) --- # --- 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 ! 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 "$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 if [ ! -f "$DTA_CONFIG_FILE" ]; then echo "❌ DTA config file not found." && exit 1; fi
echo "✅ Pre-flight checks passed." echo "✅ Pre-flight checks passed."
echo "📦 Step 1: Installing custom plugin..." 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'" 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 "✅ Plugin installed."
echo " -> Enabling Web Service " 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 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 --- # --- Step 2: Create and Configure a Custom Web Service ---
echo " -> Clearing cURL security settings..." 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=curlsecurityblockedhosts --set=""
docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/cfg.php" --name=curlsecurityallowedport --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)..." 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 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 --- # --- THIS IS THE FINAL FIX ---
# Enable username/password authentication for web service token generation # Enable username/password authentication for web service token generation
echo " -> Enabling password authentication for web services..." 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 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 --- # --- Step 4: Create Dedicated API Role and Automation User ---
echo "🧑‍💻 Step 4: Creating 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 # This is a more advanced version of the user creation script
PHP_USER_SCRIPT_PATH="/tmp/create_api_user.php" PHP_USER_SCRIPT_PATH="/tmp/create_api_user.php"
LOCAL_PHP_USER_SCRIPT_PATH="/tmp/local_create_api_user.php" LOCAL_PHP_USER_SCRIPT_PATH="/tmp/local_create_api_user.php"
cat > "$LOCAL_PHP_USER_SCRIPT_PATH" <<EOF cat > "$LOCAL_PHP_USER_SCRIPT_PATH" <<EOF
<?php <?php
define('CLI_SCRIPT', true); define('CLI_SCRIPT', true);
require('/var/www/html/config.php'); require('/var/www/html/config.php');
require_once(\$CFG->libdir.'/clilib.php'); require_once(\$CFG->libdir.'/clilib.php');
require_once(\$CFG->dirroot.'/user/lib.php'); require_once(\$CFG->dirroot.'/user/lib.php');
global \$DB; global \$DB;
\$username = \$argv[1]; \$password = \$argv[2]; \$username = \$argv[1]; \$password = \$argv[2];
if (!\$DB->record_exists('user', ['username' => \$username])) { if (!\$DB->record_exists('user', ['username' => \$username])) {
\$user = new stdClass(); \$user = new stdClass();
\$user->username = \$username; \$user->firstname = 'API'; \$user->lastname = 'User'; \$user->username = \$username; \$user->firstname = 'API'; \$user->lastname = 'User';
\$user->email = 'api.user@example.com'; \$user->confirmed = 1; \$user->email = 'api.user@example.com'; \$user->confirmed = 1;
\$user->mnethostid = \$CFG->mnet_localhost_id; \$user->auth = 'manual'; \$user->mnethostid = \$CFG->mnet_localhost_id; \$user->auth = 'manual';
\$userid = user_create_user(\$user, false, false); \$userid = user_create_user(\$user, false, false);
\$user = \$DB->get_record('user', ['id' => \$userid]); \$user = \$DB->get_record('user', ['id' => \$userid]);
\update_internal_user_password(\$user, \$password); \update_internal_user_password(\$user, \$password);
echo "Created user '\$username' with ID: \$userid\n"; echo "Created user '\$username' with ID: \$userid\n";
// Create a new role for API users if it doesn't exist // Create a new role for API users if it doesn't exist
\$rolename = 'API User'; \$rolename = 'API User';
\$apicapability = 'moodle/webservice:createtoken'; \$apicapability = 'moodle/webservice:createtoken';
\$restapicapability = 'webservice/rest:use'; \$restapicapability = 'webservice/rest:use';
\$coursecapability = 'moodle/course:create'; \$coursecapability = 'moodle/course:create';
\$enrolcapability = 'enrol/manual:enrol'; \$enrolcapability = 'enrol/manual:enrol';
if (!\$apirole = \$DB->get_record('role', ['shortname' => 'apiuser'])) { if (!\$apirole = \$DB->get_record('role', ['shortname' => 'apiuser'])) {
\$apiroleid = create_role(\$rolename, 'apiuser', 'Role for API access'); \$apiroleid = create_role(\$rolename, 'apiuser', 'Role for API access');
\$apirole = \$DB->get_record('role', ['id' => \$apiroleid]); \$apirole = \$DB->get_record('role', ['id' => \$apiroleid]);
echo "Created role '\$rolename' with ID: \$apiroleid\n"; echo "Created role '\$rolename' with ID: \$apiroleid\n";
} }
// Ensure the capability is assigned to the role // Ensure the capability is assigned to the role
\$context = \core\context\system::instance(); \$context = \core\context\system::instance();
assign_capability(\$apicapability, CAP_ALLOW, \$apirole->id, \$context->id); assign_capability(\$apicapability, CAP_ALLOW, \$apirole->id, \$context->id);
assign_capability(\$restapicapability, 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(\$coursecapability, CAP_ALLOW, \$apirole->id, \$context->id);
assign_capability(\$enrolcapability, CAP_ALLOW, \$apirole->id, \$context->id); assign_capability(\$enrolcapability, CAP_ALLOW, \$apirole->id, \$context->id);
echo "Assigned capability '\$apicapability' to role '\$rolename'\n"; echo "Assigned capability '\$apicapability' to role '\$rolename'\n";
// Assign our automation user to this new role at the system level // Assign our automation user to this new role at the system level
role_assign(\$apirole->id, \$userid, \$context->id); role_assign(\$apirole->id, \$userid, \$context->id);
// Get the IDs for the roles we need to assign // Get the IDs for the roles we need to assign
\$managerroleid = \$DB->get_field('role', 'id', ['shortname' => 'manager']); \$managerroleid = \$DB->get_field('role', 'id', ['shortname' => 'manager']);
\$coursecreatorroleid = \$DB->get_field('role', 'id', ['shortname' => 'coursecreator']); \$coursecreatorroleid = \$DB->get_field('role', 'id', ['shortname' => 'coursecreator']);
\$systemcontext = \core\context\system::instance(); \$systemcontext = \core\context\system::instance();
// Assign the 'Manager' and 'Course creator' roles to the new user at the system context // Assign the 'Manager' and 'Course creator' roles to the new user at the system context
role_assign(\$managerroleid, \$userid, \$systemcontext->id); role_assign(\$managerroleid, \$userid, \$systemcontext->id);
role_assign(\$coursecreatorroleid, \$userid, \$systemcontext->id); role_assign(\$coursecreatorroleid, \$userid, \$systemcontext->id);
echo "Assigned user '\$username' to role '\$rolename'\n"; echo "Assigned user '\$username' to role '\$rolename'\n";
} else { } else {
\$userid = \$DB->get_field('user', 'id', ['username' => \$username]); \$userid = \$DB->get_field('user', 'id', ['username' => \$username]);
echo "User '\$username' already exists with ID: \$userid\n"; echo "User '\$username' already exists with ID: \$userid\n";
} }
// Create Student User // Create Student User
\$student_username = \$argv[3]; \$student_password = \$argv[4]; \$student_username = \$argv[3]; \$student_password = \$argv[4];
if (!\$DB->record_exists('user', ['username' => \$student_username])) { if (!\$DB->record_exists('user', ['username' => \$student_username])) {
\$student = new stdClass(); \$student->username = \$student_username; \$student->firstname = 'Test'; \$student->lastname = 'Student'; \$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'; \$student->email = 'student@example.com'; \$student->confirmed = 1; \$student->mnethostid = \$CFG->mnet_localhost_id; \$student->auth = 'manual';
\$studentid = user_create_user(\$student, false, false); \$studentid = user_create_user(\$student, false, false);
\$student = \$DB->get_record('user', ['id' => \$studentid]); \$student = \$DB->get_record('user', ['id' => \$studentid]);
\update_internal_user_password(\$student, \$student_password); \update_internal_user_password(\$student, \$student_password);
echo "Created user '\$student_username' with ID: \$studentid\n"; echo "Created user '\$student_username' with ID: \$studentid\n";
// Create a new role for API users if it doesn't exist // Create a new role for API users if it doesn't exist
\$rolename = 'API Student user'; \$rolename = 'API Student user';
\$apicapability = 'moodle/webservice:createtoken'; \$apicapability = 'moodle/webservice:createtoken';
\$restapicapability = 'webservice/rest:use'; \$restapicapability = 'webservice/rest:use';
if (!\$apirole = \$DB->get_record('role', ['shortname' => 'apistudentuser'])) { if (!\$apirole = \$DB->get_record('role', ['shortname' => 'apistudentuser'])) {
\$apiroleid = create_role(\$rolename, 'apistudentuser', 'Role for API access'); \$apiroleid = create_role(\$rolename, 'apistudentuser', 'Role for API access');
\$apirole = \$DB->get_record('role', ['id' => \$apiroleid]); \$apirole = \$DB->get_record('role', ['id' => \$apiroleid]);
echo "Created role '\$rolename' with ID: \$apiroleid\n"; echo "Created role '\$rolename' with ID: \$apiroleid\n";
} }
// Ensure the capability is assigned to the role // Ensure the capability is assigned to the role
\$context = \core\context\system::instance(); \$context = \core\context\system::instance();
assign_capability(\$apicapability, CAP_ALLOW, \$apirole->id, \$context->id); assign_capability(\$apicapability, CAP_ALLOW, \$apirole->id, \$context->id);
assign_capability(\$restapicapability, CAP_ALLOW, \$apirole->id, \$context->id); assign_capability(\$restapicapability, CAP_ALLOW, \$apirole->id, \$context->id);
echo "Assigned capability '\$apicapability' to role '\$rolename'\n"; echo "Assigned capability '\$apicapability' to role '\$rolename'\n";
// Assign our automation user to this new role at the system level // Assign our automation user to this new role at the system level
role_assign(\$apirole->id, \$studentid, \$context->id); role_assign(\$apirole->id, \$studentid, \$context->id);
echo "Assigned user '\$username' to role '\$rolename'\n"; echo "Assigned user '\$username' to role '\$rolename'\n";
} }
EOF EOF
docker cp "$LOCAL_PHP_USER_SCRIPT_PATH" "$MOODLE_CONTAINER:$PHP_USER_SCRIPT_PATH" 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 -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" docker exec "$MOODLE_CONTAINER" rm "$PHP_USER_SCRIPT_PATH"
rm "$LOCAL_PHP_USER_SCRIPT_PATH" rm "$LOCAL_PHP_USER_SCRIPT_PATH"
echo "✅ Automation and student users are configured." echo "✅ Automation and student users are configured."
echo "⚙️ Step 2: Creating and configuring a dedicated web service..." 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 docker exec -i "$DB_CONTAINER_NAME" mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" <<EOF
-- Enable Web Services globally in Moodle's config -- 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 ('enablewebservices', '1') ON DUPLICATE KEY UPDATE value = '1';
INSERT INTO mdl_config (name, value) VALUES ('webserviceprotocols', 'rest') ON DUPLICATE KEY UPDATE value = 'rest'; INSERT INTO mdl_config (name, value) VALUES ('webserviceprotocols', 'rest') ON DUPLICATE KEY UPDATE value = 'rest';
# --- End of addition -- # --- End of addition --
-- Create our custom service, now with a requiredcapability set for robustness -- Create our custom service, now with a requiredcapability set for robustness
INSERT IGNORE INTO mdl_external_services (name, shortname, enabled, requiredcapability, restrictedusers, uploadfiles, downloadfiles) 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 VALUES ('DTA Automation Service', '$CUSTOM_SERVICE_NAME', 1, '', 0, 1, 1); -- <-- The capability fix
-- Get the ID of our custom service -- Get the ID of our custom service
SET @serviceid = (SELECT id FROM mdl_external_services WHERE shortname = '$CUSTOM_SERVICE_NAME'); SET @serviceid = (SELECT id FROM mdl_external_services WHERE shortname = '$CUSTOM_SERVICE_NAME');
-- Add the required functions to OUR service -- Add the required functions to OUR service
INSERT IGNORE INTO mdl_external_services_functions (externalserviceid, functionname) VALUES INSERT IGNORE INTO mdl_external_services_functions (externalserviceid, functionname) VALUES
(@serviceid, 'core_user_create_users'), (@serviceid, 'core_user_create_users'),
(@serviceid, 'core_course_create_courses'), (@serviceid, 'core_course_create_courses'),
(@serviceid, 'enrol_manual_enrol_users'), (@serviceid, 'enrol_manual_enrol_users'),
(@serviceid, 'core_files_upload'), (@serviceid, 'core_files_upload'),
(@serviceid, 'mod_assign_save_submission'), (@serviceid, 'mod_assign_save_submission'),
(@serviceid, 'mod_assign_get_submission_status'), (@serviceid, 'mod_assign_get_submission_status'),
(@serviceid, 'mod_assign_get_submissions'), (@serviceid, 'mod_assign_get_submissions'),
(@serviceid, 'mod_assign_save_grade'), (@serviceid, 'mod_assign_save_grade'),
(@serviceid, 'assignsubmission_dta_save_submission'); (@serviceid, 'assignsubmission_dta_save_submission');
# -- *** THIS IS THE CRITICAL FIX *** # -- *** THIS IS THE CRITICAL FIX ***
# -- Authorize the admin user to use this service. # -- 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); 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) INSERT IGNORE INTO mdl_external_services_users (externalserviceid, userid, iprestriction, validuntil, timecreated)
VALUES (@serviceid, @automationuserid , NULL, NULL, UNIX_TIMESTAMP()); VALUES (@serviceid, @automationuserid , NULL, NULL, UNIX_TIMESTAMP());
-- Also authorize the student user to use the service to get a token -- 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'); SET @studentid = (SELECT id FROM mdl_user WHERE username = '$STUDENT_USER_NAME');
INSERT IGNORE INTO mdl_external_services_users (externalserviceid, userid) VALUES (@serviceid, @studentid); INSERT IGNORE INTO mdl_external_services_users (externalserviceid, userid) VALUES (@serviceid, @studentid);
EOF EOF
echo "✅ Custom web service created and user authorized." echo "✅ Custom web service created and user authorized."
echo " -> Purging Moodle caches.." echo " -> Purging Moodle caches.."
docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php" docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php"
sleep 15 sleep 15
# --- Step 6: Assign System Roles via API (The Definitive Fix) --- # --- Step 6: Assign System Roles via API (The Definitive Fix) ---
echo "🛡️ Step 6: Assigning System Roles to Automation User..." 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. # 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_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') ADMIN_SETUP_TOKEN=$(echo "$ADMIN_SETUP_TOKEN_RESPONSE" | jq -r '.token')
# Get IDs needed for role assignment # 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';") 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';") 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';") 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 SYSTEM_CONTEXT_ID=10 # The ID for the System context is always 1
# Assign Manager and Course Creator roles to the automation user # Assign Manager and Course Creator roles to the automation user
curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ 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 "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[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 -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." 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" docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php"
sleep 15 sleep 15
# --- THIS IS THE CRITICAL FIX --- # --- THIS IS THE CRITICAL FIX ---
echo " -> Purging Moodle caches to apply service changes..." 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" docker exec -u "$MOODLE_SYSTEM_USER" "$MOODLE_CONTAINER" php "$MOODLE_ROOT_IN_CONTAINER/admin/cli/purge_caches.php"
echo "✅ Caches purged." echo "✅ Caches purged."
sleep 5 sleep 5
echo "🔐 Step 3: Obtaining API Token...$AUTOMATION_USER_NAME..$AUTOMATION_USER_PASS" 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" \ TOKEN_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/login/token.php" \
--data-urlencode "username=$AUTOMATION_USER_NAME" \ --data-urlencode "username=$AUTOMATION_USER_NAME" \
--data-urlencode "password=$AUTOMATION_USER_PASS" \ --data-urlencode "password=$AUTOMATION_USER_PASS" \
--data-urlencode "service=$CUSTOM_SERVICE_NAME") --data-urlencode "service=$CUSTOM_SERVICE_NAME")
# Check for a Moodle error in the response FIRST # 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 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 "❌ Moodle API returned an error when requesting a token:"
echo "$TOKEN_RESPONSE" | jq . echo "$TOKEN_RESPONSE" | jq .
exit 1 exit 1
fi fi
TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.token') TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.token')
echo "✅ Successfully obtained API token. $TOKEN" echo "✅ Successfully obtained API token. $TOKEN"
# --- Step 4: Create a Course --- # --- Step 4: Create a Course ---
echo "📚 Step 4: Creating a new course..." echo "📚 Step 4: Creating a new course..."
COURSE_SHORTNAME="DTA-Course-$(date +%s)" COURSE_SHORTNAME="DTA-Course-$(date +%s)"
COURSE_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ 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 "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") -d "courses[0][fullname]=DTA Test Course" -d "courses[0][shortname]=$COURSE_SHORTNAME" -d "courses[0][categoryid]=1")
# --- THIS IS THE ROBUST ERROR CHECK --- # --- THIS IS THE ROBUST ERROR CHECK ---
# First, check if the response is a Moodle exception object. # First, check if the response is a Moodle exception object.
# The `jq -e` command sets an exit code, which works perfectly with `if`. # 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 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 "❌ Moodle API returned an exception during course creation:"
echo "$COURSE_RESPONSE" | jq . # Pretty-print the JSON error echo "$COURSE_RESPONSE" | jq . # Pretty-print the JSON error
exit 1 exit 1
fi fi
# If not an exception, try to parse the ID. This handles both single-object and array responses. # 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') 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 if [ -z "$NEW_COURSE_ID" ] || [ "$NEW_COURSE_ID" == "null" ]; then
echo "❌ Failed to parse a valid Course ID from the API response." echo "❌ Failed to parse a valid Course ID from the API response."
echo "Full API Response:" echo "Full API Response:"
echo "$COURSE_RESPONSE" | jq . echo "$COURSE_RESPONSE" | jq .
exit 1 exit 1
fi fi
echo "✅ Successfully created course '$COURSE_SHORTNAME' with ID: $NEW_COURSE_ID" echo "✅ Successfully created course '$COURSE_SHORTNAME' with ID: $NEW_COURSE_ID"
# --- END OF ROBUST ERROR CHECK --- # --- END OF ROBUST ERROR CHECK ---
# --- Step 5: Enroll Admin User into the New Course --- # --- Step 5: Enroll Admin User into the New Course ---
echo "🧑‍🏫 Step 5: Enrolling you as a teacher in the new course..." echo "🧑‍🏫 Step 5: Enrolling you as a teacher in the new course..."
# First, get the User ID for your admin user # 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';") 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. # The roleid for a Teacher is typically 3 (editingteacher) or 4 (teacher). We'll use 3.
TEACHER_ROLE_ID=3 TEACHER_ROLE_ID=3
# Add the enrol_manual_enrol_users function to our service # 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');" 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 # 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 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" \ 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 "wstoken=$TOKEN" -d "wsfunction=enrol_manual_enrol_users" -d "moodlewsrestformat=json" \
-d "enrolments[0][roleid]=$TEACHER_ROLE_ID" \ -d "enrolments[0][roleid]=$TEACHER_ROLE_ID" \
-d "enrolments[0][userid]=$AUTOMATION_USER_ID" \ -d "enrolments[0][userid]=$AUTOMATION_USER_ID" \
-d "enrolments[0][courseid]=$NEW_COURSE_ID") -d "enrolments[0][courseid]=$NEW_COURSE_ID")
# A successful response is null, so we check for an exception instead. # 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 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 "❌ Moodle API returned an exception during user enrollment:"
echo "$ENROL_RESPONSE" | jq . echo "$ENROL_RESPONSE" | jq .
exit 1 exit 1
fi fi
echo "✅ Successfully enrolled you in the course." echo "✅ Successfully enrolled you in the course."
# --- Step 5: Upload Configuration File for DTA Plugin --- # --- Step 5: Upload Configuration File for DTA Plugin ---
echo "📤 Step 5: Uploading config 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" \ 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") -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') 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 if [ -z "$DTA_FILE_ITEMID" ] || [ "$DTA_FILE_ITEMID" == "null" ]; then
echo "❌ Failed to upload DTA config file. Response:" echo "❌ Failed to upload DTA config file. Response:"
echo "$FILE_UPLOAD_RESPONSE" | jq . echo "$FILE_UPLOAD_RESPONSE" | jq .
exit 1 exit 1
fi fi
echo "✅ DTA config file uploaded. Received Item ID: $DTA_FILE_ITEMID" echo "✅ DTA config file uploaded. Received Item ID: $DTA_FILE_ITEMID"
# --- Step 6: Create Assignment via Custom PHP Script --- # --- Step 6: Create Assignment via Custom PHP Script ---
echo "📝 Step 6: Creating assignment with detailed error logging..." echo "📝 Step 6: Creating assignment with detailed error logging..."
PHP_ASSIGN_SCRIPT_PATH="/tmp/create_assignment.php" PHP_ASSIGN_SCRIPT_PATH="/tmp/create_assignment.php"
LOCAL_PHP_ASSIGN_SCRIPT_PATH="/tmp/local_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 --- # --- This PHP script now has a try/catch block for detailed errors ---
cat > "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" <<EOF cat > "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" <<EOF
<?php <?php
define('CLI_SCRIPT', true); define('CLI_SCRIPT', true);
require('/var/www/html/config.php'); require('/var/www/html/config.php');
require_once(\$CFG->dirroot.'/course/modlib.php'); require_once(\$CFG->dirroot.'/course/modlib.php');
global \$DB, \$USER; global \$DB, \$USER;
// programmatically "log in" as the admin user // programmatically "log in" as the admin user
// This gives file_save_draft_area_files the user context it needs. // This gives file_save_draft_area_files the user context it needs.
\$adminusername = "$AUTOMATION_USER_NAME"; \$adminusername = "$AUTOMATION_USER_NAME";
\$adminuser = \$DB->get_record('user', ['username' => \$adminusername, 'deleted' => 0], '*', MUST_EXIST); \$adminuser = \$DB->get_record('user', ['username' => \$adminusername, 'deleted' => 0], '*', MUST_EXIST);
\core\session\manager::set_user(\$adminuser); \core\session\manager::set_user(\$adminuser);
// The global $USER object is now populated. // The global $USER object is now populated.
if (count(\$argv) < 3) { exit("Error: Course ID and DTA Item ID are required.\n"); } if (count(\$argv) < 3) { exit("Error: Course ID and DTA Item ID are required.\n"); }
\$courseid = (int)\$argv[1]; \$courseid = (int)\$argv[1];
\$dta_itemid = (int)\$argv[2]; \$dta_itemid = (int)\$argv[2];
\$course = \$DB->get_record('course', ['id' => \$courseid], '*', MUST_EXIST); \$course = \$DB->get_record('course', ['id' => \$courseid], '*', MUST_EXIST);
\$data = new stdClass(); \$data = new stdClass();
// --- Core Module Details --- // --- Core Module Details ---
\$data->course = \$courseid; \$data->course = \$courseid;
\$data->modulename = 'assign'; \$data->modulename = 'assign';
\$data->name = 'Fully Automated DTA Assignment'; \$data->name = 'Fully Automated DTA Assignment';
\$data->intro = 'This assignment was created by the final automation script.'; \$data->intro = 'This assignment was created by the final automation script.';
\$data->introformat = FORMAT_HTML; \$data->introformat = FORMAT_HTML;
\$data->section = 1; \$data->section = 1;
\$data->visible = 1; \$data->visible = 1;
// --- Submission Settings --- // --- Submission Settings ---
\$data->assignsubmission_dta_enabled = 1; \$data->assignsubmission_dta_enabled = 1;
\$data->assignsubmission_file_enabled = 1; \$data->assignsubmission_file_enabled = 1;
// --- THIS IS FIX #1: Correct property name for the DTA file manager --- // --- 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' // This assumes the form element for your file manager is named 'package_filemanager'
// The final property name becomes assignsubmission_dta_package_filemanager // The final property name becomes assignsubmission_dta_package_filemanager
\$data->dta = 1; // Explicitly enable the dta group of settings \$data->dta = 1; // Explicitly enable the dta group of settings
\$data->tests_draft_dta = \$dta_itemid; \$data->tests_draft_dta = \$dta_itemid;
\$data->assignsubmission_file_maxsizebytes = 0; \$data->assignsubmission_file_maxsizebytes = 0;
\$data->cmidnumber = ''; \$data->cmidnumber = '';
\$data->assignsubmission_file_maxfiles = 20; \$data->assignsubmission_file_maxfiles = 20;
\$data->assignsubmission_onlinetext_enabled = 0; \$data->assignsubmission_onlinetext_enabled = 0;
\$data->submissiondrafts = 0; // Final submissions cannot be edited \$data->submissiondrafts = 0; // Final submissions cannot be edited
\$data->requiresubmissionstatement = 0; // The direct fix for the error \$data->requiresubmissionstatement = 0; // The direct fix for the error
\$data->requireallteammemberssubmit = 0; \$data->requireallteammemberssubmit = 0;
\$data->teamsubmission = 0; \$data->teamsubmission = 0;
// --- Notification Settings --- // --- Notification Settings ---
\$data->sendnotifications = 1; \$data->sendnotifications = 1;
\$data->sendlatenotifications = 0; \$data->sendlatenotifications = 0;
\$data->sendstudentnotifications = 1; \$data->sendstudentnotifications = 1;
// --- Grading Settings --- // --- Grading Settings ---
\$data->grade = 100; // Max grade \$data->grade = 100; // Max grade
\$data->blindmarking = 0; \$data->blindmarking = 0;
\$data->markingworkflow = 0; \$data->markingworkflow = 0;
\$data->markingallocation = 0; \$data->markingallocation = 0;
\$data->attemptreopenmethod = 'none'; \$data->attemptreopenmethod = 'none';
// --- Date Settings --- // --- Date Settings ---
\$data->allowsubmissionsfromdate = time(); \$data->allowsubmissionsfromdate = time();
\$data->duedate = time() + (7 * 24 * 60 * 60); // Due in 7 days \$data->duedate = time() + (7 * 24 * 60 * 60); // Due in 7 days
\$data->cutoffdate = 0; // No hard cutoff \$data->cutoffdate = 0; // No hard cutoff
\$data->gradingduedate = 0; \$data->gradingduedate = 0;
// --- Moodle Magic - these fields are necessary but we don't need to change them --- // --- 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->module = \$DB->get_field('modules', 'id', ['name' => 'assign'], MUST_EXIST);
\$data->timemodified = time(); \$data->timemodified = time();
\$data->alwaysshowdescription = 0; \$data->alwaysshowdescription = 0;
\$data->completionsubmit = 0; \$data->completionsubmit = 0;
// --- DEBUG: Print the exact data object we are sending --- // --- DEBUG: Print the exact data object we are sending ---
fwrite(STDERR, "DEBUG: Data object being sent to add_moduleinfo():\n"); fwrite(STDERR, "DEBUG: Data object being sent to add_moduleinfo():\n");
fwrite(STDERR, print_r(\$data, true) . "\n"); fwrite(STDERR, print_r(\$data, true) . "\n");
try { try {
// Attempt to create the assignment // Attempt to create the assignment
\$cm = add_moduleinfo(\$data, \$course); \$cm = add_moduleinfo(\$data, \$course);
fwrite(STDERR, "DEBUG: Returned object from add_moduleinfo():\n"); fwrite(STDERR, "DEBUG: Returned object from add_moduleinfo():\n");
fwrite(STDERR, print_r(\$cm, true) . "\n"); fwrite(STDERR, print_r(\$cm, true) . "\n");
if (!\$cm || !is_object(\$cm) || !\$cm->coursemodule) { if (!\$cm || !is_object(\$cm) || !\$cm->coursemodule) {
fwrite(STDERR, "Error: add_moduleinfo() returned invalid object\n"); fwrite(STDERR, "Error: add_moduleinfo() returned invalid object\n");
exit(1); exit(1);
} }
// Some versions: id = course module id, instance = assignment id // Some versions: id = course module id, instance = assignment id
\$assignmentid = property_exists(\$cm, 'instance') ? \$cm->instance : null; \$assignmentid = property_exists(\$cm, 'instance') ? \$cm->instance : null;
\$coursemoduleid = property_exists(\$cm, 'coursemodule') ? \$cm->coursemodule : null; \$coursemoduleid = property_exists(\$cm, 'coursemodule') ? \$cm->coursemodule : null;
if (!\$assignmentid) { if (!\$assignmentid) {
fwrite(STDERR, "Error: Could not extract assignment ID\n"); fwrite(STDERR, "Error: Could not extract assignment ID\n");
exit(1); exit(1);
} }
rebuild_course_cache(\$course->id, true); rebuild_course_cache(\$course->id, true);
// ✅ Echo only the assignment ID (for Bash) // ✅ Echo only the assignment ID (for Bash)
echo \$assignmentid . "\n"; echo \$assignmentid . "\n";
} catch (Exception \$e) { } catch (Exception \$e) {
// If anything goes wrong, catch the exception and print all details // If anything goes wrong, catch the exception and print all details
echo "\n!!!!!! SCRIPT FAILED: An Exception was caught! !!!!!!\n\n"; echo "\n!!!!!! SCRIPT FAILED: An Exception was caught! !!!!!!\n\n";
echo "Error Type: " . get_class(\$e) . "\n"; echo "Error Type: " . get_class(\$e) . "\n";
echo "Error Message: " . \$e->getMessage() . "\n\n"; echo "Error Message: " . \$e->getMessage() . "\n\n";
// Moodle's database exceptions often have extra useful debug info // Moodle's database exceptions often have extra useful debug info
if (!empty(\$e->debuginfo)) { if (!empty(\$e->debuginfo)) {
echo "Moodle Debug Info:\n" . \$e->debuginfo . "\n\n"; echo "Moodle Debug Info:\n" . \$e->debuginfo . "\n\n";
} }
echo "Stack Trace:\n" . \$e->getTraceAsString() . "\n"; echo "Stack Trace:\n" . \$e->getTraceAsString() . "\n";
exit(1); // Exit with an error code exit(1); // Exit with an error code
} }
EOF EOF
docker cp "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" "$MOODLE_CONTAINER:$PHP_ASSIGN_SCRIPT_PATH" 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) 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" docker exec "$MOODLE_CONTAINER" rm "$PHP_ASSIGN_SCRIPT_PATH"
rm "$LOCAL_PHP_ASSIGN_SCRIPT_PATH" rm "$LOCAL_PHP_ASSIGN_SCRIPT_PATH"
echo " -> Created assignment with ID: $NEW_ASSIGNMENT_ID" echo " -> Created assignment with ID: $NEW_ASSIGNMENT_ID"
echo "✅ Test scenario created." echo "✅ Test scenario created."
# --- Step 8: Student Submission Workflow --- # --- Step 8: Student Submission Workflow ---
echo "🧑‍🎓 Step 8: Beginning 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';") 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 # Role 5 is the standard 'Student' role
STUDENT_ROLE_ID=5 STUDENT_ROLE_ID=5
STUDENT_ENROL_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ 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 "wstoken=$TOKEN" -d "wsfunction=enrol_manual_enrol_users" -d "moodlewsrestformat=json" \
-d "enrolments[0][roleid]=$STUDENT_ROLE_ID" \ -d "enrolments[0][roleid]=$STUDENT_ROLE_ID" \
-d "enrolments[0][userid]=$STUDENT_USER_ID" \ -d "enrolments[0][userid]=$STUDENT_USER_ID" \
-d "enrolments[0][courseid]=$NEW_COURSE_ID") -d "enrolments[0][courseid]=$NEW_COURSE_ID")
echo " -> Enrolled student in course." echo " -> Enrolled student in course."
echo " -> Getting token for student..." echo " -> Getting token for student..."
STUDENT_TOKEN_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/login/token.php" \ 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") --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') 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 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 " -> Successfully obtained token for student. $STUDENT_TOKEN"
echo " -> Uploading student submission file..." 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_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') 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 " -> Student file uploaded. Item ID: $STUDENT_FILE_ITEMID"
echo " -> Saving final submission using our custom web service..." echo " -> Saving final submission using our custom web service..."
SAVE_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \ SAVE_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \
-d "wstoken=$STUDENT_TOKEN" \ -d "wstoken=$STUDENT_TOKEN" \
-d "wsfunction=assignsubmission_dta_save_submission" \ -d "wsfunction=assignsubmission_dta_save_submission" \
-d "moodlewsrestformat=json" \ -d "moodlewsrestformat=json" \
-d "assignmentid=$NEW_ASSIGNMENT_ID" \ -d "assignmentid=$NEW_ASSIGNMENT_ID" \
-d "userid=$STUDENT_USER_ID" \ -d "userid=$STUDENT_USER_ID" \
-d "itemid=$STUDENT_FILE_ITEMID") -d "itemid=$STUDENT_FILE_ITEMID")
if echo "$SAVE_RESPONSE" | jq -e 'if type=="object" and .exception then true else false end' > /dev/null; then 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 "❌ Moodle API returned an exception during submission save:"
echo "$SAVE_RESPONSE" | jq . echo "$SAVE_RESPONSE" | jq .
exit 1 exit 1
fi fi
echo "✅ Submission successfully sent to Moodle, which has triggered the backend grading." echo "✅ Submission successfully sent to Moodle, which has triggered the backend grading."
# --- Step 7: Verify Grade and Feedback in Moodle Database --- # --- Step 7: Verify Grade and Feedback in Moodle Database ---
echo "🧐 Step 7: Verifying grade and feedback has been posted back to Moodle..." echo "🧐 Step 7: Verifying grade and feedback has been posted back to Moodle..."
echo " -> Waiting 20 seconds for DTA backend to make the callback..." echo " -> Waiting 20 seconds for DTA backend to make the callback..."
sleep 20 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';") 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';") 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 if [ "$SUMMARY_COUNT" -eq 0 ]; then
echo "❌ ERROR: No feedback summary found in the database for submission ID $SUBMISSION_ID." echo "❌ ERROR: No feedback summary found in the database for submission ID $SUBMISSION_ID."
echo "The backend callback likely failed. Check the backend logs:" echo "The backend callback likely failed. Check the backend logs:"
docker logs backend docker logs backend
exit 1 exit 1
fi fi
echo "--- FINAL MOODLE DATABASE STATUS ---" echo "--- FINAL MOODLE DATABASE STATUS ---"
echo "✅ Found DTA summary record in the database." echo "✅ Found DTA summary record in the database."
echo "Detailed Test Results from mdl_assignsubmission_dta_result:" 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';" 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" \ STATUS_RESPONSE=$(curl --fail -sS -L -X POST "$MOODLE_URL/webservice/rest/server.php" \
-d "wstoken=$TOKEN" \ -d "wstoken=$TOKEN" \
-d "wsfunction=mod_assign_get_submission_status" \ -d "wsfunction=mod_assign_get_submission_status" \
-d "moodlewsrestformat=json" \ -d "moodlewsrestformat=json" \
-d "assignid=$NEW_ASSIGNMENT_ID" \ -d "assignid=$NEW_ASSIGNMENT_ID" \
-d "userid=$STUDENT_USER_ID") -d "userid=$STUDENT_USER_ID")
GRADE_STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.lastattempt.gradingstatus') GRADE_STATUS=$(echo "$STATUS_RESPONSE" | jq -r '.lastattempt.gradingstatus')
if [ "$GRADE_STATUS" == "graded" ]; then if [ "$GRADE_STATUS" == "graded" ]; then
echo "🎉 SUCCESS! The submission is marked as 'graded' in the Moodle GUI." echo "🎉 SUCCESS! The submission is marked as 'graded' in the Moodle GUI."
else 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." 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 exit 1
fi fi
echo "🎉🎉 Moodle automation complete. The environment is fully configured. 🎉🎉" echo "🎉🎉 Moodle automation complete. The environment is fully configured. 🎉🎉"
\ No newline at end of file
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