Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Django Ninja templates #782

Open
wants to merge 12 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same typo here:
init_listner -> init_listener

Copy link
Member

@afonsobspinto afonsobspinto Nov 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "__APP_NAME__.settings")
should actually be:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_baseapp.settings")
since that's the place where the settings are defined

File renamed without changes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the problem is not caused by this PR but there's a typo in this file:
init_listner should be init_listener. Would it be possible to fix it here?

File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ See [backend/README.md#Develop]

### Frontend

Backend code is inside the *frontend* directory.
Frontend code is inside the *frontend* directory.

Frontend is by default generated as a React web application, but no constraint about this specific technology.

Expand Down
35 changes: 35 additions & 0 deletions application-templates/django-ninja/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
ARG MNP_UI
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there's a reason for the renaming but without any context on that my preference would be to keep the more generic name: CLOUDHARNESS_FRONTEND_BUILD instead

ARG CLOUDHARNESS_DJANGO

FROM $MNP_UI AS frontend

ARG APP_DIR=/app

WORKDIR ${APP_DIR}
COPY frontend/package.json .
COPY frontend/yarn.lock .
RUN yarn install --timeout 60000

COPY frontend .
RUN yarn build

#####

FROM $CLOUDHARNESS_DJANGO

WORKDIR ${APP_DIR}
RUN mkdir -p ${APP_DIR}/static/www

COPY backend/requirements.txt ${APP_DIR}
RUN --mount=type=cache,target=/root/.cache python -m pip install --upgrade pip &&\
pip3 install --no-cache-dir -r requirements.txt --prefer-binary

COPY backend/requirements.txt backend/setup.py ${APP_DIR}
RUN python3 -m pip install -e .

COPY backend ${APP_DIR}
RUN python3 manage.py collectstatic --noinput

COPY --from=frontend /app/dist ${APP_DIR}/static/www

ENTRYPOINT uvicorn --workers ${WORKERS} --host 0.0.0.0 --port ${PORT} django_baseapp.asgi:application
86 changes: 86 additions & 0 deletions application-templates/django-ninja/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# __APP_NAME__

Django-Ninja/React-based web application.
This application is constructed to be deployed inside a cloud-harness Kubernetes.
It can be also run locally for development and test purpose.

The code is generated with the script `harness-application`.

## Configuration

### Accounts

The CloudHarness Django application template comes with a configuration that can retrieve user account updates from Keycloak (accounts)
To enable this feature:
* log in into the accounts admin interface
* select in the left sidebar Events
* select the `Config` tab
* enable "metacell-admin-event-listener" under the `Events Config` - `Event Listeners`

An other option is to enable the "metacell-admin-event-listener" through customizing the Keycloak realm.json from the CloudHarness repository.

## Develop

This application is composed of a Django-Ninja backend and a React frontend.

### Backend

Backend code is inside the *backend* directory.
See [backend/README.md#Develop]

### Frontend

Frontend code is inside the *frontend* directory.

Frontend is by default generated as a React web application, but no constraint about this specific technology.

#### Call the backend apis
All the api stubs are automatically generated in the [frontend/rest](frontend/rest) directory by `harness-application`
and `harness-generate`.

## Local build & run

### Install dependencies
1 - Clone cloud-harness into your project root folder

2 - Run the dev setup script
```
cd applications/__APP_NAME__
bash dev-setup.sh
```

### Prepare backend

Create a Django local superuser account, this you only need to do on initial setup
```bash
cd backend
python3 manage.py migrate # to sync the database with the Django models
python3 manage.py collectstatic --noinput # to copy all assets to the static folder
python3 manage.py createsuperuser
# link the frontend dist to the django static folder, this is only needed once, frontend updates will automatically be applied
cd static/www
ln -s ../../../frontend/dist dist
```

### Build frontend

Compile the frontend
```bash
cd frontend
npm install
npm run build
```

### Run backend application

start the Django server
```bash
uvicorn --workers 2 --host 0.0.0.0 --port 8000 django_baseapp.asgi:application
```


### Running local with port forwardings to a kubernetes cluster
When you create port forwards to microservices in your k8s cluster you want to forced your local backend server to initialize
the AuthService and EventService services.
This can be done by setting the `KUBERNETES_SERVICE_HOST` environment variable to a dummy or correct k8s service host.
The `KUBERNETES_SERVICE_HOST` switch will activate the creation of the keycloak client and client roles of this microservice.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import time
from django.http import HttpRequest
from ninja import NinjaAPI
from ..exceptions import Http401, Http403


api = NinjaAPI(title='__APP_NAME__ API', version='0.1.0')


@api.exception_handler(Http401)
def unauthorized(request, exc):
return api.create_response(
request,
{'message': 'Unauthorized'},
status=401,
)


@api.exception_handler(Http403)
def forbidden(request, exc):
return api.create_response(
request,
{'message': 'Forbidden'},
status=403,
)


@api.get('/ping', response={200: float}, tags=['test'])
def ping(request: HttpRequest):
return time.time()


@api.get('/live', response={200: str}, tags=['test'])
def live(request: HttpRequest):
return 'OK'


@api.get('/ready', response={200: str}, tags=['test'])
def ready(request: HttpRequest):
return 'OK'
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class Http401(Exception):
pass


class Http403(Exception):
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ninja import Schema

# Create your schema here
167 changes: 167 additions & 0 deletions application-templates/django-ninja/backend/django_baseapp/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""
Django settings for the MNP Checkout project.

Generated by 'django-admin startproject' using Django 3.2.12.

For more information on this file, see
https://docs.djangoproject.com/en/3.2/topics/settings/

For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.2/ref/settings/
"""

import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent


# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "django-insecure-81kv$0=07xac7r(pgz6ndb5t0at4-z@ae6&f@u6_3jo&9d#4kl"

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False if os.environ.get("PRODUCTION", None) else True

ALLOWED_HOSTS = [
"*",
]

# Application definition

INSTALLED_APPS = [
"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",
"cloudharness.middleware.django.CloudharnessMiddleware",
]


ROOT_URLCONF = "django_baseapp.urls"

TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_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 = "django_baseapp.wsgi.application"


# Password validation
# https://docs.djangoproject.com/en/3.2/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/3.2/topics/i18n/

LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"

USE_I18N = True

USE_L10N = True

USE_TZ = True

# Default primary key field type
# https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field

DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"


PROJECT_NAME = "__APP_NAME__".upper()

# Persistent storage
PERSISTENT_ROOT = os.path.join(BASE_DIR, "persistent")

# ***********************************************************************
# * __APP_NAME__ settings
# ***********************************************************************
from cloudharness.applications import get_configuration # noqa E402
from cloudharness.utils.config import ALLVALUES_PATH, CloudharnessConfig # noqa E402

# ***********************************************************************
# * import base CloudHarness Django settings
# ***********************************************************************
from cloudharness_django.settings import * # noqa E402

# add the local apps
INSTALLED_APPS += [
"__APP_NAME__",
"django_baseapp",
"ninja",
]

# override django admin base template with a local template
# to add some custom styling
TEMPLATES[0]["DIRS"] = [BASE_DIR / "templates"]

# Static files (CSS, JavaScript, Images)
MEDIA_ROOT = PERSISTENT_ROOT
STATIC_ROOT = os.path.join(BASE_DIR, "static")
MEDIA_URL = "/media/"
STATIC_URL = "/static/"

# KC Client & roles
KC_CLIENT_NAME = PROJECT_NAME.lower()

# __APP_NAME__ specific roles

# Default KC roles
KC_ADMIN_ROLE = f"{KC_CLIENT_NAME}-administrator" # admin user
KC_MANAGER_ROLE = f"{KC_CLIENT_NAME}-manager" # manager user
KC_USER_ROLE = f"{KC_CLIENT_NAME}-user" # customer user
KC_ALL_ROLES = [
KC_ADMIN_ROLE,
KC_MANAGER_ROLE,
KC_USER_ROLE,
]
KC_PRIVILEGED_ROLES = [
KC_ADMIN_ROLE,
KC_MANAGER_ROLE,
]

KC_DEFAULT_USER_ROLE = None # don't add the user role to the realm default role
31 changes: 31 additions & 0 deletions application-templates/django-ninja/backend/django_baseapp/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""MNP Checkout URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
import re
from django.conf import settings
from django.views.static import serve
from django.contrib import admin
from django.urls import path, re_path
from __APP_NAME__.api import api
from django_baseapp.views import index


urlpatterns = [
path("admin/", admin.site.urls),
path("api/", api.urls),
re_path(r"^%s(?P<path>.*)$" % re.escape(settings.MEDIA_URL.lstrip("/")), serve, kwargs=dict(document_root=settings.MEDIA_ROOT)),
re_path(r"^%s(?P<path>.*)$" % re.escape(settings.STATIC_URL.lstrip("/")), serve, kwargs=dict(document_root=settings.STATIC_ROOT)),
re_path(r"^(?P<path>.*)$", index, name="index"),
]
2 changes: 2 additions & 0 deletions application-templates/django-ninja/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pydantic==2.9.2
django-ninja
Copy link
Member

@afonsobspinto afonsobspinto Nov 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to have cloudharness and cloudharness_django as dependencies here? Since they are conceptually required to run the application (f.e. they are used in settings.py) cc @ddelpiano

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect so. I understand we might need CH repo locally if I am in need of regen the template files or the codefresh pipeline, but if I am purely working on the backend I might not need CH and such dependency should be part of the requirements.txt so that all I need to run the backend is already covered by that. Thanks!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree it's better to be explicit about all the dependencies that are imported, even if they are inherited by the base image (like this case)

Loading
Loading