Repository: CodeWithEmad/tutor-contrib-wordpress (language: python)

Issue:
Support for WordPress instance management
Currently, the plugin only handles some object creation in the database. We need to manage a WordPress instance with the support of theme and plugins.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5eebcda..d1dec62 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,7 +25,7 @@ jobs:
         # https://github.com/actions/setup-python
         uses: actions/setup-python@v5
         with:
-          python-version: 3.8
+          python-version: 3.9
           cache: "pip"
       - name: Upgrade pip and setuptools
         # https://pypi.org/project/pip/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bcf62a..fce434d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,13 @@
 
+<a id='changelog-1.1.0'></a>
+## v1.1.0 (2024-11-27)
+
+- [Feature] Added comprehensive WordPress integration for Open edX including:
+  - Full WordPress installation and configuration management
+  - Integration with Open edX Commerce WordPress Plugin and WooCommerce
+  - Configurable WordPress settings including version, host, ports, and database options
+  (by @CodeWithEmad)
+
 <a id='changelog-1.0.1'></a>
 ## v1.0.1 (2024-07-27)
 
diff --git a/README.rst b/README.rst
index e572c28..9459160 100644
--- a/README.rst
+++ b/README.rst
@@ -10,28 +10,23 @@ WordPress Plugin for `Tutor`_
    :target: https://www.gnu.org/licenses/agpl-3.0.en.html
 
 Overview
---------
+********
 
-This is a `Tutor`_ plugin for integrating the `Open edX Commerce WordPress Plugin`_ with your Open edX instance.
-The plugin allows seamless synchronization and interaction between your WordPress site and Open edX platform,
-providing a cohesive e-commerce experience. If you don't know how to install the WordPress plugin, check out
-this `installation guide`_.
+This is a `Tutor`_ plugin that provides complete WordPress integration with your Open edX instance. It includes:
 
-Before you start
-----------------
+- Full WordPress installation and management
+- Integration with the `Open edX Commerce WordPress Plugin`_
 
-This plugin does **not** create or manage a WordPress instance at the moment. It is designed to assist with the
-configuration and integration of the Open edX platform with an existing WordPress site. If you need this feature,
-feel free to open an issue or better yet, a PR.
+If you need help installing the WordPress plugin, check out this `installation guide`_.
 
 Prerequisites
--------------
+*************
 
-- A running WordPress site with the `Open edX Commerce WordPress Plugin`_ installed.
-- Installation of Tutor version >= 15.0.0.
+- Installation of Tutor version >= 15.0.0
+- MySQL database (can use the same one as Open edX)
 
 Installation
-------------
+************
 
 Ensure you are using Tutor v15+ (Olive onwards). First, install the plugin by running:
 
@@ -51,59 +46,95 @@ Alternatively, if you already have a running Open edX instance, just run the nec
 .. code-block:: bash
 
     tutor dev|local|k8s do init --limit=wordpress
+    tutor dev|local|k8s start wordpress
 
 Configuration
--------------
+*************
 
-Inside your WordPress admin panel, you will need to configure the following values:
+The plugin automatically installs WordPress with the Open edX Commerce and Woocommerce plugins. Inside your WordPress
+admin panel, go to Settings -> Open edX Sync plugin, and configure:
 
 - **Open edX Domain**
 - **Client ID**
 - **Client Secret**
 
+To verify the connection, click on "Generate JWT Token". If the process is successful, a new token will be generated.
+
 .. image:: https://raw.githubusercontent.com/codewithemad/tutor-contrib-wordpress/master/images/openedx-sync-plugin-settings.png
    :alt: Open edX Sync Plugin Settings in your WordPress Settings
 
-
 You can retrieve these configuration values by running:
 
 .. code-block:: bash
 
-    tutor wordpress config
+    tutor dev|local|k8s do wordpress config
 
+This command will output the current configurations, including the Client ID, Client Secret, Open edX Domain,
+and WordPress Domain. Here is an example of the output:
 
 .. code-block:: text
 
     ===============================================
-            WordPress Plugin Configurations
+        WordPress Plugin Configurations
     ===============================================
 
