This is an automated email from the ASF dual-hosted git repository.

yasithdev pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-portals.git


The following commit(s) were added to refs/heads/main by this push:
     new 33d5b5fc3 feat(cms): scaffold the standalone Wagtail CMS service 
(airavata-cms) (#113)
33d5b5fc3 is described below

commit 33d5b5fc327e541b16da8cc2f44862e734bb1b9a
Author: Yasith Jayawardana <[email protected]>
AuthorDate: Mon Jun 8 01:18:16 2026 -0400

    feat(cms): scaffold the standalone Wagtail CMS service (airavata-cms) (#113)
    
    Stand up a new, independent Wagtail 7.4 / Django 6.0 (Python 3.13) project, 
airavata-cms, that will own the gateway's admin-authored content (landing 
pages, documentation sites) decoupled from airavata-django-portal. This is the 
second step of extracting Wagtail out of the portal: the portal keeps no CMS or 
database, and a reverse-proxy router will later stitch the two into one site 
for end users.
    
    This PR is the scaffold only - the canonical `wagtail start` layout (the 
airavata_cms project with split base/dev/production settings, a home page app, 
the search view, a production Dockerfile, and pinned requirements) plus a 
.gitignore and README. It migrates cleanly and passes `manage.py check` on 
sqlite. Porting the existing portal page models (HomePage / BlankPage / 
CybergatewayHomePage) and the seagrid theme/fixtures, and adding the router, 
follow in subsequent steps.
---
 airavata-cms/.dockerignore                         |  39 +++++
 airavata-cms/.gitignore                            |   9 +
 airavata-cms/Dockerfile                            |  86 ++++++++++
 airavata-cms/README.md                             |  32 ++++
 airavata-cms/airavata_cms/__init__.py              |   0
 airavata-cms/airavata_cms/settings/__init__.py     |   0
 airavata-cms/airavata_cms/settings/base.py         | 184 +++++++++++++++++++++
 airavata-cms/airavata_cms/settings/dev.py          |  18 ++
 airavata-cms/airavata_cms/settings/production.py   |  14 ++
 .../airavata_cms/static/css/airavata_cms.css       |   0
 .../airavata_cms/static/js/airavata_cms.js         |   0
 airavata-cms/airavata_cms/templates/404.html       |  11 ++
 airavata-cms/airavata_cms/templates/500.html       |  13 ++
 airavata-cms/airavata_cms/templates/base.html      |  46 ++++++
 airavata-cms/airavata_cms/urls.py                  |  35 ++++
 airavata-cms/airavata_cms/wsgi.py                  |  16 ++
 airavata-cms/home/__init__.py                      |   0
 airavata-cms/home/apps.py                          |   6 +
 airavata-cms/home/migrations/0001_initial.py       |  31 ++++
 .../home/migrations/0002_create_homepage.py        |  66 ++++++++
 airavata-cms/home/migrations/__init__.py           |   0
 airavata-cms/home/models.py                        |   7 +
 airavata-cms/home/static/css/welcome_page.css      | 184 +++++++++++++++++++++
 airavata-cms/home/templates/home/home_page.html    |  21 +++
 airavata-cms/home/templates/home/welcome_page.html |  52 ++++++
 airavata-cms/home/tests.py                         |  42 +++++
 airavata-cms/manage.py                             |  22 +++
 airavata-cms/requirements.txt                      |   2 +
 airavata-cms/search/__init__.py                    |   0
 airavata-cms/search/templates/search/search.html   |  38 +++++
 airavata-cms/search/views.py                       |  46 ++++++
 31 files changed, 1020 insertions(+)

diff --git a/airavata-cms/.dockerignore b/airavata-cms/.dockerignore
new file mode 100644
index 000000000..63ce98d58
--- /dev/null
+++ b/airavata-cms/.dockerignore
@@ -0,0 +1,39 @@
+# Django project
+/media/
+/static/
+*.sqlite3
+
+# Python and others
+__pycache__
+*.pyc
+.DS_Store
+*.swp
+/venv/
+/tmp/
+/.vagrant/
+/Vagrantfile.local
+node_modules/
+/npm-debug.log
+/.idea/
+.vscode
+coverage
+.python-version
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
diff --git a/airavata-cms/.gitignore b/airavata-cms/.gitignore
new file mode 100644
index 000000000..1d350496b
--- /dev/null
+++ b/airavata-cms/.gitignore
@@ -0,0 +1,9 @@
+*.pyc
+__pycache__/
+db.sqlite3
+/media/
+/static_collected/
+/staticfiles/
+/venv/
+.venv/
+.env
diff --git a/airavata-cms/Dockerfile b/airavata-cms/Dockerfile
new file mode 100644
index 000000000..0ded7a93d
--- /dev/null
+++ b/airavata-cms/Dockerfile
@@ -0,0 +1,86 @@
+
+# This stage installs build dependencies and compiles Python packages.
+# It will be discarded in the final image, keeping only the compiled packages.
+FROM python:3.12-slim-bookworm AS builder
+
+# Install system packages required to build Python packages.
+RUN apt-get update --yes --quiet && apt-get install --yes --quiet 
--no-install-recommends \
+    build-essential \
+    libpq-dev \
+    libmariadb-dev \
+    libjpeg62-turbo-dev \
+    zlib1g-dev \
+    libwebp-dev \
+ && rm -rf /var/lib/apt/lists/* \
+ && python -m venv /opt/venv
+
+ENV PATH="/opt/venv/bin:$PATH"
+
+# Install the project requirements.
+COPY requirements.txt /
+RUN pip install -r /requirements.txt
+
+# Install the application server.
+RUN pip install "gunicorn==25.1.0"
+
+
+# RUNTIME STAGE
+# Use an official Python runtime based on Debian 12 "bookworm" as a parent 
image.
+FROM python:3.12-slim-bookworm AS runtime
+
+# Install runtime system packages required by Wagtail and Django.
+# These are the runtime libraries needed by the compiled Python packages.
+RUN apt-get update --yes --quiet && apt-get install --yes --quiet 
--no-install-recommends \
+    libpq5 \
+    libmariadb3 \
+    libjpeg62-turbo \
+    libwebp7 \
+ && rm -rf /var/lib/apt/lists/*
+
+# Add user that will be used in the container.
+RUN useradd wagtail
+
+# Port used by this container to serve HTTP.
+EXPOSE 8000
+
+# Set environment variables.
+# 1. Force Python stdout and stderr streams to be unbuffered.
+# 2. Set PORT variable that is used by Gunicorn. This should match "EXPOSE"
+#    command.
+# 3. Add the virtual environment to PATH.
+ENV PYTHONUNBUFFERED=1 \
+    PORT=8000 \
+    PATH="/opt/venv/bin:$PATH"
+
+
+
+# Copy the virtual environment from the builder stage.
+COPY --from=builder /opt/venv /opt/venv
+
+# Use /app folder as a directory where the source code is stored.
+WORKDIR /app
+
+# Set this directory to be owned by the "wagtail" user. This Wagtail project
+# uses SQLite, the folder needs to be owned by the user that
+# will be writing to the database file.
+RUN chown wagtail:wagtail /app
+
+# Copy the source code of the project into the container.
+COPY --chown=wagtail:wagtail . .
+
+# Use user "wagtail" to run the build commands below and the server itself.
+USER wagtail
+
+# Collect static files.
+RUN python manage.py collectstatic --noinput --clear
+
+# Runtime command that executes when "docker run" is called, it does the
+# following:
+#   1. Migrate the database.
+#   2. Start the application server.
+# WARNING:
+#   Migrating database at the same time as starting the server IS NOT THE BEST
+#   PRACTICE. The database should be migrated manually or using the release
+#   phase facilities of your hosting platform. This is used only so the
+#   Wagtail instance can be started with a simple "docker run" command.
+CMD set -xe; python manage.py migrate --noinput; gunicorn 
airavata_cms.wsgi:application
diff --git a/airavata-cms/README.md b/airavata-cms/README.md
new file mode 100644
index 000000000..637c12422
--- /dev/null
+++ b/airavata-cms/README.md
@@ -0,0 +1,32 @@
+# airavata-cms
+
+Standalone [Wagtail](https://wagtail.org/) CMS for Airavata gateways — landing
+pages, documentation sites, and other admin-authored content.
+
+It is deliberately **decoupled** from `airavata-django-portal`: the portal is a
+stateless gateway UI that talks only to the Airavata API, while this service 
owns
+its own database and the editable content. A reverse proxy/router stitches the
+two into one site for end users (the portal serves the app paths; this CMS 
serves
+landing/docs/content paths).
+
+## Stack
+
+- Wagtail 7.4 / Django 6.0 / Python 3.13
+
+## Develop
+
+```bash
+python3 -m venv venv && source venv/bin/activate
+pip install -r requirements.txt
+export DJANGO_SETTINGS_MODULE=airavata_cms.settings.dev
+python manage.py migrate
+python manage.py createsuperuser
+python manage.py runserver            # CMS admin at /admin, pages at /
+```
+
+## Layout
+
+- `airavata_cms/` — project (settings split into `base`/`dev`/`production`, 
urls, wsgi)
+- `home/` — the home page app (Wagtail `Page` models live here)
+- `search/` — Wagtail search view
+- `Dockerfile` — production image (gunicorn)
diff --git a/airavata-cms/airavata_cms/__init__.py 
b/airavata-cms/airavata_cms/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/airavata-cms/airavata_cms/settings/__init__.py 
b/airavata-cms/airavata_cms/settings/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/airavata-cms/airavata_cms/settings/base.py 
b/airavata-cms/airavata_cms/settings/base.py
new file mode 100644
index 000000000..72be1bd6e
--- /dev/null
+++ b/airavata-cms/airavata_cms/settings/base.py
@@ -0,0 +1,184 @@
+"""
+Django settings for airavata_cms project.
+
+Generated by 'django-admin startproject' using Django 6.0.6.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/6.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/6.0/ref/settings/
+"""
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+from pathlib import Path
+
+PROJECT_DIR = Path(__file__).resolve().parent.parent
+BASE_DIR = PROJECT_DIR.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    "home",
+    "search",
+    "wagtail.contrib.forms",
+    "wagtail.contrib.redirects",
+    "wagtail.embeds",
+    "wagtail.sites",
+    "wagtail.users",
+    "wagtail.snippets",
+    "wagtail.documents",
+    "wagtail.images",
+    "wagtail.search",
+    "wagtail.admin",
+    "wagtail",
+    "modelcluster",
+    "taggit",
+    "django_filters",
+    "django.contrib.admin",
+    "django.contrib.auth",
+    "django.contrib.contenttypes",
+    "django.contrib.sessions",
+    "django.contrib.messages",
+    "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+    "django.middleware.security.SecurityMiddleware",
+    "django.contrib.sessions.middleware.SessionMiddleware",
+    "django.middleware.common.CommonMiddleware",
+    "django.middleware.csrf.CsrfViewMiddleware",
+    "django.contrib.auth.middleware.AuthenticationMiddleware",
+    "django.contrib.messages.middleware.MessageMiddleware",
+    "django.middleware.clickjacking.XFrameOptionsMiddleware",
+    "wagtail.contrib.redirects.middleware.RedirectMiddleware",
+]
+
+ROOT_URLCONF = "airavata_cms.urls"
+
+TEMPLATES = [
+    {
+        "BACKEND": "django.template.backends.django.DjangoTemplates",
+        "DIRS": [
+            PROJECT_DIR / "templates",
+        ],
+        "APP_DIRS": True,
+        "OPTIONS": {
+            "context_processors": [
+                "django.template.context_processors.debug",
+                "django.template.context_processors.request",
+                "django.contrib.auth.context_processors.auth",
+                "django.contrib.messages.context_processors.messages",
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = "airavata_cms.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
+
+DATABASES = {
+    "default": {
+        "ENGINE": "django.db.backends.sqlite3",
+        "NAME": BASE_DIR / "db.sqlite3",
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        "NAME": 
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+    },
+    {
+        "NAME": 
"django.contrib.auth.password_validation.MinimumLengthValidator",
+    },
+    {
+        "NAME": 
"django.contrib.auth.password_validation.CommonPasswordValidator",
+    },
+    {
+        "NAME": 
"django.contrib.auth.password_validation.NumericPasswordValidator",
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/6.0/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/6.0/howto/static-files/
+
+STATICFILES_FINDERS = [
+    "django.contrib.staticfiles.finders.FileSystemFinder",
+    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+]
+
+STATICFILES_DIRS = [
+    PROJECT_DIR / "static",
+]
+
+STATIC_ROOT = BASE_DIR / "static"
+STATIC_URL = "/static/"
+
+MEDIA_ROOT = BASE_DIR / "media"
+MEDIA_URL = "/media/"
+
+# Default storage settings
+# See https://docs.djangoproject.com/en/6.0/ref/settings/#std-setting-STORAGES
+STORAGES = {
+    "default": {
+        "BACKEND": "django.core.files.storage.FileSystemStorage",
+    },
+    "staticfiles": {
+        "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
+    },
+}
+
+# Django sets a maximum of 1000 fields per form by default, but particularly 
complex page models
+# can exceed this limit within Wagtail's page editor.
+DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
+
+
+# Wagtail settings
+
+WAGTAIL_SITE_NAME = "airavata_cms"
+
+# Search
+# https://docs.wagtail.org/en/stable/topics/search/backends.html
+WAGTAILSEARCH_BACKENDS = {
+    "default": {
+        "BACKEND": "wagtail.search.backends.database",
+    }
+}
+
+# Base URL to use when referring to full URLs within the Wagtail admin backend 
-
+# e.g. in notification emails. Don't include '/admin' or a trailing slash
+WAGTAILADMIN_BASE_URL = "http://example.com";
+
+# Allowed file extensions for documents in the document library.
+# This can be omitted to allow all files, but note that this may present a 
security risk
+# if untrusted users are allowed to upload files -
+# see 
https://docs.wagtail.org/en/stable/advanced_topics/deploying.html#user-uploaded-files
+WAGTAILDOCS_EXTENSIONS = ['csv', 'docx', 'key', 'odt', 'pdf', 'pptx', 'rtf', 
'txt', 'xlsx', 'zip']
+
+# Maximum upload size for documents in bytes.
+WAGTAILDOCS_MAX_UPLOAD_SIZE = 10 * 1024 * 1024  # 10MB
diff --git a/airavata-cms/airavata_cms/settings/dev.py 
b/airavata-cms/airavata_cms/settings/dev.py
new file mode 100644
index 000000000..2e8b953ba
--- /dev/null
+++ b/airavata-cms/airavata_cms/settings/dev.py
@@ -0,0 +1,18 @@
+from .base import *
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 
"django-insecure-cb1(qc(z5iaw0xsk0@*2e%ls0g&n0q(f#xu@d(h77#bufgahs3"
+
+# SECURITY WARNING: define the correct hosts in production!
+ALLOWED_HOSTS = ["*"]
+
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
+
+
+try:
+    from .local import *
+except ImportError:
+    pass
diff --git a/airavata-cms/airavata_cms/settings/production.py 
b/airavata-cms/airavata_cms/settings/production.py
new file mode 100644
index 000000000..936a45ff3
--- /dev/null
+++ b/airavata-cms/airavata_cms/settings/production.py
@@ -0,0 +1,14 @@
+from .base import *
+
+DEBUG = False
+
+# ManifestStaticFilesStorage is recommended in production, to prevent
+# outdated JavaScript / CSS assets being served from cache
+# (e.g. after a Wagtail upgrade).
+# See 
https://docs.djangoproject.com/en/6.0/ref/contrib/staticfiles/#manifeststaticfilesstorage
+STORAGES["staticfiles"]["BACKEND"] = 
"django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
+
+try:
+    from .local import *
+except ImportError:
+    pass
diff --git a/airavata-cms/airavata_cms/static/css/airavata_cms.css 
b/airavata-cms/airavata_cms/static/css/airavata_cms.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/airavata-cms/airavata_cms/static/js/airavata_cms.js 
b/airavata-cms/airavata_cms/static/js/airavata_cms.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/airavata-cms/airavata_cms/templates/404.html 
b/airavata-cms/airavata_cms/templates/404.html
new file mode 100644
index 000000000..f19ab953b
--- /dev/null
+++ b/airavata-cms/airavata_cms/templates/404.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% block title %}Page not found{% endblock %}
+
+{% block body_class %}template-404{% endblock %}
+
+{% block content %}
+<h1>Page not found</h1>
+
+<h2>Sorry, this page could not be found.</h2>
+{% endblock %}
diff --git a/airavata-cms/airavata_cms/templates/500.html 
b/airavata-cms/airavata_cms/templates/500.html
new file mode 100644
index 000000000..77379e558
--- /dev/null
+++ b/airavata-cms/airavata_cms/templates/500.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en" dir="ltr">
+    <head>
+        <meta charset="utf-8" />
+        <title>Internal server error</title>
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+    </head>
+    <body>
+        <h1>Internal server error</h1>
+
+        <h2>Sorry, there seems to be an error. Please try again soon.</h2>
+    </body>
+</html>
diff --git a/airavata-cms/airavata_cms/templates/base.html 
b/airavata-cms/airavata_cms/templates/base.html
new file mode 100644
index 000000000..f9967adea
--- /dev/null
+++ b/airavata-cms/airavata_cms/templates/base.html
@@ -0,0 +1,46 @@
+{% load static wagtailcore_tags wagtailuserbar %}
+
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <title>
+            {% block title %}
+            {% if page.seo_title %}{{ page.seo_title }}{% else %}{{ page.title 
}}{% endif %}
+            {% endblock %}
+            {% block title_suffix %}
+            {% wagtail_site as current_site %}
+            {% if current_site and current_site.site_name %}- {{ 
current_site.site_name }}{% endif %}
+            {% endblock %}
+        </title>
+        {% if page.search_description %}
+        <meta name="description" content="{{ page.search_description }}" />
+        {% endif %}
+        <meta name="viewport" content="width=device-width, initial-scale=1" />
+
+        {# Force all links in the live preview panel to be opened in a new tab 
#}
+        {% if request.in_preview_panel %}
+        <base target="_blank">
+        {% endif %}
+
+        {# Global stylesheets #}
+        <link rel="stylesheet" type="text/css" href="{% static 
'css/airavata_cms.css' %}">
+
+        {% block extra_css %}
+        {# Override this in templates to add extra stylesheets #}
+        {% endblock %}
+    </head>
+
+    <body class="{% block body_class %}{% endblock %}">
+        {% wagtailuserbar %}
+
+        {% block content %}{% endblock %}
+
+        {# Global javascript #}
+        <script type="text/javascript" src="{% static 'js/airavata_cms.js' 
%}"></script>
+
+        {% block extra_js %}
+        {# Override this in templates to add extra javascript #}
+        {% endblock %}
+    </body>
+</html>
diff --git a/airavata-cms/airavata_cms/urls.py 
b/airavata-cms/airavata_cms/urls.py
new file mode 100644
index 000000000..c2e8a0cf6
--- /dev/null
+++ b/airavata-cms/airavata_cms/urls.py
@@ -0,0 +1,35 @@
+from django.conf import settings
+from django.urls import include, path
+from django.contrib import admin
+
+from wagtail.admin import urls as wagtailadmin_urls
+from wagtail import urls as wagtail_urls
+from wagtail.documents import urls as wagtaildocs_urls
+
+from search import views as search_views
+
+urlpatterns = [
+    path("django-admin/", admin.site.urls),
+    path("admin/", include(wagtailadmin_urls)),
+    path("documents/", include(wagtaildocs_urls)),
+    path("search/", search_views.search, name="search"),
+]
+
+
+if settings.DEBUG:
+    from django.conf.urls.static import static
+    from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+
+    # Serve static and media files from development server
+    urlpatterns += staticfiles_urlpatterns()
+    urlpatterns += static(settings.MEDIA_URL, 
document_root=settings.MEDIA_ROOT)
+
+urlpatterns = urlpatterns + [
+    # For anything not caught by a more specific rule above, hand over to
+    # Wagtail's page serving mechanism. This should be the last pattern in
+    # the list:
+    path("", include(wagtail_urls)),
+    # Alternatively, if you want Wagtail pages to be served from a subpath
+    # of your site, rather than the site root:
+    #    path("pages/", include(wagtail_urls)),
+]
diff --git a/airavata-cms/airavata_cms/wsgi.py 
b/airavata-cms/airavata_cms/wsgi.py
new file mode 100644
index 000000000..f8ce17387
--- /dev/null
+++ b/airavata-cms/airavata_cms/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for airavata_cms project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "airavata_cms.settings.dev")
+
+application = get_wsgi_application()
diff --git a/airavata-cms/home/__init__.py b/airavata-cms/home/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/airavata-cms/home/apps.py b/airavata-cms/home/apps.py
new file mode 100644
index 000000000..e7d1c7eb9
--- /dev/null
+++ b/airavata-cms/home/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class HomeConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "home"
diff --git a/airavata-cms/home/migrations/0001_initial.py 
b/airavata-cms/home/migrations/0001_initial.py
new file mode 100644
index 000000000..67f201d32
--- /dev/null
+++ b/airavata-cms/home/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("wagtailcore", "0040_page_draft_title"),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name="HomePage",
+            fields=[
+                (
+                    "page_ptr",
+                    models.OneToOneField(
+                        on_delete=models.CASCADE,
+                        parent_link=True,
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        to="wagtailcore.Page",
+                    ),
+                ),
+            ],
+            options={
+                "abstract": False,
+            },
+            bases=("wagtailcore.page",),
+        ),
+    ]
diff --git a/airavata-cms/home/migrations/0002_create_homepage.py 
b/airavata-cms/home/migrations/0002_create_homepage.py
new file mode 100644
index 000000000..a3e918ec5
--- /dev/null
+++ b/airavata-cms/home/migrations/0002_create_homepage.py
@@ -0,0 +1,66 @@
+from django.db import migrations
+
+
+def create_homepage(apps, schema_editor):
+    # Get models
+    ContentType = apps.get_model("contenttypes.ContentType")
+    Page = apps.get_model("wagtailcore.Page")
+    Site = apps.get_model("wagtailcore.Site")
+    HomePage = apps.get_model("home.HomePage")
+
+    # Delete the default homepage (of type Page) as created by 
wagtailcore.0002_initial_data,
+    # if it exists
+    page_content_type = ContentType.objects.get(
+        model="page", app_label="wagtailcore"
+    )
+    Page.objects.filter(
+        content_type=page_content_type, slug="home", depth=2
+    ).delete()
+
+    # Create content type for homepage model
+    homepage_content_type, __ = ContentType.objects.get_or_create(
+        model="homepage", app_label="home"
+    )
+
+    # Create a new homepage
+    homepage = HomePage.objects.create(
+        title="Home",
+        draft_title="Home",
+        slug="home",
+        content_type=homepage_content_type,
+        path="00010001",
+        depth=2,
+        numchild=0,
+        url_path="/home/",
+    )
+
+    # Create a site with the new homepage set as the root
+    Site.objects.create(hostname="localhost", root_page=homepage, 
is_default_site=True)
+
+
+def remove_homepage(apps, schema_editor):
+    # Get models
+    ContentType = apps.get_model("contenttypes.ContentType")
+    HomePage = apps.get_model("home.HomePage")
+
+    # Delete the default homepage
+    # Page and Site objects CASCADE
+    HomePage.objects.filter(slug="home", depth=2).delete()
+
+    # Delete content type for homepage model
+    ContentType.objects.filter(model="homepage", app_label="home").delete()
+
+
+class Migration(migrations.Migration):
+
+    run_before = [
+        ("wagtailcore", "0053_locale_model"),
+    ]
+
+    dependencies = [
+        ("home", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.RunPython(create_homepage, remove_homepage),
+    ]
diff --git a/airavata-cms/home/migrations/__init__.py 
b/airavata-cms/home/migrations/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/airavata-cms/home/models.py b/airavata-cms/home/models.py
new file mode 100644
index 000000000..5076f57a1
--- /dev/null
+++ b/airavata-cms/home/models.py
@@ -0,0 +1,7 @@
+from django.db import models
+
+from wagtail.models import Page
+
+
+class HomePage(Page):
+    pass
diff --git a/airavata-cms/home/static/css/welcome_page.css 
b/airavata-cms/home/static/css/welcome_page.css
new file mode 100644
index 000000000..bad293346
--- /dev/null
+++ b/airavata-cms/home/static/css/welcome_page.css
@@ -0,0 +1,184 @@
+html {
+    box-sizing: border-box;
+}
+
+*,
+*:before,
+*:after {
+    box-sizing: inherit;
+}
+
+body {
+    max-width: 960px;
+    min-height: 100vh;
+    margin: 0 auto;
+    padding: 0 15px;
+    color: #231f20;
+    font-family: 'Helvetica Neue', 'Segoe UI', Arial, sans-serif;
+    line-height: 1.25;
+}
+
+a {
+    background-color: transparent;
+    color: #308282;
+    text-decoration: underline;
+}
+
+a:hover {
+    color: #ea1b10;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+p,
+ul {
+    padding: 0;
+    margin: 0;
+    font-weight: 400;
+}
+
+svg:not(:root) {
+    overflow: hidden;
+}
+
+.header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding-top: 20px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #e6e6e6;
+}
+
+.logo {
+    width: 150px;
+    margin-inline-end: 20px;
+}
+
+.logo a {
+    display: block;
+}
+
+.figure-logo {
+    max-width: 150px;
+    max-height: 55.1px;
+}
+
+.release-notes {
+    font-size: 14px;
+}
+
+.main {
+    padding: 40px 0;
+    margin: 0 auto;
+    text-align: center;
+}
+
+.figure-space {
+    max-width: 265px;
+}
+
+@keyframes pos {
+    0%, 100% {
+        transform: rotate(-6deg);
+    }
+    50% {
+        transform: rotate(6deg);
+    }
+}
+
+.egg {
+    fill: #43b1b0;
+    animation: pos 3s ease infinite;
+    transform: translateY(50px);
+    transform-origin: 50% 80%;
+}
+
+.main-text {
+    max-width: 400px;
+    margin: 5px auto;
+}
+
+.main-text h1 {
+    font-size: 22px;
+}
+
+.main-text p {
+    margin: 15px auto 0;
+}
+
+.footer {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    border-top: 1px solid #e6e6e6;
+    padding: 10px;
+}
+
+.option {
+    display: block;
+    padding: 10px 10px 10px 34px;
+    position: relative;
+    text-decoration: none;
+}
+
+.option svg {
+    width: 24px;
+    height: 24px;
+    fill: gray;
+    border: 1px solid #d9d9d9;
+    padding: 5px;
+    border-radius: 100%;
+    top: 10px;
+    inset-inline-start: 0;
+    position: absolute;
+}
+
+.option h2 {
+    font-size: 19px;
+    text-decoration: underline;
+}
+
+.option p {
+    padding-top: 3px;
+    color: #231f20;
+    font-size: 15px;
+    font-weight: 300;
+}
+
+@media (max-width: 996px) {
+    body {
+        max-width: 780px;
+    }
+}
+
+@media (max-width: 767px) {
+    .option {
+        flex: 0 0 50%;
+    }
+}
+
+@media (max-width: 599px) {
+    .main {
+        padding: 20px 0;
+    }
+
+    .figure-space {
+        max-width: 200px;
+    }
+
+    .footer {
+        display: block;
+        width: 300px;
+        margin: 0 auto;
+    }
+}
+
+@media (max-width: 360px) {
+    .header-link {
+        max-width: 100px;
+    }
+}
diff --git a/airavata-cms/home/templates/home/home_page.html 
b/airavata-cms/home/templates/home/home_page.html
new file mode 100644
index 000000000..db9e9b036
--- /dev/null
+++ b/airavata-cms/home/templates/home/home_page.html
@@ -0,0 +1,21 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block body_class %}template-homepage{% endblock %}
+
+{% block extra_css %}
+
+{% comment %}
+Delete the line below if you're just getting started and want to remove the 
welcome screen!
+{% endcomment %}
+<link rel="stylesheet" href="{% static 'css/welcome_page.css' %}">
+{% endblock extra_css %}
+
+{% block content %}
+
+{% comment %}
+Delete the line below if you're just getting started and want to remove the 
welcome screen!
+{% endcomment %}
+{% include 'home/welcome_page.html' %}
+
+{% endblock content %}
diff --git a/airavata-cms/home/templates/home/welcome_page.html 
b/airavata-cms/home/templates/home/welcome_page.html
new file mode 100644
index 000000000..dcacaf31a
--- /dev/null
+++ b/airavata-cms/home/templates/home/welcome_page.html
@@ -0,0 +1,52 @@
+{% load i18n wagtailcore_tags %}
+
+<header class="header">
+    <div class="logo">
+        <a href="https://wagtail.org/";>
+            <svg class="figure-logo" xmlns="http://www.w3.org/2000/svg"; 
viewBox="0 0 342.5 126.2"><title>{% trans "Visit the Wagtail website" 
%}</title><path fill="#FFF" d="M84 1.9v5.7s-10.2-3.8-16.8 3.1c-4.8 5-5.2 10.6-3 
18.1 21.6 0 25 12.1 25 12.1L87 27l6.8-8.3c0-9.8-8.1-16.3-9.8-16.8z"/><circle 
cx="85.9" cy="15.9" r="2.6"/><path d="M89.2 
40.9s-3.3-16.6-24.9-12.1c-2.2-7.5-1.8-13 3-18.1C73.8 3.8 84 7.6 84 
7.6V1.9C80.4.3 77 0 73.2 0 59.3 0 51.6 10.4 48.3 17.4L9.2 89.3l11-2.1-20.2 39 
14.1 [...]
+        </a>
+    </div>
+    <div class="header-link">
+        {% comment %}
+        This works for all cases but prerelease versions:
+        {% endcomment %}
+        <a href="{% wagtail_documentation_path %}/releases/{% 
wagtail_release_notes_path %}">
+            {% trans "View the release notes" %}
+        </a>
+    </div>
+</header>
+<main class="main">
+    <div class="figure">
+        <svg class="figure-space" xmlns="http://www.w3.org/2000/svg"; 
viewBox="0 0 300 300" aria-hidden="true">
+            <path class="egg" fill="currentColor" d="M150 250c-42.741 
0-75-32.693-75-90s42.913-110 75-110c32.088 0 75 52.693 75 110s-32.258 90-75 
90z"/>
+            <ellipse fill="#ddd" cx="150" cy="270" rx="40" ry="7"/>
+        </svg>
+    </div>
+    <div class="main-text">
+        <h1>{% trans "Welcome to your new Wagtail site!" %}</h1>
+        <p>{% trans 'Please feel free to <a 
href="https://github.com/wagtail/wagtail/wiki/Slack";>join our community on 
Slack</a>, or get started with one of the links below.' %}</p>
+    </div>
+</main>
+<footer class="footer" role="contentinfo">
+    <a class="option option-one" href="{% wagtail_documentation_path %}/">
+        <svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 24 24" 
aria-hidden="true"><path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 
2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 
3-3.4 3-5.7 0-3.9-3.1-7-7-7zm2.9 11.1l-.9.6V16h-4v-2.3l-.9-.6C7.8 12.2 7 10.6 7 
9c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.6-.8 3.2-2.1 4.1z"/></svg>
+        <div>
+            <h2>{% trans "Wagtail Documentation" %}</h2>
+            <p>{% trans "Topics, references, & how-tos" %}</p>
+        </div>
+    </a>
+    <a class="option option-two" href="{% wagtail_documentation_path 
%}/getting_started/tutorial.html">
+        <svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 24 24" 
aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 
16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 
6-1.4-1.4z"/></svg>
+        <div>
+            <h2>{% trans "Tutorial" %}</h2>
+            <p>{% trans "Build your first Wagtail site" %}</p>
+        </div>
+    </a>
+    <a class="option option-three" href="{% url 'wagtailadmin_home' %}">
+        <svg xmlns="http://www.w3.org/2000/svg"; viewBox="0 0 24 24" 
aria-hidden="true"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.5 13c-1.2 
0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 
16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 
5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 
3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 
4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2 [...]
+        <div>
+            <h2>{% trans "Admin Interface" %}</h2>
+            <p>{% trans "Create your superuser first!" %}</p>
+        </div>
+    </a>
+</footer>
diff --git a/airavata-cms/home/tests.py b/airavata-cms/home/tests.py
new file mode 100644
index 000000000..2a737de45
--- /dev/null
+++ b/airavata-cms/home/tests.py
@@ -0,0 +1,42 @@
+from home.models import HomePage
+
+from wagtail.models import Page, Site
+from wagtail.test.utils import WagtailPageTestCase
+
+
+class HomeSetUpTests(WagtailPageTestCase):
+    """
+    Tests for basic page structure setup and HomePage creation.
+    """
+
+    def test_root_create(self):
+        root_page = Page.objects.get(pk=1)
+        self.assertIsNotNone(root_page)
+
+    def test_homepage_create(self):
+        root_page = Page.objects.get(pk=1)
+        homepage = HomePage(title="Home")
+        root_page.add_child(instance=homepage)
+        self.assertTrue(HomePage.objects.filter(title="Home").exists())
+
+
+class HomeTests(WagtailPageTestCase):
+    """
+    Tests for homepage functionality and rendering.
+    """
+
+    def setUp(self):
+        """
+        Create a homepage instance for testing.
+        """
+        root_page = Page.get_first_root_node()
+        Site.objects.create(hostname="testsite", root_page=root_page, 
is_default_site=True)
+        self.homepage = HomePage(title="Home")
+        root_page.add_child(instance=self.homepage)
+
+    def test_homepage_is_renderable(self):
+        self.assertPageIsRenderable(self.homepage)
+
+    def test_homepage_template_used(self):
+        response = self.client.get(self.homepage.url)
+        self.assertTemplateUsed(response, "home/home_page.html")
diff --git a/airavata-cms/manage.py b/airavata-cms/manage.py
new file mode 100755
index 000000000..f3680bb72
--- /dev/null
+++ b/airavata-cms/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    """Run administrative tasks."""
+    os.environ.setdefault("DJANGO_SETTINGS_MODULE", 
"airavata_cms.settings.dev")
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/airavata-cms/requirements.txt b/airavata-cms/requirements.txt
new file mode 100644
index 000000000..d4cdd6f08
--- /dev/null
+++ b/airavata-cms/requirements.txt
@@ -0,0 +1,2 @@
+Django>=6,<6.1
+wagtail>=7.4,<7.5
diff --git a/airavata-cms/search/__init__.py b/airavata-cms/search/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/airavata-cms/search/templates/search/search.html 
b/airavata-cms/search/templates/search/search.html
new file mode 100644
index 000000000..476427f25
--- /dev/null
+++ b/airavata-cms/search/templates/search/search.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags %}
+
+{% block body_class %}template-searchresults{% endblock %}
+
+{% block title %}Search{% endblock %}
+
+{% block content %}
+<h1>Search</h1>
+
+<form action="{% url 'search' %}" method="get">
+    <input type="text" name="query"{% if search_query %} value="{{ 
search_query }}"{% endif %}>
+    <input type="submit" value="Search" class="button">
+</form>
+
+{% if search_results %}
+<ul>
+    {% for result in search_results %}
+    <li>
+        <h4><a href="{% pageurl result %}">{{ result }}</a></h4>
+        {% if result.search_description %}
+        {{ result.search_description }}
+        {% endif %}
+    </li>
+    {% endfor %}
+</ul>
+
+{% if search_results.has_previous %}
+<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ 
search_results.previous_page_number }}">Previous</a>
+{% endif %}
+
+{% if search_results.has_next %}
+<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ 
search_results.next_page_number }}">Next</a>
+{% endif %}
+{% elif search_query %}
+No results found
+{% endif %}
+{% endblock %}
diff --git a/airavata-cms/search/views.py b/airavata-cms/search/views.py
new file mode 100644
index 000000000..678bb7e87
--- /dev/null
+++ b/airavata-cms/search/views.py
@@ -0,0 +1,46 @@
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
+from django.template.response import TemplateResponse
+
+from wagtail.models import Page
+
+# To enable logging of search queries for use with the "Promoted search 
results" module
+# <https://docs.wagtail.org/en/stable/reference/contrib/searchpromotions.html>
+# uncomment the following line and the lines indicated in the search function
+# (after adding wagtail.contrib.search_promotions to INSTALLED_APPS):
+
+# from wagtail.contrib.search_promotions.models import Query
+
+
+def search(request):
+    search_query = request.GET.get("query", None)
+    page = request.GET.get("page", 1)
+
+    # Search
+    if search_query:
+        search_results = Page.objects.live().search(search_query)
+
+        # To log this query for use with the "Promoted search results" module:
+
+        # query = Query.get(search_query)
+        # query.add_hit()
+
+    else:
+        search_results = Page.objects.none()
+
+    # Pagination
+    paginator = Paginator(search_results, 10)
+    try:
+        search_results = paginator.page(page)
+    except PageNotAnInteger:
+        search_results = paginator.page(1)
+    except EmptyPage:
+        search_results = paginator.page(paginator.num_pages)
+
+    return TemplateResponse(
+        request,
+        "search/search.html",
+        {
+            "search_query": search_query,
+            "search_results": search_results,
+        },
+    )


Reply via email to