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 }}&page={{
search_results.previous_page_number }}">Previous</a>
+{% endif %}
+
+{% if search_results.has_next %}
+<a href="{% url 'search' %}?query={{ search_query|urlencode }}&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,
+ },
+ )