-    Open edX Domain: http://local.edly.io
-    Client ID: qjCayDktffXrU09N17NrslKyWQ2EwzWn
-    Client ID (dev): JDx6Uy0hN67VUfacxKcLyYQz7HK9liVx
-    Client Secret: P4w82huaZQdyz4qolknsIHYneGEoIggc
+    Client ID: vvpTamiepPwjZhr0uOQGr5PhYBzp2hQw 
+    Client ID (dev): MlbXk1V3wB7nWPAAyLF3McyfBBMqExa4 
+    Client Secret: MdrgbtU8Q94He3gejF6Zf5MDookoeozO 
 
+    Open edX Domain: http://local.edly.io:8000 
+    Wordpress Domain: http://site.local.edly.io:8080
 
-Or by using Tutor to print them individually:
+Variables
+*********
 
-.. code-block:: bash
+The plugin supports the following configuration variables:
+
+- ``WORDPRESS_VERSION``: Plugin version
+- ``WORDPRESS_HOST``: WordPress site hostname
+- ``WORDPRESS_PORT``: WordPress port (default: 8080)
+- ``WORDPRESS_DOCKER_IMAGE``: Docker image for WordPress
+- ``WORDPRESS_OPENEDX_PLUGIN``: URL to the Open edX Commerce plugin
+- ``WORDPRESS_WOOCOMMERCE_PLUGIN``: (default: "https://downloads.wordpress.org/plugin/woocommerce.9.4.2.zip")
+    The WooCommerce plugin zip file URL. You can specify a different version if needed.
+- ``WORDPRESS_OFFICIAL_IMAGE``: (default: "wordpress:6.7.1-php8.1")
+    The official WordPress Docker image used as the base for building the plugin's custom image.
+    This image includes PHP and Apache server. You can specify a different version or PHP variant
+    if needed.
+
+Database Settings
+=================
+
+- ``WORDPRESS_MYSQL_HOST``: MySQL host
+- ``WORDPRESS_MYSQL_PORT``: MySQL port
+- ``WORDPRESS_MYSQL_DATABASE``: Database name (default: wordpress)
+- ``WORDPRESS_MYSQL_USERNAME``: Database username
+- ``WORDPRESS_MYSQL_PASSWORD``: Database password (auto-generated)
+- ``WORDPRESS_TABLE_PREFIX``: Table prefix (default: wp_)
 
-    tutor config printvalue LMS_HOST
-    tutor config printvalue WORDPRESS_OAUTH2_SECRET
-    tutor config printvalue WORDPRESS_OAUTH2_KEY_SSO
-    tutor config printvalue WORDPRESS_OAUTH2_KEY_SSO_DEV
+Storage Settings
+================
+
+- ``WORDPRESS_DATA_VOLUME_SIZE``: Size of WordPress persistent volume (default: 5Gi)
+
+OAuth2 Settings
+===============
+
+- ``WORDPRESS_OAUTH2_SECRET``: OAuth2 secret key (auto-generated)
+- ``WORDPRESS_OAUTH2_KEY_SSO``: OAuth2 client ID (auto-generated)
+- ``WORDPRESS_OAUTH2_KEY_SSO_DEV``: OAuth2 development client ID (auto-generated)
 
 Contributing
-------------
+************
 
-We welcome all contributions! Feel free to open a Pull Request.
+We welcome all contributions! Feel free to open a Pull Request or an Issue.
 
 License
--------
+*******
 
 This software is licensed under the terms of the `AGPLv3`_.
 
 .. _Tutor: https://docs.tutor.edly.io
-.. _installation guide: https://docs.openedx.org/projects/wordpress-ecommerce-plugin/en/latest/plugin_quickstart.html
 .. _Open edX Commerce WordPress Plugin: https://github.com/openedx/openedx-wordpress-ecommerce
 .. _AGPLv3: https://github.com/codewithemad/tutor-contrib-wordpress/blob/master/LICENSE.txt
+.. _installation guide: https://docs.openedx.org/projects/wordpress-ecommerce-plugin/en/latest/plugin_quickstart.html
diff --git a/images/openedx-sync-plugin-settings.png b/images/openedx-sync-plugin-settings.png
index 9a4726a..9e224d9 100644
Binary files a/images/openedx-sync-plugin-settings.png and b/images/openedx-sync-plugin-settings.png differ
diff --git a/setup.py b/setup.py
index 4fae30f..c3e2feb 100644
--- a/setup.py
+++ b/setup.py
@@ -36,29 +36,29 @@ def load_about():
     license="AGPLv3",
     author="Emad Rad",
     author_email="codewithemad@gmail.com",
