mirror of
https://github.com/YunoHost-Apps/django_example_ynh.git
synced 2024-09-03 18:26:21 +02:00
WIP: rename/split from django_ynh
See discussion here: https://github.com/YunoHost-Apps/django_ynh/pull/13 Corresponding to: https://github.com/jedie/django_yunnohost_integration/pull/1
This commit is contained in:
parent
eb3fe62cac
commit
a8164a2905
39 changed files with 1214 additions and 906 deletions
4
.flake8
4
.flake8
|
@ -2,6 +2,6 @@
|
|||
# Move to pyproject.toml after: https://gitlab.com/pycqa/flake8/-/issues/428
|
||||
#
|
||||
[flake8]
|
||||
exclude = .pytest_cache, .tox, dist, htmlcov, */migrations/*
|
||||
#ignore = E402
|
||||
exclude = .pytest_cache, .tox, dist, htmlcov, local_test
|
||||
ignore = F405
|
||||
max-line-length = 119
|
||||
|
|
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -3,11 +3,8 @@
|
|||
!.editorconfig
|
||||
!.flake8
|
||||
!.gitignore
|
||||
coverage.xml
|
||||
__pycache__
|
||||
secret.txt
|
||||
/htmlcov/
|
||||
/local_test/
|
||||
/dist/
|
||||
/poetry.lock
|
||||
*.log
|
||||
/coverage.xml
|
||||
/htmlcov/
|
||||
|
|
5
Makefile
5
Makefile
|
@ -31,7 +31,7 @@ update: install-poetry ## update the sources and installation and generate "con
|
|||
lint: ## Run code formatters and linter
|
||||
poetry run flynt --fail-on-change --line_length=${MAX_LINE_LENGTH} .
|
||||
poetry run isort --check-only .
|
||||
poetry run flake8 django_ynh
|
||||
poetry run flake8 .
|
||||
|
||||
fix-code-style: ## Fix code formatting
|
||||
poetry run flynt --line_length=${MAX_LINE_LENGTH} .
|
||||
|
@ -47,9 +47,6 @@ tox: check-poetry ## Run pytest via tox with all environments
|
|||
pytest: install ## Run pytest
|
||||
poetry run python3 ./run_pytest.py
|
||||
|
||||
publish: ## Release new version to PyPi
|
||||
poetry run python3 ./publish.py
|
||||
|
||||
local-test: install ## Run local_test.py to run the project locally
|
||||
poetry run python3 ./local_test.py
|
||||
|
||||
|
|
31
README.md
31
README.md
|
@ -1,16 +1,9 @@
|
|||
# django_ynh
|
||||
# django_example_ynh
|
||||
|
||||
Demo [YunoHost Application](https://install-app.yunohost.org/?app=django_example_ynh) to demonstrate the integration of a Django project under YunoHost.
|
||||
|
||||
Glue code to package django projects as yunohost apps.
|
||||
|
||||
This repository is:
|
||||
|
||||
* The Python package [django-ynh](https://pypi.org/project/django-ynh/) with helpers for integrate a Django project as YunoHost package
|
||||
* A example [YunoHost Application](https://install-app.yunohost.org/?app=django_ynh) that can be installed
|
||||
|
||||
|
||||
[![Integration level](https://dash.yunohost.org/integration/django_ynh.svg)](https://dash.yunohost.org/appci/app/django_ynh) ![](https://ci-apps.yunohost.org/ci/badges/django_ynh.status.svg) ![](https://ci-apps.yunohost.org/ci/badges/django_ynh.maintain.svg)
|
||||
[![Install django_ynh with YunoHost](https://install-app.yunohost.org/install-with-yunohost.svg)](https://install-app.yunohost.org/?app=django_ynh)
|
||||
[![Integration level](https://dash.yunohost.org/integration/django_example_ynh.svg)](https://dash.yunohost.org/appci/app/django_example_ynh) ![](https://ci-apps.yunohost.org/ci/badges/django_example_ynh.status.svg) ![](https://ci-apps.yunohost.org/ci/badges/django_example_ynh.maintain.svg)
|
||||
[![Install django_example_ynh with YunoHost](https://install-app.yunohost.org/install-with-yunohost.svg)](https://install-app.yunohost.org/?app=django_example_ynh)
|
||||
|
||||
|
||||
Pull requests welcome ;)
|
||||
|
@ -92,9 +85,9 @@ and urls made for YunoHost installation.
|
|||
|
||||
e.g.:
|
||||
```bash
|
||||
~$ git clone https://github.com/YunoHost-Apps/django_ynh.git
|
||||
~$ git clone https://github.com/YunoHost-Apps/django_example_ynh.git
|
||||
~$ cd django_ynh/
|
||||
~/django_ynh$ make
|
||||
~/django_example_ynh$ make
|
||||
install-poetry install or update poetry
|
||||
install install project via poetry
|
||||
update update the sources and installation and generate "conf/requirements.txt"
|
||||
|
@ -107,9 +100,9 @@ publish Release new version to PyPi
|
|||
local-test Run local_test.py to run the project locally
|
||||
local-diff-settings Run "manage.py diffsettings" with local test
|
||||
|
||||
~/django_ynh$ make install-poetry
|
||||
~/django_ynh$ make install
|
||||
~/django_ynh$ make local-test
|
||||
~/django_example_ynh$ make install-poetry
|
||||
~/django_example_ynh$ make install
|
||||
~/django_example_ynh$ make local-test
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
@ -123,6 +116,10 @@ Notes:
|
|||
|
||||
* [compare v0.1.5...master](https://github.com/YunoHost-Apps/django_ynh/compare/v0.1.5...master) **dev**
|
||||
* tbc
|
||||
* v0.2.0.alpha0 **dev**
|
||||
* rename/split `django_ynh` into:
|
||||
* `django_yunohost_integration` - Python package with the glue code to integrate a Django project with YunoHost
|
||||
* `django_example_ynh` - Demo YunoHost App to demonstrate the integration of a Django project under YunoHost
|
||||
* [v0.1.5 - 19.01.2021](https://github.com/YunoHost-Apps/django_ynh/compare/v0.1.4...v0.1.5)
|
||||
* Make some deps `gunicorn`, `psycopg2-binary`, `django-redis`, `django-axes` optional
|
||||
* [v0.1.4 - 08.01.2021](https://github.com/YunoHost-Apps/django_ynh/compare/v0.1.3...v0.1.4)
|
||||
|
@ -150,7 +147,7 @@ Notes:
|
|||
|
||||
These projects used `django_ynh`:
|
||||
|
||||
* https://github.com/YunoHost-Apps/pyinventory_ynh
|
||||
* https://github.com/YunoHost-Apps/django_example_ynh
|
||||
* https://github.com/YunoHost-Apps/django-for-runners_ynh
|
||||
|
||||
---
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[Unit]
|
||||
Description=django_ynh DEMO application server
|
||||
Description=PyInventory application server
|
||||
After=redis.service postgresql.service
|
||||
|
||||
[Service]
|
|
@ -5,9 +5,19 @@ location __PATH__/static/ {
|
|||
expires 30d;
|
||||
}
|
||||
|
||||
# TODO: django-sendfile2:
|
||||
#location __PATH__/media/ {
|
||||
# # DATA_DIR/media/
|
||||
# alias __PUBLIC_PATH__/media/;
|
||||
# expires 30d;
|
||||
#}
|
||||
|
||||
location __PATH__/ {
|
||||
# https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf
|
||||
|
||||
# this is needed if you have file import via upload enabled
|
||||
client_max_body_size 100M;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
asgiref==3.3.1; python_version >= "3.6" and python_version < "4.0" \
|
||||
asgiref==3.3.1; python_version >= "3.7" and python_full_version < "4.0.0" \
|
||||
--hash=sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17 \
|
||||
--hash=sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0
|
||||
django-axes==5.10.0; python_version >= "3.6" and python_version < "4.0" \
|
||||
--hash=sha256:8a62cd4cc78ef08007e8102def34be83832995eb6e3e0c814d605741b82a2796 \
|
||||
--hash=sha256:3c81ddca8a9d7fd0019cb440f711cc873c3039546f7eacb3f2ec616bf0ec1b32
|
||||
django-ipware==3.0.2; python_version >= "3.6" and python_version < "4.0" \
|
||||
django-axes==5.13.1; python_version >= "3.7" and python_version < "4.0" and python_full_version < "4.0.0" \
|
||||
--hash=sha256:8f7870dc18ace6155127073bffe7276719c71c5ad4e67b45abedc0207f64a2f6 \
|
||||
--hash=sha256:aef814f738742b01cc7730cd2bebe3e5b462b807fd6c2ed609b62437ccc71f80
|
||||
django-ipware==3.0.2; python_version >= "3.7" and python_version < "4.0" and python_full_version < "4.0.0" \
|
||||
--hash=sha256:c7df8e1410a8e5d6b1fbae58728402ea59950f043c3582e033e866f0f0cf5e94
|
||||
django-redis==4.12.1; python_version >= "3.5" \
|
||||
django-redis==4.12.1; python_version >= "3.7" and python_full_version < "4.0.0" \
|
||||
--hash=sha256:306589c7021e6468b2656edc89f62b8ba67e8d5a1c8877e2688042263daa7a63 \
|
||||
--hash=sha256:1133b26b75baa3664164c3f44b9d5d133d1b8de45d94d79f38d1adc5b1d502e5
|
||||
django==3.1.4; python_version >= "3.6" \
|
||||
--hash=sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2 \
|
||||
--hash=sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03
|
||||
gunicorn==20.0.4; python_version >= "3.4" \
|
||||
django-yunohost-integration==0.2.0a0; python_version >= "3.7" and python_full_version < "4.0.0" \
|
||||
--hash=sha256:a45197a3c4595a496674e7e48a58f58351b6022526893264831e0d6ba463a44f \
|
||||
--hash=sha256:9acdf320537ccce47ceb1d51d2a00fafbf30938152d3b22c59a3a2ead7952227
|
||||
django==3.1.7; python_version >= "3.7" and python_full_version < "4.0.0" and python_version < "4.0" \
|
||||
--hash=sha256:baf099db36ad31f970775d0be5587cc58a6256a6771a44eb795b554d45f211b8 \
|
||||
--hash=sha256:32ce792ee9b6a0cbbec340123e229ac9f765dff8c2a4ae9247a14b2ba3a365a7
|
||||
gunicorn==20.0.4; python_version >= "3.7" and python_full_version < "4.0.0" \
|
||||
--hash=sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c \
|
||||
--hash=sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626
|
||||
psycopg2-binary==2.8.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \
|
||||
psycopg2-binary==2.8.6; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.4.0" \
|
||||
--hash=sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0 \
|
||||
--hash=sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4 \
|
||||
--hash=sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db \
|
||||
|
@ -51,12 +54,12 @@ psycopg2-binary==2.8.6; (python_version >= "2.7" and python_full_version < "3.0.
|
|||
--hash=sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd \
|
||||
--hash=sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056 \
|
||||
--hash=sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6
|
||||
pytz==2020.5; python_version >= "3.6" and python_version < "4.0" \
|
||||
--hash=sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4 \
|
||||
--hash=sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5
|
||||
redis==3.5.3; python_version >= "3.5" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.5" \
|
||||
pytz==2021.1; python_version >= "3.7" and python_full_version < "4.0.0" \
|
||||
--hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798 \
|
||||
--hash=sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da
|
||||
redis==3.5.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_version >= "3.7" and python_full_version < "4.0.0" and python_full_version >= "3.5.0" \
|
||||
--hash=sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24 \
|
||||
--hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2
|
||||
sqlparse==0.4.1; python_version >= "3.6" and python_version < "4.0" \
|
||||
sqlparse==0.4.1; python_version >= "3.7" and python_full_version < "4.0.0" \
|
||||
--hash=sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0 \
|
||||
--hash=sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8
|
||||
|
|
|
@ -1,24 +1,21 @@
|
|||
"""
|
||||
**************************************************************************
|
||||
Please do not modify this file, it will be reset at the next update.
|
||||
You can edit the file __FINAL_HOME_PATH__/local_settings.py and add/modify
|
||||
the settings you need.
|
||||
################################################################################
|
||||
################################################################################
|
||||
|
||||
The parameters you add in local_settings.py will overwrite these,
|
||||
but you can use the options and documentation in this file to find out
|
||||
what can be done.
|
||||
**************************************************************************
|
||||
# Please do not modify this file, it will be reset at the next update.
|
||||
# You can edit the file __FINAL_HOME_PATH__/local_settings.py and add/modify the settings you need.
|
||||
# The parameters you add in local_settings.py will overwrite these,
|
||||
# but you can use the options and documentation in this file to find out what can be done.
|
||||
|
||||
################################################################################
|
||||
################################################################################
|
||||
|
||||
Django Settings here depends on YunoHost app settings.
|
||||
"""
|
||||
from pathlib import Path as __Path
|
||||
|
||||
from django_ynh.base_settings import * # noqa
|
||||
from django_ynh.secret_key import get_or_create_secret as __get_or_create_secret
|
||||
from inventory_project.settings.base import * # noqa
|
||||
|
||||
|
||||
DEBUG = True # This is only the DEMO app ;) But should never be on in production!
|
||||
|
||||
DEBUG = False # Don't turn DEBUG on in production!
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
@ -28,7 +25,7 @@ assert FINAL_HOME_PATH.is_dir(), f'Directory not exists: {FINAL_HOME_PATH}'
|
|||
FINAL_WWW_PATH = __Path('__FINAL_WWW_PATH__') # /var/www/$app
|
||||
assert FINAL_WWW_PATH.is_dir(), f'Directory not exists: {FINAL_WWW_PATH}'
|
||||
|
||||
LOG_FILE = __Path('__LOG_FILE__') # /var/log/$app/django_ynh.log
|
||||
LOG_FILE = __Path('__LOG_FILE__') # /var/log/$app/django_example_ynh.log
|
||||
assert LOG_FILE.is_file(), f'File not exists: {LOG_FILE}'
|
||||
|
||||
PATH_URL = '__PATH_URL__' # $YNH_APP_ARG_PATH
|
||||
|
@ -36,11 +33,40 @@ PATH_URL = PATH_URL.strip('/')
|
|||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
ROOT_URLCONF = 'urls' # /opt/yunohost/django_example_ynh/ynh_urls.py
|
||||
|
||||
# Function that will be called to finalize a user profile:
|
||||
YNH_SETUP_USER = 'setup_user.setup_demo_user'
|
||||
YNH_SETUP_USER = 'setup_user.setup_project_user'
|
||||
|
||||
SECRET_KEY = __get_or_create_secret(FINAL_HOME_PATH / 'secret.txt') # /opt/yunohost/$app/secret.txt
|
||||
|
||||
INSTALLED_APPS.append('django_ynh')
|
||||
|
||||
MIDDLEWARE.insert(
|
||||
MIDDLEWARE.index('django.contrib.auth.middleware.AuthenticationMiddleware') + 1,
|
||||
# login a user via HTTP_REMOTE_USER header from SSOwat:
|
||||
'django_ynh.sso_auth.auth_middleware.SSOwatRemoteUserMiddleware',
|
||||
)
|
||||
|
||||
# Keep ModelBackend around for per-user permissions and superuser
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'axes.backends.AxesBackend', # AxesBackend should be the first backend!
|
||||
#
|
||||
# Authenticate via SSO and nginx 'HTTP_REMOTE_USER' header:
|
||||
'django_ynh.sso_auth.auth_backend.SSOwatUserBackend',
|
||||
#
|
||||
# Fallback to normal Django model backend:
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
LOGIN_REDIRECT_URL = None
|
||||
LOGIN_URL = '/yunohost/sso/'
|
||||
LOGOUT_REDIRECT_URL = '/yunohost/sso/'
|
||||
# /yunohost/sso/?action=logout
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
ADMINS = (('__ADMIN__', '__ADMINMAIL__'),)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
@ -77,14 +103,13 @@ DEFAULT_FROM_EMAIL = '__ADMINMAIL__'
|
|||
# List of URLs your site is supposed to serve
|
||||
ALLOWED_HOSTS = ['__DOMAIN__']
|
||||
|
||||
|
||||
# _____________________________________________________________________________
|
||||
# Configuration for caching
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': 'redis://127.0.0.1:6379/__REDIS_DB__',
|
||||
# If redis is running on same host as django_ynh, you might
|
||||
# If redis is running on same host as PyInventory, you might
|
||||
# want to use unix sockets instead:
|
||||
# 'LOCATION': 'unix:///var/run/redis/redis.sock?db=1',
|
||||
'OPTIONS': {
|
||||
|
@ -94,7 +119,6 @@ CACHES = {
|
|||
},
|
||||
}
|
||||
|
||||
|
||||
# _____________________________________________________________________________
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
|
||||
|
@ -109,10 +133,18 @@ else:
|
|||
STATIC_ROOT = str(FINAL_WWW_PATH / 'static')
|
||||
MEDIA_ROOT = str(FINAL_WWW_PATH / 'media')
|
||||
|
||||
# _____________________________________________________________________________
|
||||
# django-ckeditor
|
||||
|
||||
CKEDITOR_BASEPATH = STATIC_URL + 'ckeditor/ckeditor/'
|
||||
|
||||
# _____________________________________________________________________________
|
||||
# Django-dbbackup
|
||||
|
||||
DBBACKUP_STORAGE_OPTIONS['location'] = str(FINAL_HOME_PATH / 'backups')
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': True,
|
||||
|
@ -142,6 +174,7 @@ LOGGING = {
|
|||
'axes': {'handlers': ['log_file', 'mail_admins'], 'level': 'WARNING', 'propagate': False},
|
||||
'django_tools': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
|
||||
'django_ynh': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
|
||||
'inventory': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
def setup_demo_user(user):
|
||||
def setup_project_user(user):
|
||||
"""
|
||||
The django_ynh DEMO use the Django admin. So we need a "staff" user ;)
|
||||
All users used the Django admin, so we need to set the "staff" user flag.
|
||||
Called from django_ynh.sso_auth
|
||||
"""
|
||||
user.is_staff = True
|
||||
user.save()
|
||||
|
|
|
@ -2,7 +2,7 @@ from django.conf import settings
|
|||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from django_ynh.views import request_media_debug_view
|
||||
from django_yunohost_integration.views import request_media_debug_view
|
||||
|
||||
|
||||
# settings.PATH_URL is the $YNH_APP_ARG_PATH
|
||||
|
|
|
@ -6,7 +6,7 @@ import os
|
|||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
from django.core.wsgi import get_wsgi_application # noqa
|
||||
|
||||
|
||||
application = get_wsgi_application()
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
__version__ = '0.1.5'
|
|
@ -1,102 +0,0 @@
|
|||
"""
|
||||
Base settings for a Django project installed in Yunohost.
|
||||
All values should not depent on YunoHost app settings.
|
||||
"""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# settings that should be set in project settings:
|
||||
|
||||
SECRET_KEY = None
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
ROOT_URLCONF = 'urls' # .../conf/urls.py
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'axes', # https://github.com/jazzband/django-axes
|
||||
'django_ynh',
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
#
|
||||
# login a user via HTTP_REMOTE_USER header from SSOwat:
|
||||
'django_ynh.sso_auth.auth_middleware.SSOwatRemoteUserMiddleware',
|
||||
#
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
#
|
||||
# AxesMiddleware should be the last middleware:
|
||||
'axes.middleware.AxesMiddleware',
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'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',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
# Keep ModelBackend around for per-user permissions and superuser
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'axes.backends.AxesBackend', # AxesBackend should be the first backend!
|
||||
#
|
||||
# Authenticate via SSO and nginx 'HTTP_REMOTE_USER' header:
|
||||
'django_ynh.sso_auth.auth_backend.SSOwatUserBackend',
|
||||
#
|
||||
# Fallback to normal Django model backend:
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
)
|
||||
|
||||
LOGIN_REDIRECT_URL = None
|
||||
LOGIN_URL = '/yunohost/sso/'
|
||||
LOGOUT_REDIRECT_URL = '/yunohost/sso/'
|
||||
# /yunohost/sso/?action=logout
|
||||
|
||||
# _____________________________________________________________________________
|
||||
# Setting below, should be overwritten!
|
||||
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'formatters': {
|
||||
'verbose': {
|
||||
'format': '{asctime} {levelname} {name} {module}.{funcName} {message}',
|
||||
'style': '{',
|
||||
},
|
||||
},
|
||||
'handlers': {'console': {'class': 'logging.StreamHandler', 'formatter': 'verbose'}},
|
||||
'loggers': {
|
||||
'django': {'handlers': ['console'], 'level': 'INFO', 'propagate': False},
|
||||
'django.auth': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
|
||||
'django.security': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
|
||||
'django.request': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
|
||||
'django_ynh': {'handlers': ['console'], 'level': 'DEBUG', 'propagate': False},
|
||||
},
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
"""
|
||||
Create a YunoHost package local test
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from django_ynh.path_utils import assert_is_dir, assert_is_file
|
||||
from django_ynh.test_utils import generate_basic_auth
|
||||
|
||||
|
||||
def verbose_check_call(command, verbose=True, **kwargs):
|
||||
""" 'verbose' version of subprocess.check_call() """
|
||||
if verbose:
|
||||
print('_' * 100)
|
||||
msg = f'Call: {command!r}'
|
||||
verbose_kwargs = ', '.join(f'{k}={v!r}' for k, v in sorted(kwargs.items()))
|
||||
if verbose_kwargs:
|
||||
msg += f' (kwargs: {verbose_kwargs})'
|
||||
print(f'{msg}\n', flush=True)
|
||||
|
||||
env = os.environ.copy()
|
||||
env['PYTHONUNBUFFERED'] = '1'
|
||||
|
||||
popenargs = shlex.split(command)
|
||||
subprocess.check_call(popenargs, universal_newlines=True, env=env, **kwargs)
|
||||
|
||||
|
||||
def call_manage_py(final_home_path, args):
|
||||
verbose_check_call(
|
||||
command=f'{sys.executable} manage.py {args}',
|
||||
cwd=final_home_path,
|
||||
)
|
||||
|
||||
|
||||
def copy_patch(src_file, replaces, final_home_path):
|
||||
dst_file = final_home_path / src_file.name
|
||||
print(f'{src_file} -> {dst_file}')
|
||||
|
||||
with src_file.open('r') as f:
|
||||
content = f.read()
|
||||
|
||||
if replaces:
|
||||
for old, new in replaces.items():
|
||||
if old in content:
|
||||
print(f' * Replace "{old}" -> "{new}"')
|
||||
content = content.replace(old, new)
|
||||
|
||||
with dst_file.open('w') as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def create_local_test(django_settings_path, destination, runserver=False):
|
||||
django_settings_path = django_settings_path.resolve()
|
||||
assert_is_file(django_settings_path)
|
||||
|
||||
django_settings_name = django_settings_path.stem
|
||||
|
||||
conf_path = django_settings_path.parent
|
||||
|
||||
project_name = conf_path.parent.name
|
||||
|
||||
assert isinstance(destination, Path)
|
||||
destination = destination.resolve()
|
||||
if not destination.is_dir():
|
||||
destination.mkdir(parents=False)
|
||||
|
||||
assert_is_dir(destination)
|
||||
|
||||
final_home_path = destination / 'opt_yunohost'
|
||||
final_www_path = destination / 'var_www'
|
||||
log_file = destination / f'var_log_{project_name}.log'
|
||||
|
||||
REPLACES = {
|
||||
'__FINAL_HOME_PATH__': str(final_home_path),
|
||||
'__FINAL_WWW_PATH__': str(final_www_path),
|
||||
'__LOG_FILE__': str(log_file),
|
||||
'__PATH_URL__': 'app_path',
|
||||
'__DOMAIN__': '127.0.0.1',
|
||||
'django.db.backends.postgresql': 'django.db.backends.sqlite3',
|
||||
"'NAME': '__APP__',": f"'NAME': '{destination / 'test_db.sqlite'}',",
|
||||
'django_redis.cache.RedisCache': 'django.core.cache.backends.dummy.DummyCache',
|
||||
# Just use the default logging setup from django_ynh project:
|
||||
'LOGGING = {': 'HACKED_DEACTIVATED_LOGGING = {',
|
||||
}
|
||||
|
||||
for p in (final_home_path, final_www_path):
|
||||
if p.is_dir():
|
||||
print(f'Already exists: "{p}", ok.')
|
||||
else:
|
||||
p.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
log_file.touch(exist_ok=True)
|
||||
|
||||
for src_file in conf_path.glob('*.py'):
|
||||
copy_patch(src_file=src_file, replaces=REPLACES, final_home_path=final_home_path)
|
||||
|
||||
with Path(final_home_path / 'local_settings.py').open('w') as f:
|
||||
f.write('# Only for local test run\n')
|
||||
f.write('DEBUG = True\n')
|
||||
f.write('SERVE_FILES = True # May used in urls.py\n')
|
||||
f.write('AUTH_PASSWORD_VALIDATORS = [] # accept all passwords\n')
|
||||
|
||||
# call "local_test/manage.py" via subprocess:
|
||||
call_manage_py(final_home_path, 'check --deploy')
|
||||
if runserver:
|
||||
call_manage_py(final_home_path, 'migrate --no-input')
|
||||
call_manage_py(final_home_path, 'collectstatic --no-input')
|
||||
call_manage_py(final_home_path, 'create_superuser --username="test"')
|
||||
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = django_settings_name
|
||||
|
||||
# All environment variables are passed to Django's "runnserver" ;)
|
||||
# "Simulate" SSOwat authentication, by set "http headers"
|
||||
# Still missing is the 'SSOwAuthUser' cookie,
|
||||
# but this is ignored, if settings.DEBUG=True ;)
|
||||
os.environ['HTTP_AUTH_USER'] = 'test'
|
||||
os.environ['HTTP_REMOTE_USER'] = 'test'
|
||||
|
||||
os.environ['HTTP_AUTHORIZATION'] = generate_basic_auth(username='test', password='test123')
|
||||
|
||||
try:
|
||||
call_manage_py(final_home_path, 'runserver')
|
||||
except KeyboardInterrupt:
|
||||
print('\nBye ;)')
|
||||
|
||||
return final_home_path
|
||||
|
||||
|
||||
def cli():
|
||||
parser = argparse.ArgumentParser(description='Generate a YunoHost package local test')
|
||||
|
||||
parser.add_argument(
|
||||
'--django_settings_path',
|
||||
action='store',
|
||||
metavar='path',
|
||||
help='Path to YunoHost package settings.py file (in "conf" directory)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--destination',
|
||||
action='store',
|
||||
metavar='path',
|
||||
help='Destination directory for the local test files',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--runserver',
|
||||
action='store',
|
||||
type=bool,
|
||||
default=False,
|
||||
help='Start Django "runserver" after local test file creation?',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
create_local_test(
|
||||
django_settings_path=Path(args.django_settings_path),
|
||||
destination=Path(args.destination),
|
||||
runserver=args.runserver,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
cli()
|
|
@ -1,50 +0,0 @@
|
|||
"""
|
||||
Create or update Django super user with a unusable password
|
||||
|
||||
A "unusable password" because it's not needed while auth via SSOwat ;)
|
||||
|
||||
Can be called e.g.:
|
||||
./manage.py create_superuser --username="bar" --email="foo@bar.tld"
|
||||
"""
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.management import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Create or update Django super user with a unusable password (auth via SSOwat)'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--username",
|
||||
action="store",
|
||||
required=True,
|
||||
)
|
||||
parser.add_argument(
|
||||
"--email",
|
||||
action="store",
|
||||
default=None,
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
username = options['username']
|
||||
email = options['email']
|
||||
|
||||
User = get_user_model()
|
||||
user = User.objects.filter(username=username).first()
|
||||
if user:
|
||||
self.stderr.write(f'Update existing user "{user}" and set his password.')
|
||||
user.is_active = True
|
||||
user.is_staff = True
|
||||
user.is_superuser = True
|
||||
if email:
|
||||
user.email = email
|
||||
else:
|
||||
print(f'Create new super user "{username}"', file=sys.stderr)
|
||||
user = User.objects.create_superuser(username=username, email=email, password=None)
|
||||
|
||||
user.set_unusable_password()
|
||||
user.save()
|
|
@ -1,25 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
def assert_is_dir(dir_path):
|
||||
assert isinstance(dir_path, Path)
|
||||
assert dir_path.is_dir, f'Directory does not exists: {dir_path}'
|
||||
|
||||
|
||||
def assert_is_file(file_path):
|
||||
assert isinstance(file_path, Path)
|
||||
assert file_path.is_file, f'File not found: {file_path}'
|
||||
|
||||
|
||||
def is_relative_to(p, other):
|
||||
"""
|
||||
Path.is_relative_to() is new in Python 3.9
|
||||
"""
|
||||
p = Path(p)
|
||||
other = Path(other)
|
||||
try:
|
||||
p.relative_to(other)
|
||||
except ValueError:
|
||||
return False
|
||||
else:
|
||||
return True
|
|
@ -1,38 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from django_ynh.local_test import create_local_test
|
||||
from django_ynh.path_utils import assert_is_dir, assert_is_file
|
||||
|
||||
|
||||
def run_pytest(django_settings_path, destination):
|
||||
"""
|
||||
1. Generate "local test installation"
|
||||
2. Run pytest against generated sources
|
||||
"""
|
||||
assert_is_file(django_settings_path)
|
||||
|
||||
conf_path = django_settings_path.parent
|
||||
base_path = conf_path.parent
|
||||
test_path = Path(base_path / 'tests')
|
||||
|
||||
assert_is_dir(test_path)
|
||||
|
||||
final_home_path = create_local_test(
|
||||
django_settings_path=django_settings_path,
|
||||
destination=destination,
|
||||
runserver=False,
|
||||
)
|
||||
django_settings_name = django_settings_path.stem
|
||||
os.environ['DJANGO_SETTINGS_MODULE'] = django_settings_name
|
||||
print(f'DJANGO_SETTINGS_MODULE={django_settings_name}')
|
||||
|
||||
sys.path.insert(0, str(final_home_path))
|
||||
|
||||
import pytest
|
||||
|
||||
# collect only project tests and pass existing pytest arguments:
|
||||
sys.argv = [__file__, str(test_path)] + sys.argv[1:]
|
||||
|
||||
raise SystemExit(pytest.console_main())
|
|
@ -1,23 +0,0 @@
|
|||
"""
|
||||
Helper to create a random string for settings.SECRET_KEY
|
||||
|
||||
SECURITY WARNING: keep the secret key used in production secret!
|
||||
"""
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from secrets import token_urlsafe
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_or_create_secret(secret_file):
|
||||
assert isinstance(secret_file, Path)
|
||||
assert secret_file.parent.is_dir, f'Directory does not exists: {secret_file.parent}'
|
||||
|
||||
if not secret_file.is_file():
|
||||
logger.info('Generate %s', secret_file)
|
||||
secret_file.open('w').write(token_urlsafe(128))
|
||||
|
||||
with secret_file.open('r') as f:
|
||||
return f.read()
|
|
@ -1,61 +0,0 @@
|
|||
"""
|
||||
remote user authentication backend
|
||||
|
||||
Note: SSOwat/nginx add authentication headers:
|
||||
|
||||
'HTTP_AUTHORIZATION': 'Basic XXXXXXXXXXXXXXXX='
|
||||
'HTTP_AUTH_USER': 'username'
|
||||
'HTTP_REMOTE_USER': 'username'
|
||||
|
||||
Basic auth contains "{username}:{plaintext-password}"
|
||||
|
||||
and we get SSOwat cookies like:
|
||||
|
||||
'HTTP_COOKIE': 'SSOwAuthUser=username; '
|
||||
'SSOwAuthHash=593876aa66...99e69f88af1e; '
|
||||
'SSOwAuthExpire=1609227697.998; '
|
||||
|
||||
* Login a user via HTTP_REMOTE_USER header, but check also username in:
|
||||
* SSOwAuthUser
|
||||
* HTTP_AUTH_USER
|
||||
* HTTP_AUTHORIZATION (Basic auth)
|
||||
* Create new users
|
||||
* Update Email, First / Last name for existing users
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from django.contrib.auth.backends import RemoteUserBackend
|
||||
|
||||
from django_ynh.sso_auth.user_profile import call_setup_user, update_user_profile
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SSOwatUserBackend(RemoteUserBackend):
|
||||
"""
|
||||
Authentication backend via SSO/nginx header
|
||||
"""
|
||||
|
||||
create_unknown_user = True
|
||||
|
||||
def authenticate(self, request, remote_user):
|
||||
logger.info('Remote user authenticate: %r', remote_user)
|
||||
return super().authenticate(request, remote_user)
|
||||
|
||||
def configure_user(self, request, user):
|
||||
"""
|
||||
Configure a new user after creation and return the updated user.
|
||||
"""
|
||||
logger.warning('Configure user %s', user)
|
||||
|
||||
user = update_user_profile(request, user)
|
||||
user = call_setup_user(user=user)
|
||||
|
||||
return user
|
||||
|
||||
def user_can_authenticate(self, user):
|
||||
logger.warning('Remote user login: %s', user)
|
||||
assert not user.is_anonymous
|
||||
return True
|
|
@ -1,101 +0,0 @@
|
|||
import base64
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.middleware import RemoteUserMiddleware
|
||||
|
||||
|
||||
try:
|
||||
from axes.exceptions import AxesBackendPermissionDenied as SuspiciousOperation # log to Axes DB models
|
||||
except ImportError:
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
from django_ynh.sso_auth.user_profile import call_setup_user, update_user_profile
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
class SSOwatRemoteUserMiddleware(RemoteUserMiddleware):
|
||||
"""
|
||||
Middleware to login a user via HTTP_REMOTE_USER header.
|
||||
Use Django Axes if something is wrong.
|
||||
Update exising user informations.
|
||||
"""
|
||||
|
||||
header = 'HTTP_REMOTE_USER'
|
||||
force_logout_if_no_header = True
|
||||
|
||||
def process_request(self, request):
|
||||
# Keep the information if the user is already logged in
|
||||
was_authenticated = request.user.is_authenticated
|
||||
|
||||
super().process_request(request) # login remote user
|
||||
|
||||
user = request.user
|
||||
|
||||
if not user.is_authenticated:
|
||||
logger.debug('Not logged in -> nothing to verify here')
|
||||
return
|
||||
|
||||
# Check SSOwat cookie informations:
|
||||
try:
|
||||
username = request.COOKIES['SSOwAuthUser']
|
||||
except KeyError:
|
||||
logger.error('SSOwAuthUser cookie missing!')
|
||||
|
||||
if settings.DEBUG:
|
||||
# e.g.: local test can't set a Cookie easily
|
||||
logger.warning('Ignore error, because settings.DEBUG is on!')
|
||||
else:
|
||||
# emits a signal indicating user login failed, which is processed by
|
||||
# axes.signals.log_user_login_failed which logs and flags the failed request.
|
||||
raise SuspiciousOperation('Cookie missing')
|
||||
else:
|
||||
logger.info('SSOwat username from cookies: %r', username)
|
||||
if username != user.username:
|
||||
raise SuspiciousOperation('Wrong username')
|
||||
|
||||
# Compare with HTTP_AUTH_USER
|
||||
try:
|
||||
username = request.META['HTTP_AUTH_USER']
|
||||
except KeyError:
|
||||
logger.error('HTTP_AUTH_USER missing!')
|
||||
raise SuspiciousOperation('No HTTP_AUTH_USER')
|
||||
|
||||
if username != user.username:
|
||||
raise SuspiciousOperation('Wrong HTTP_AUTH_USER username')
|
||||
|
||||
# Also check 'HTTP_AUTHORIZATION', but only the username ;)
|
||||
try:
|
||||
authorization = request.META['HTTP_AUTHORIZATION']
|
||||
except KeyError:
|
||||
logger.error('HTTP_AUTHORIZATION missing!')
|
||||
raise SuspiciousOperation('No HTTP_AUTHORIZATION')
|
||||
|
||||
scheme, creds = authorization.split(' ', 1)
|
||||
if scheme.lower() != 'basic':
|
||||
logger.error('HTTP_AUTHORIZATION with %r not supported', scheme)
|
||||
raise SuspiciousOperation('HTTP_AUTHORIZATION scheme not supported')
|
||||
|
||||
creds = str(base64.b64decode(creds), encoding='utf-8')
|
||||
username = creds.split(':', 1)[0]
|
||||
if username != user.username:
|
||||
raise SuspiciousOperation('Wrong HTTP_AUTHORIZATION username')
|
||||
|
||||
if not was_authenticated:
|
||||
# First request, after login -> update user informations
|
||||
logger.info('Remote user "%s" was logged in', user)
|
||||
user = update_user_profile(request, user)
|
||||
|
||||
user = call_setup_user(user=user)
|
||||
assert isinstance(user, UserModel)
|
||||
|
||||
# persist user in the session
|
||||
request.user = user
|
||||
auth.login(request, user)
|
|
@ -1,95 +0,0 @@
|
|||
import logging
|
||||
from functools import lru_cache
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
UserModel = get_user_model()
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_setup_user_func():
|
||||
setup_user_func = import_string(settings.YNH_SETUP_USER)
|
||||
assert callable(setup_user_func)
|
||||
return setup_user_func
|
||||
|
||||
|
||||
def call_setup_user(user):
|
||||
"""
|
||||
Hook for the YunoHost package application to setup a Django user.
|
||||
Call function defined in settings.YNH_SETUP_USER
|
||||
|
||||
called via:
|
||||
* SSOwatUserBackend after a new user was created
|
||||
* SSOwatRemoteUserMiddleware on login request
|
||||
"""
|
||||
old_pk = user.pk
|
||||
|
||||
setup_user_func = get_setup_user_func()
|
||||
logger.debug('Call "%s" for user "%s"', settings.YNH_SETUP_USER, user)
|
||||
|
||||
user = setup_user_func(user=user)
|
||||
|
||||
assert isinstance(user, UserModel)
|
||||
assert user.pk == old_pk
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def update_user_profile(request, user):
|
||||
"""
|
||||
Update existing user information:
|
||||
* Email
|
||||
* First / Last name
|
||||
|
||||
Called via:
|
||||
* SSOwatUserBackend after a new user was created
|
||||
* SSOwatRemoteUserMiddleware on login request
|
||||
"""
|
||||
update_fields = []
|
||||
|
||||
if user.is_authenticated and not user.has_usable_password():
|
||||
# Empty password is not valid, so we can't save the model, because of full_clean() call
|
||||
logger.info('Set unusable password for user: %s', user)
|
||||
user.set_unusable_password()
|
||||
update_fields.append('password')
|
||||
|
||||
email = request.META.get('HTTP_EMAIL')
|
||||
if email and user.email != email:
|
||||
logger.info('Update email: %r -> %r', user.email, email)
|
||||
user.email = email
|
||||
update_fields.append('email')
|
||||
|
||||
raw_username = request.META.get('HTTP_NAME')
|
||||
if raw_username:
|
||||
if ' ' in raw_username:
|
||||
first_name, last_name = raw_username.split(' ', 1)
|
||||
else:
|
||||
first_name = ''
|
||||
last_name = raw_username
|
||||
|
||||
if user.first_name != first_name:
|
||||
logger.info('Update first name: %r -> %r', user.first_name, first_name)
|
||||
user.first_name = first_name
|
||||
update_fields.append('first_name')
|
||||
|
||||
if user.last_name != last_name:
|
||||
logger.info('Update last name: %r -> %r', user.last_name, last_name)
|
||||
user.last_name = last_name
|
||||
update_fields.append('last_name')
|
||||
|
||||
if update_fields:
|
||||
try:
|
||||
user.full_clean()
|
||||
except ValidationError:
|
||||
logger.exception('Can not update user: %s', user)
|
||||
else:
|
||||
user.save(update_fields=update_fields)
|
||||
|
||||
return user
|
|
@ -1,8 +0,0 @@
|
|||
import base64
|
||||
|
||||
|
||||
def generate_basic_auth(username, password):
|
||||
basic_auth = f'{username}:{password}'
|
||||
basic_auth_creds = bytes(basic_auth, encoding='utf-8')
|
||||
creds = str(base64.b64encode(basic_auth_creds), encoding='utf-8')
|
||||
return f'basic {creds}'
|
|
@ -1,27 +0,0 @@
|
|||
import logging
|
||||
import pprint
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.http.response import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def request_media_debug_view(request):
|
||||
""" debug request.META """
|
||||
|
||||
assert settings.DEBUG is True, 'Only in DEBUG mode available!'
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
logger.info('Deny debug view: User not logged in!')
|
||||
UserModel = get_user_model()
|
||||
logger.info('Existing users are: %s', ', '.join(f'"{user}"' for user in UserModel.objects.all()))
|
||||
return redirect('admin:index')
|
||||
|
||||
meta = pprint.pformat(request.META)
|
||||
html = f'<html><body>request.META: <pre>{meta}</pre></body></html>'
|
||||
|
||||
return HttpResponse(html)
|
|
@ -1,16 +1,16 @@
|
|||
{
|
||||
"name": "django_ynh",
|
||||
"id": "django_ynh",
|
||||
"name": "Django Example",
|
||||
"id": "django_example_ynh",
|
||||
"packaging_format": 1,
|
||||
"description": {
|
||||
"en": "Glue code to package django projects as yunohost apps."
|
||||
"en": "Demo YunoHost Application to demonstrate the integration of a Django project under YunoHost."
|
||||
},
|
||||
"version": "0.1.5~ynh1",
|
||||
"url": "https://github.com/jedie/django_ynh",
|
||||
"version": "v0.2.0.alpha0~ynh1",
|
||||
"url": "https://github.com/YunoHost-Apps/django_example_ynh",
|
||||
"license": "GPL-3.0",
|
||||
"maintainer": {
|
||||
"name": "Jens Diemer",
|
||||
"email": "django_ynh@jensdiemer.de"
|
||||
"email": "django_example_ynh@jensdiemer.de"
|
||||
},
|
||||
"previous_maintainers": [],
|
||||
"requirements": {
|
||||
|
@ -26,8 +26,8 @@
|
|||
"name": "domain",
|
||||
"type": "domain",
|
||||
"ask": {
|
||||
"en": "Choose a domain for django_ynh",
|
||||
"fr": "Choisissez un domaine pour django_ynh"
|
||||
"en": "Choose a domain for PyInventory",
|
||||
"fr": "Choisissez un domaine pour PyInventory"
|
||||
},
|
||||
"example": "domain.org"
|
||||
},
|
||||
|
@ -35,18 +35,18 @@
|
|||
"name": "path",
|
||||
"type": "path",
|
||||
"ask": {
|
||||
"en": "Choose a path for django_ynh",
|
||||
"fr": "Choisissez un chemin pour django_ynh"
|
||||
"en": "Choose a path for PyInventory",
|
||||
"fr": "Choisissez un chemin pour PyInventory"
|
||||
},
|
||||
"example": "/django_ynh",
|
||||
"default": "/django_ynh"
|
||||
"example": "/django_example_ynh",
|
||||
"default": "/django_example_ynh"
|
||||
},
|
||||
{
|
||||
"name": "admin",
|
||||
"type": "user",
|
||||
"ask": {
|
||||
"en": "Choose an admin user for django_ynh",
|
||||
"fr": "Choisissez l'administrateur pour django_ynh"
|
||||
"en": "Choose an admin user for PyInventory",
|
||||
"fr": "Choisissez l'administrateur pour PyInventory"
|
||||
},
|
||||
"example": "johndoe"
|
||||
},
|
||||
|
@ -54,8 +54,8 @@
|
|||
"name": "is_public",
|
||||
"type": "boolean",
|
||||
"ask": {
|
||||
"en": "Should django_ynh be public accessible?",
|
||||
"fr": "django_ynh doit-il être accessible au public ?"
|
||||
"en": "Should PyInventory be public accessible?",
|
||||
"fr": "PyInventory doit-il être accessible au public ?"
|
||||
},
|
||||
"help": {
|
||||
"en": "Any YunoHost user and anonymous people from the web will be able to access the application",
|
||||
|
|
1021
poetry.lock
generated
Normal file
1021
poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
31
publish.py
31
publish.py
|
@ -1,31 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
from poetry_publish.publish import poetry_publish
|
||||
from poetry_publish.utils.subprocess_utils import verbose_check_call
|
||||
|
||||
import django_ynh
|
||||
from django_ynh.path_utils import assert_is_file
|
||||
|
||||
|
||||
PACKAGE_ROOT = Path(django_ynh.__file__).parent.parent.parent
|
||||
|
||||
|
||||
def publish():
|
||||
"""
|
||||
Publish to PyPi
|
||||
Call this via:
|
||||
$ make publish
|
||||
"""
|
||||
assert_is_file(PACKAGE_ROOT / 'README.md')
|
||||
|
||||
verbose_check_call('make', 'pytest') # don't publish if tests fail
|
||||
verbose_check_call('make', 'fix-code-style') # don't publish if code style wrong
|
||||
|
||||
poetry_publish(
|
||||
package_root=PACKAGE_ROOT,
|
||||
version=django_ynh.__version__,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
publish()
|
|
@ -1,28 +1,15 @@
|
|||
[tool.poetry]
|
||||
name = "django_ynh"
|
||||
version = "0.1.5"
|
||||
description = "Glue code to package django projects as yunohost apps."
|
||||
name = "django_example_ynh"
|
||||
version = "v0.2.0.alpha0"
|
||||
description = "Demo YunoHost Application to demonstrate the integration of a Django project under YunoHost."
|
||||
authors = ["JensDiemer <git@jensdiemer.de>"]
|
||||
license = "GPL"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/YunoHost-Apps/django_ynh"
|
||||
packages = [
|
||||
{ include = "django_ynh" },
|
||||
]
|
||||
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.7,<4.0.0"
|
||||
django = "*"
|
||||
# The follogin extra packages are used for install "django_ynh" as YunoHost app:
|
||||
gunicorn = { version = "*", optional = true }
|
||||
psycopg2-binary = { version = "*", optional = true }
|
||||
django-redis = { version = "*", optional = true }
|
||||
django-axes = { version = "*", optional = true } # https://github.com/jazzband/django-axes
|
||||
django_yunohost_integration = {version = "*", extras = ["ynh"]}
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
django-axes = "*" # https://github.com/jazzband/django-axes
|
||||
poetry-publish = "*" # https://github.com/jedie/poetry-publish
|
||||
bx_py_utils = "*"
|
||||
tox = "*"
|
||||
pytest = "*"
|
||||
|
@ -35,23 +22,20 @@ flynt = "*"
|
|||
black = "*"
|
||||
pyupgrade = "*"
|
||||
|
||||
[tool.poetry.extras]
|
||||
ynh = ["gunicorn", "psycopg2-binary", "django-redis", "django-axes"] # install as YunoHost app
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
|
||||
[tool.isort]
|
||||
# https://pycqa.github.io/isort/docs/configuration/config_files/#pyprojecttoml-preferred-format
|
||||
atomic=true
|
||||
line_length=120
|
||||
case_sensitive=false
|
||||
skip_glob=["*/htmlcov/*","*/migrations/*","*/volumes/*"]
|
||||
skip_glob=["*/htmlcov/*","*/migrations/*"]
|
||||
multi_line_output=3
|
||||
include_trailing_comma=true
|
||||
known_first_party=["django_ynh","django_ynh_project","django_ynh_tests"]
|
||||
known_first_party=[]
|
||||
no_lines_before="LOCALFOLDER"
|
||||
default_section="THIRDPARTY"
|
||||
sections=["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
|
||||
|
@ -61,7 +45,7 @@ lines_after_imports=2
|
|||
[tool.pytest.ini_options]
|
||||
# https://docs.pytest.org/en/latest/customize.html#pyproject-toml
|
||||
minversion = "6.0"
|
||||
norecursedirs = ".* .git __pycache__ conf coverage* dist htmlcov volumes"
|
||||
norecursedirs = ".* .git __pycache__ conf coverage* dist htmlcov"
|
||||
# sometimes helpfull "addopts" arguments:
|
||||
# -vv
|
||||
# --verbose
|
||||
|
@ -96,7 +80,7 @@ skip_missing_interpreters = True
|
|||
|
||||
[testenv]
|
||||
passenv = *
|
||||
whitelist_externals = pytest
|
||||
whitelist_externals = make
|
||||
commands =
|
||||
pytest --workers auto --tests-per-worker 1 --pyargs django_ynh django_ynh_project
|
||||
make pytest
|
||||
"""
|
||||
|
|
|
@ -18,7 +18,7 @@ app=$YNH_APP_INSTANCE_NAME
|
|||
public_path=/var/www/$app
|
||||
final_path=/opt/yunohost/$app
|
||||
log_path=/var/log/$app
|
||||
log_file="${log_path}/django_ynh.log"
|
||||
log_file="${log_path}/django_example_ynh.log"
|
||||
|
||||
#=================================================
|
||||
# COMMON VARIABLES
|
||||
|
@ -27,9 +27,6 @@ log_file="${log_path}/django_ynh.log"
|
|||
# dependencies used by the app
|
||||
pkg_dependencies="build-essential python3-dev python3-pip python3-venv git postgresql postgresql-contrib"
|
||||
|
||||
# To install/upgrade this project via pip:
|
||||
pip_install_string="django_ynh[ynh]==0.1.5"
|
||||
|
||||
#=================================================
|
||||
# Redis HELPERS
|
||||
#=================================================
|
||||
|
|
|
@ -113,7 +113,7 @@ fi
|
|||
#=================================================
|
||||
# MODIFY SETTINGS
|
||||
#=================================================
|
||||
ynh_script_progression --message="Modify django_ynh's config file..."
|
||||
ynh_script_progression --message="Modify PyInventory's config file..."
|
||||
|
||||
# save old settings file
|
||||
settings="$final_path/settings.py"
|
||||
|
|
|
@ -114,13 +114,12 @@ chown -R "$app" "$final_path"
|
|||
set -o nounset
|
||||
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip
|
||||
ynh_exec_as $app $final_path/venv/bin/pip install -r "$final_path/requirements.txt"
|
||||
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade "$pip_install_string"
|
||||
)
|
||||
|
||||
#=================================================
|
||||
# copy config files
|
||||
# ================================================
|
||||
ynh_script_progression --message="Create django_ynh configuration file..."
|
||||
ynh_script_progression --message="Create project configuration files..."
|
||||
|
||||
gunicorn_conf="$final_path/gunicorn.conf.py"
|
||||
cp "../conf/gunicorn.conf.py" "$gunicorn_conf"
|
||||
|
@ -158,11 +157,10 @@ cp ../conf/wsgi.py "$final_path/wsgi.py"
|
|||
|
||||
touch "$final_path/local_settings.py"
|
||||
|
||||
|
||||
#=================================================
|
||||
# MIGRATE / COLLECTSTATIC / CREATE SUPERUSER
|
||||
# MIGRATE / COLLECTSTATIC / CREATEADMIN
|
||||
#=================================================
|
||||
ynh_script_progression --message="migrate/collectstatic/create superuser..." --weight=10
|
||||
ynh_script_progression --message="migrate/collectstatic/createadmin..." --weight=10
|
||||
|
||||
(
|
||||
set +o nounset
|
||||
|
@ -176,8 +174,8 @@ ynh_script_progression --message="migrate/collectstatic/create superuser..." --w
|
|||
./manage.py migrate --no-input
|
||||
./manage.py collectstatic --no-input
|
||||
|
||||
# Create/update Django superuser (set unusable password, because auth done via SSOwat):
|
||||
./manage.py create_superuser --username="$admin" --email="$admin_mail"
|
||||
# Create/update Django superuser (set unusable password, because auth done via SSOwat):
|
||||
./manage.py create_superuser --username="$admin" --email="$admin_mail"
|
||||
|
||||
# Check the configuration
|
||||
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later.
|
||||
|
@ -216,7 +214,7 @@ chown -R "$app" "$final_path"
|
|||
ynh_script_progression --message="Configuring a systemd service..."
|
||||
|
||||
# https://github.com/YunoHost/yunohost/blob/dev/data/helpers.d/systemd
|
||||
ynh_add_systemd_config --service="$app" --template="django_ynh.service"
|
||||
ynh_add_systemd_config --service="$app" --template="django_example_ynh.service"
|
||||
|
||||
#=================================================
|
||||
# SETUP SSOWAT
|
||||
|
@ -232,9 +230,9 @@ then
|
|||
fi
|
||||
|
||||
#=================================================
|
||||
# Start django_ynh via systemd
|
||||
# Start django_example_ynh via systemd
|
||||
#=================================================
|
||||
ynh_script_progression --message="Starting django_ynh's services..." --weight=5
|
||||
ynh_script_progression --message="Starting PyInventory's services..." --weight=5
|
||||
|
||||
ynh_systemd_action --service_name="$app" --action="start"
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ then
|
|||
fi
|
||||
|
||||
#=================================================
|
||||
# STOP django_ynh'S SERVICES
|
||||
# STOP PYINVENTORY'S SERVICES
|
||||
#=================================================
|
||||
ynh_script_progression --message="Stopping and removing systemd services..." --weight=5
|
||||
|
||||
|
|
|
@ -118,7 +118,7 @@ ynh_restore_file --origin_path="/etc/logrotate.d/$app"
|
|||
#=================================================
|
||||
# GENERIC FINALIZATION
|
||||
#=================================================
|
||||
# START django_ynh
|
||||
# START PYINVENTORY
|
||||
#=================================================
|
||||
ynh_script_progression --message="Starting a systemd service..." --weight=5
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ ynh_system_user_create --username="$app" --home_dir="$final_path" --use_shell
|
|||
#=================================================
|
||||
ynh_script_progression --message="Configuring a systemd service..."
|
||||
|
||||
ynh_add_systemd_config --service="$app" --template="django_ynh.service"
|
||||
ynh_add_systemd_config --service="$app" --template="django_example_ynh.service"
|
||||
|
||||
#=================================================
|
||||
# UPGRADE VENV
|
||||
|
@ -98,7 +98,6 @@ chown -R "$app" "$final_path"
|
|||
set -o nounset
|
||||
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade pip
|
||||
ynh_exec_as $app $final_path/venv/bin/pip install -r "$final_path/requirements.txt"
|
||||
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade "$pip_install_string"
|
||||
)
|
||||
|
||||
#=================================================
|
||||
|
@ -150,30 +149,29 @@ cp ../conf/wsgi.py "$final_path/wsgi.py"
|
|||
|
||||
touch "$final_path/local_settings.py"
|
||||
|
||||
|
||||
#=================================================
|
||||
# MIGRATE / COLLECTSTATIC / CREATE SUPERUSER
|
||||
# MIGRATE PYINVENTORY
|
||||
#=================================================
|
||||
ynh_script_progression --message="migrate/collectstatic/create superuser..." --weight=10
|
||||
ynh_script_progression --message="migrate/collectstatic/createadmin..." --weight=10
|
||||
|
||||
(
|
||||
set +o nounset
|
||||
source "${final_path}/venv/bin/activate"
|
||||
set -o nounset
|
||||
cd "${final_path}"
|
||||
set +o nounset
|
||||
source "${final_path}/venv/bin/activate"
|
||||
set -o nounset
|
||||
cd "${final_path}"
|
||||
|
||||
# Just for debugging:
|
||||
./manage.py diffsettings
|
||||
# Just for debugging:
|
||||
./manage.py diffsettings
|
||||
|
||||
./manage.py migrate --no-input
|
||||
./manage.py collectstatic --no-input
|
||||
./manage.py migrate --no-input
|
||||
./manage.py collectstatic --no-input
|
||||
|
||||
# Create/update Django superuser (set unusable password, because auth done via SSOwat):
|
||||
./manage.py create_superuser --username="$admin" --email="$admin_mail"
|
||||
# Create/update Django superuser (set unusable password, because auth done via SSOwat):
|
||||
./manage.py create_superuser --username="$admin" --email="$admin_mail"
|
||||
|
||||
# Check the configuration
|
||||
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later.
|
||||
./manage.py check --deploy || true
|
||||
# Check the configuration
|
||||
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later.
|
||||
./manage.py check --deploy || true
|
||||
)
|
||||
|
||||
#=================================================
|
||||
|
@ -203,9 +201,9 @@ chown -R "$app" "$public_path"
|
|||
chown -R "$app" "$final_path"
|
||||
|
||||
#=================================================
|
||||
# Start django_ynh via systemd
|
||||
# Start django_example_ynh via systemd
|
||||
#=================================================
|
||||
ynh_script_progression --message="Starting django_ynh's services..." --weight=5
|
||||
ynh_script_progression --message="Starting PyInventory's services..." --weight=5
|
||||
|
||||
ynh_systemd_action --service_name="$app" --action="start"
|
||||
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
from axes.models import AccessAttempt, AccessLog
|
||||
from axes.models import AccessLog
|
||||
from bx_py_utils.test_utils.html_assertion import HtmlAssertionMixin
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import override_settings
|
||||
from django.test.testcases import TestCase
|
||||
from django.urls import NoReverseMatch
|
||||
from django.urls.base import reverse
|
||||
|
||||
from django_ynh.test_utils import generate_basic_auth
|
||||
from django_ynh.views import request_media_debug_view
|
||||
|
||||
import inventory
|
||||
|
||||
|
||||
@override_settings(DEBUG=False)
|
||||
class DjangoYnhTestCase(HtmlAssertionMixin, TestCase):
|
||||
|
@ -23,13 +25,22 @@ class DjangoYnhTestCase(HtmlAssertionMixin, TestCase):
|
|||
|
||||
assert str(settings.FINAL_HOME_PATH).endswith('/local_test/opt_yunohost')
|
||||
assert str(settings.FINAL_WWW_PATH).endswith('/local_test/var_www')
|
||||
assert str(settings.LOG_FILE).endswith('/local_test/var_log_django_ynh.log')
|
||||
assert str(settings.LOG_FILE).endswith('/local_test/var_log_django_example_ynh.log')
|
||||
|
||||
assert settings.ROOT_URLCONF == 'urls'
|
||||
|
||||
def test_urls(self):
|
||||
assert reverse('admin:index') == '/app_path/'
|
||||
assert reverse(request_media_debug_view) == '/app_path/debug/'
|
||||
|
||||
# The django_ynh debug view should not be avaiable:
|
||||
with self.assertRaises(NoReverseMatch):
|
||||
reverse(request_media_debug_view)
|
||||
|
||||
# Serve user uploads via django_tools.serve_media_app:
|
||||
assert settings.MEDIA_URL == '/app_path/media/'
|
||||
assert reverse('serve_media_app:serve-media', kwargs={'user_token': 'token', 'path': 'foo/bar/'}) == (
|
||||
'/app_path/media/token/foo/bar/'
|
||||
)
|
||||
|
||||
def test_auth(self):
|
||||
response = self.client.get('/app_path/')
|
||||
|
@ -55,7 +66,11 @@ class DjangoYnhTestCase(HtmlAssertionMixin, TestCase):
|
|||
assert user.is_superuser is False
|
||||
|
||||
self.assert_html_parts(
|
||||
response, parts=('<title>Site administration | Django site admin</title>', '<strong>test</strong>')
|
||||
response,
|
||||
parts=(
|
||||
f'<title>Site administration | PyInventory v{inventory.__version__}</title>',
|
||||
'<strong>test</strong>',
|
||||
),
|
||||
)
|
||||
|
||||
def test_wrong_auth_user(self):
|
|
@ -2,10 +2,8 @@ import shutil
|
|||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import django_ynh
|
||||
|
||||
|
||||
BASE_PATH = Path(django_ynh.__file__).parent.parent
|
||||
BASE_PATH = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def test_lint():
|
||||
|
|
|
@ -3,10 +3,10 @@ import shutil
|
|||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import django_ynh
|
||||
import django_exyunohost_integration
|
||||
|
||||
|
||||
PACKAGE_ROOT = Path(django_ynh.__file__).parent.parent
|
||||
PACKAGE_ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def assert_file_contains_string(file_path, string):
|
||||
|
@ -17,29 +17,15 @@ def assert_file_contains_string(file_path, string):
|
|||
raise AssertionError(f'File {file_path} does not contain {string!r} !')
|
||||
|
||||
|
||||
def test_version(package_root=None, version=None):
|
||||
if package_root is None:
|
||||
package_root = PACKAGE_ROOT
|
||||
def test_version():
|
||||
version = inventory.__version__
|
||||
|
||||
if version is None:
|
||||
version = django_ynh.__version__
|
||||
|
||||
if 'dev' not in version and 'rc' not in version:
|
||||
version_string = f'v{version}'
|
||||
|
||||
assert_file_contains_string(file_path=Path(package_root, 'README.md'), string=version_string)
|
||||
|
||||
assert_file_contains_string(file_path=Path(package_root, 'pyproject.toml'), string=f'version = "{version}"')
|
||||
assert_file_contains_string(file_path=Path(package_root, 'manifest.json'), string=f'"version": "{version}~ynh')
|
||||
assert_file_contains_string(
|
||||
file_path=Path(package_root, 'scripts', '_common.sh'), string=f'"django_ynh[ynh]=={version}"'
|
||||
)
|
||||
assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'pyproject.toml'), string=f'version = "{version}~ynh')
|
||||
assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'pyproject.toml'), string=f'django_example_ynh = "=={version}"')
|
||||
assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'manifest.json'), string=f'"version": "{version}~ynh')
|
||||
|
||||
|
||||
def test_poetry_check(package_root=None):
|
||||
if package_root is None:
|
||||
package_root = PACKAGE_ROOT
|
||||
|
||||
def test_poetry_check():
|
||||
poerty_bin = shutil.which('poetry')
|
||||
|
||||
output = subprocess.check_output(
|
||||
|
@ -47,7 +33,7 @@ def test_poetry_check(package_root=None):
|
|||
universal_newlines=True,
|
||||
env=os.environ,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=str(package_root),
|
||||
cwd=str(PACKAGE_ROOT),
|
||||
)
|
||||
print(output)
|
||||
assert output == 'All set!\n'
|
||||
|
|
Loading…
Reference in a new issue