-    description="Tutor plugin for WooCommerce",
+    description="Tutor plugin for WordPress.",
     long_description=load_readme(),
     long_description_content_type="text/x-rst",
     packages=find_packages(exclude=["tests*"]),
     include_package_data=True,
-    python_requires=">=3.8",
-    install_requires=["tutor>=14.0.0,<19.0.0"],
+    python_requires=">=3.7",
+    install_requires=[
+        "tutor>=15.0.0,<19.0.0",
+        "importlib_resources",  # for older versions of tutor
+    ],
     extras_require={
         "dev": [
-            "tutor[dev]>=14.0.0,<19.0.0",
-        ]
-    },
-    entry_points={
-        "tutor.plugin.v1": [
-            "wordpress = tutorwordpress.plugin"
+            "tutor[dev]>=15.0.0,<19.0.0",
         ]
     },
+    entry_points={"tutor.plugin.v1": ["wordpress = tutorwordpress.plugin"]},
     classifiers=[
         "Development Status :: 3 - Alpha",
         "Intended Audience :: Developers",
         "License :: OSI Approved :: GNU Affero General Public License v3",
         "Operating System :: OS Independent",
         "Programming Language :: Python",
+        "Programming Language :: Python :: 3.7",
         "Programming Language :: Python :: 3.8",
         "Programming Language :: Python :: 3.9",
         "Programming Language :: Python :: 3.10",
diff --git a/tutorwordpress/patches/caddyfile b/tutorwordpress/patches/caddyfile
new file mode 100644
index 0000000..b5de133
--- /dev/null
+++ b/tutorwordpress/patches/caddyfile
@@ -0,0 +1,5 @@
+# WordPress
+{{ WORDPRESS_HOST }}{$default_site_port} {
+
+    import proxy "wordpress:{{ WORDPRESS_PORT }}"
+}
diff --git a/tutorwordpress/patches/k8s-deployments b/tutorwordpress/patches/k8s-deployments
new file mode 100644
index 0000000..ead8451
--- /dev/null
+++ b/tutorwordpress/patches/k8s-deployments
@@ -0,0 +1,51 @@
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: wordpress
+  labels:
+    app.kubernetes.io/name: wordpress
+spec:
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: wordpress
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: wordpress
+    spec:
+      initContainers:
+        - name: init-wordpress
+          image: {{ WORDPRESS_DOCKER_IMAGE }}
+          command: ['sh', '-c', 'chown -R www-data:www-data /var/www/html/wp-content']
+          volumeMounts:
+            - name: wordpress-data
+              mountPath: /var/www/html/wp-content
+          securityContext:
+            runAsUser: 0
+      containers:
+        - name: wordpress
+          image: {{ WORDPRESS_DOCKER_IMAGE }}
+          ports:
+            - containerPort: 8080
+          env:
+            - name: WORDPRESS_DB_HOST
+              value: {{ WORDPRESS_MYSQL_HOST }}
+            - name: WORDPRESS_DB_USER
+              value: {{ WORDPRESS_MYSQL_USERNAME }}
+            - name: WORDPRESS_DB_PASSWORD
+              value: {{ WORDPRESS_MYSQL_PASSWORD }}
+            - name: WORDPRESS_DB_NAME
+              value: {{ WORDPRESS_MYSQL_DATABASE }}
+            - name: WORDPRESS_TABLE_PREFIX
+              value: {{ WORDPRESS_TABLE_PREFIX }}
+          volumeMounts:
+            - name: wordpress-data
+              mountPath: /var/www/html/wp-content
+          securityContext:
+            runAsUser: 33
+            runAsGroup: 33
+      volumes:
+        - name: wordpress-data
+          persistentVolumeClaim:
+            claimName: wordpress-data
diff --git a/tutorwordpress/patches/k8s-jobs b/tutorwordpress/patches/k8s-jobs
new file mode 100644
index 0000000..baad901
--- /dev/null
+++ b/tutorwordpress/patches/k8s-jobs
@@ -0,0 +1,21 @@
+---
+apiVersion: batch/v1
+kind: Job
+metadata:
+  name: wordpress-job
+  labels:
+    app.kubernetes.io/component: job
+spec:
+  template:
+    spec:
+      restartPolicy: Never
+      containers:
+        - name: wordpress
+          image: {{ WORDPRESS_DOCKER_IMAGE }}
+          volumeMounts:
+            - name: wordpress-data  
+              mountPath: /var/www/html/wp-content
+      volumes:
+        - name: wordpress-data
+          persistentVolumeClaim:
+            claimName: wordpress-data
diff --git a/tutorwordpress/patches/k8s-services b/tutorwordpress/patches/k8s-services
new file mode 100644
index 0000000..2387e5a
--- /dev/null
+++ b/tutorwordpress/patches/k8s-services
@@ -0,0 +1,13 @@
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: wordpress
+spec:
+  type: ClusterIP
+  ports:
+    - port: {{ WORDPRESS_PORT }}
+      targetPort: 8080
+      protocol: TCP
+  selector:
+    app.kubernetes.io/name: wordpress
diff --git a/tutorwordpress/patches/k8s-volumes b/tutorwordpress/patches/k8s-volumes
new file mode 100644
index 0000000..90bafcb
--- /dev/null
+++ b/tutorwordpress/patches/k8s-volumes
@@ -0,0 +1,14 @@
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: wordpress-data
+  labels:
+    app.kubernetes.io/component: volume
+    app.kubernetes.io/name: wordpress-data
+spec:
+  accessModes:
+    - ReadWriteOnce
+  resources:
+    requests:
+      storage: {{ WORDPRESS_DATA_VOLUME_SIZE }}
diff --git a/tutorwordpress/patches/local-docker-compose-caddy-aliases b/tutorwordpress/patches/local-docker-compose-caddy-aliases
new file mode 100644
index 0000000..ab50070
--- /dev/null
+++ b/tutorwordpress/patches/local-docker-compose-caddy-aliases
@@ -0,0 +1,1 @@
+- "{{ WORDPRESS_HOST }}"
diff --git a/tutorwordpress/patches/local-docker-compose-dev-services b/tutorwordpress/patches/local-docker-compose-dev-services
new file mode 100644
index 0000000..6d08116
--- /dev/null
+++ b/tutorwordpress/patches/local-docker-compose-dev-services
@@ -0,0 +1,11 @@
+wordpress:
+  stdin_open: true
+  tty: true
+  ports:
+    - {{ WORDPRESS_PORT }}:8080
+  environment:
+    WORDPRESS_DEBUG: 1
+    WORDPRESS_CONFIG_EXTRA: |
+      define('WP_DEBUG', true);
+      define('WP_DEBUG_LOG', true);
+      define('WP_DEBUG_DISPLAY', true);
diff --git a/tutorwordpress/patches/local-docker-compose-jobs-services b/tutorwordpress/patches/local-docker-compose-jobs-services
new file mode 100644
index 0000000..03d85b3
--- /dev/null
+++ b/tutorwordpress/patches/local-docker-compose-jobs-services
@@ -0,0 +1,7 @@
+wordpress-job:
+  image: {{ WORDPRESS_DOCKER_IMAGE }}
+  volumes:
+    - ../../data/wordpress:/var/www/html/wp-content
+  depends_on:
+    - lms
+    {% if RUN_MYSQL %}- mysql{% endif %}
diff --git a/tutorwordpress/patches/local-docker-compose-services b/tutorwordpress/patches/local-docker-compose-services
new file mode 100644
index 0000000..63309d6
--- /dev/null
+++ b/tutorwordpress/patches/local-docker-compose-services
@@ -0,0 +1,16 @@
+wordpress:
+  image: {{ WORDPRESS_DOCKER_IMAGE }}
+  restart: unless-stopped
+  environment:
+    WORDPRESS_DB_HOST: {{ WORDPRESS_MYSQL_HOST }}
+    WORDPRESS_DB_USER: {{ WORDPRESS_MYSQL_USERNAME }}
+    WORDPRESS_DB_PASSWORD: {{ WORDPRESS_MYSQL_PASSWORD }}
+    WORDPRESS_DB_NAME: {{ WORDPRESS_MYSQL_DATABASE }}
+    WORDPRESS_TABLE_PREFIX: {{ WORDPRESS_TABLE_PREFIX }}
+  volumes:
+    - ../../data/wordpress/wp-content:/var/www/html/wp-content
+  {%- if RUN_MYSQL %}
+  depends_on:
+    - mysql
+  {% endif -%}
+
diff --git a/tutorwordpress/plugin.py b/tutorwordpress/plugin.py
index 03c3d12..9721047 100644
--- a/tutorwordpress/plugin.py
+++ b/tutorwordpress/plugin.py
@@ -1,32 +1,59 @@
 from __future__ import annotations
 
+from glob import glob
 import os
 import sys
+import typing as t
 
 import click
-from tutor import fmt, hooks, config as tutor_config
+from tutor import fmt, hooks as tutor_hooks, config as tutor_config
+from tutor.__about__ import __version_suffix__
 
 from .__about__ import __version__
 
+# Support for older Python versions
 if sys.version_info >= (3, 9):
-    from importlib.resources import files as importlib_resources_files
+    import importlib_resources
 else:
     import pkg_resources
 
+# Handle version suffix in nightly mode, just like tutor core
+if __version_suffix__:
+    __version__ += "-" + __version_suffix__
+
 ###############
 # CONFIGURATION
 ###############
 
-hooks.Filters.CONFIG_DEFAULTS.add_items(
+tutor_hooks.Filters.CONFIG_DEFAULTS.add_items(
     [
-        # Add your new settings that have default values here.
-        # Each new setting is a pair: (setting_name, default_value).
-        # Prefix your setting names with 'WORDPRESS_'.
         ("WORDPRESS_VERSION", __version__),
+        ("WORDPRESS_HOST", "site.{{ LMS_HOST }}"),
+        ("WORDPRESS_PORT", "8080"),
+        # https://hub.docker.com/_/wordpress/
+        ("WORDPRESS_OFFICIAL_IMAGE", "wordpress:6.7.1-php8.1"),
+        (
+            "WORDPRESS_DOCKER_IMAGE",
+            "docker.io/codewithemad/tutor-wordpress:{{ WORDPRESS_VERSION }}",
+        ),
+        ("WORDPRESS_MYSQL_HOST", "{{ MYSQL_HOST }}"),
+        ("WORDPRESS_MYSQL_PORT", "{{ MYSQL_PORT }}"),
+        ("WORDPRESS_MYSQL_DATABASE", "wordpress"),
+        ("WORDPRESS_MYSQL_USERNAME", "wordpress"),
+        ("WORDPRESS_TABLE_PREFIX", "wp_"),
+        ("WORDPRESS_DATA_VOLUME_SIZE", "5Gi"),
+        (
+            "WORDPRESS_OPENEDX_PLUGIN",
+            "https://github.com/openedx/openedx-wordpress-ecommerce/releases/download/v2.0.7/openedx-commerce.zip",
+        ),
+        (
+            "WORDPRESS_WOOCOMMERCE_PLUGIN",
+            "https://downloads.wordpress.org/plugin/woocommerce.9.4.2.zip",
+        ),
     ]
 )
 
-hooks.Filters.CONFIG_UNIQUE.add_items(
+tutor_hooks.Filters.CONFIG_UNIQUE.add_items(
     [
         # Add settings that don't have a reasonable default for all users here.
         # For instance: passwords, secret keys, etc.
@@ -35,6 +62,7 @@
         ("WORDPRESS_OAUTH2_SECRET", "{{ 32|random_string }}"),
         ("WORDPRESS_OAUTH2_KEY_SSO", "{{ 32|random_string }}"),
         ("WORDPRESS_OAUTH2_KEY_SSO_DEV", "{{ 32|random_string }}"),
+        ("WORDPRESS_MYSQL_PASSWORD", "{{ 32|random_string }}"),
     ]
 )
 
@@ -44,13 +72,14 @@
 
 MY_INIT_TASKS: list[tuple[str, tuple[str, ...]]] = [
     ("lms", ("wordpress", "tasks", "lms", "init.sh")),
+    ("mysql", ("wordpress", "tasks", "mysql", "init.sh")),
 ]
 
 
 def get_template_full_path(package_name: str, *template_path: str) -> str:
     if sys.version_info >= (3, 9):
         return str(
-            importlib_resources_files(package_name)
+            importlib_resources.files(package_name)
             / os.path.join("templates", *template_path)
         )
     else:
@@ -62,7 +91,78 @@ def get_template_full_path(package_name: str, *template_path: str) -> str:
     full_path: str = get_template_full_path("tutorwordpress", *template_path)
     with open(full_path, encoding="utf-8") as init_task_file:
         init_task: str = init_task_file.read()
-    hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task))
+    tutor_hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task))
+
+
+#########################
+# DOCKER IMAGE MANAGEMENT
+#########################
+
+tutor_hooks.Filters.IMAGES_BUILD.add_item(
+    (
+        "wordpress",
+        ("plugins", "wordpress", "build", "wordpress"),
+        "{{ WORDPRESS_DOCKER_IMAGE }}",
+        (),
+    )
+)
+tutor_hooks.Filters.IMAGES_PULL.add_items(
+    [
+        (
+            "wordpress",
+            "{{ WORDPRESS_DOCKER_IMAGE }}",
+        ),
+    ]
+)
+tutor_hooks.Filters.IMAGES_PUSH.add_item(
+    (
+        "wordpress",
+        "{{ WORDPRESS_DOCKER_IMAGE }}",
+    )
+)
+
+####################
+# TEMPLATE RENDERING
+####################
+
+tutor_hooks.Filters.ENV_TEMPLATE_ROOTS.add_items(
+    # Root paths for template files, relative to the project root.
+    [
+        str(get_template_full_path("tutorwordpress", "")),
+    ]
+)
+
+tutor_hooks.Filters.ENV_TEMPLATE_TARGETS.add_items(
+    # For each pair (source_path, destination_path):
+    # templates at ``source_path`` (relative to your ENV_TEMPLATE_ROOTS) will be
+    # rendered to ``source_path/destination_path`` (relative to your Tutor environment).
+    # For example, ``tutorwordpress/templates/wordpress/build``
+    # will be rendered to ``$(tutor config printroot)/env/plugins/wordpress/build``.
+    [
+        ("wordpress/build", "plugins"),
+        ("wordpress/apps", "plugins"),
+    ],
+)
+
+
+################
+# PATCH LOADING
+################
+
+# For each file in tutorwordpress/patches,
+# apply a patch based on the file's name and contents.
+if sys.version_info >= (3, 9):
+    glob_pattern = str(importlib_resources.files("tutorwordpress") / "patches" / "*")
+else:
+    glob_pattern = os.path.join(
+        pkg_resources.resource_filename("tutorwordpress", "patches"), "*"
+    )
+
+for path in glob(glob_pattern):
+    with open(path, encoding="utf-8") as patch_file:
+        tutor_hooks.Filters.ENV_PATCHES.add_item(
+            (os.path.basename(path), patch_file.read())
+        )
 
 #####################
 # CUSTOM CLI COMMANDS
@@ -74,7 +174,7 @@ def wordpress() -> None:
     pass
 
 
-hooks.Filters.CLI_COMMANDS.add_item(wordpress)
+tutor_hooks.Filters.CLI_COMMANDS.add_item(wordpress)
 
 
 @wordpress.command()
@@ -84,13 +184,22 @@ def config(context: click.Context) -> None:
     Print out the configurations used in WordPress plugin.
     """
     config = tutor_config.load(context.obj.root)
-    protocol = "https" if config["ENABLE_HTTPS"] else "http"
+    https_enabled = config["ENABLE_HTTPS"]
+    protocol = "https" if https_enabled else "http"
     lms_host = f"{protocol}://{config['LMS_HOST']}"
+    wordpress_host = f"{protocol}://{config['WORDPRESS_HOST']}"
+    if not https_enabled:
+        lms_host += ":8000"
+        wordpress_host += f":{config['WORDPRESS_PORT']}"
+
     click.echo(fmt.title("WordPress Plugin Configurations"))
-    fmt.echo_info(
+    info_text = (
         "\n"
-        f"Open edX Domain: {lms_host} \n"
         f"Client ID: {config['WORDPRESS_OAUTH2_KEY_SSO']} \n"
         f"Client ID (dev): {config['WORDPRESS_OAUTH2_KEY_SSO_DEV']} \n"
         f"Client Secret: {config['WORDPRESS_OAUTH2_SECRET']} \n"
+        "\n"
+        f"Open edX Domain: {lms_host} \n"
+        f"Wordpress Domain: {wordpress_host}"
     )
+    fmt.echo_info(info_text)
diff --git a/tutorwordpress/templates/wordpress/build/wordpress/Dockerfile b/tutorwordpress/templates/wordpress/build/wordpress/Dockerfile
new file mode 100644
index 0000000..40857ba
--- /dev/null
+++ b/tutorwordpress/templates/wordpress/build/wordpress/Dockerfile
@@ -0,0 +1,30 @@
+# Use the official WordPress image
+FROM {{ WORDPRESS_OFFICIAL_IMAGE }}
+
+# Install necessary utilities
+RUN apt-get update && \
+    apt-get install -y wget unzip && \
+    rm -rf /var/lib/apt/lists/*
+
+# Download and install WooCommerce
+RUN wget {{ WORDPRESS_WOOCOMMERCE_PLUGIN }} -O /tmp/woocommerce.zip && \
+    unzip -o /tmp/woocommerce.zip -d /usr/src/wordpress/wp-content/plugins/ && \
+    rm /tmp/woocommerce.zip
+
+# Download and install the OpenEdX WordPress plugin
+RUN wget {{ WORDPRESS_OPENEDX_PLUGIN }} -O /tmp/openedx-commerce.zip && \
+    unzip -o /tmp/openedx-commerce.zip -d /usr/src/wordpress/wp-content/plugins/ && \
+    rm /tmp/openedx-commerce.zip
+
+# Set proper permissions
+RUN chown -R www-data:www-data /usr/src/wordpress/wp-content/plugins/
+
+# Clean up
+RUN apt-get purge -y wget unzip && \
+    apt-get autoremove -y
+
+# Add ServerName directive to suppress Apache warning
+RUN echo "ServerName localhost" >> /etc/apache2/apache2.conf
+
+# Configure Apache to listen on port 8080
+RUN sed -i 's/80/8080/g' /etc/apache2/ports.conf /etc/apache2/sites-available/000-default.conf
diff --git a/tutorwordpress/templates/wordpress/tasks/mysql/init.sh b/tutorwordpress/templates/wordpress/tasks/mysql/init.sh
new file mode 100644
index 0000000..0857f03
--- /dev/null
+++ b/tutorwordpress/templates/wordpress/tasks/mysql/init.sh
@@ -0,0 +1,4 @@
+mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e "CREATE DATABASE IF NOT EXISTS {{ WORDPRESS_MYSQL_DATABASE }};"
+mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e "CREATE USER IF NOT EXISTS '{{ WORDPRESS_MYSQL_USERNAME }}';"
+mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e "ALTER USER '{{ WORDPRESS_MYSQL_USERNAME }}'@'%' IDENTIFIED BY '{{ WORDPRESS_MYSQL_PASSWORD }}';"
+mysql -u {{ MYSQL_ROOT_USERNAME }} --password="{{ MYSQL_ROOT_PASSWORD }}" --host "{{ MYSQL_HOST }}" --port {{ MYSQL_PORT }} -e "GRANT ALL PRIVILEGES ON {{ WORDPRESS_MYSQL_DATABASE }}.* TO '{{ WORDPRESS_MYSQL_USERNAME }}'@'%';"


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 0049a5d..31fcb21 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-         python-version: ['3.8', '3.12']
+         python-version: ['3.7', '3.12']
     steps:
       - uses: actions/checkout@v4
       - name: Set up Python ${{ matrix.python-version }}
@@ -20,6 +20,15 @@ jobs:
         run: python -m pip install --upgrade pip
       - name: Install dependencies
         run: |
+          if [ "${{ matrix.python-version }}" = "3.7" ]; then
+            pip install "tutor==15.0.0" pylint black mypy types-setuptools
+          else
+            pip install -U tutor
+          fi
+          echo "Installing tutor-contrib-wordpress"
           pip install .[dev]
+          echo "Enable wordpress plugin and save configuration"
+          tutor plugins enable wordpress
+          tutor config save
       - name: Test lint, types, and format
         run: make test