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:
JensDiemer 2021-02-28 10:56:42 +01:00
parent eb3fe62cac
commit a8164a2905
39 changed files with 1214 additions and 906 deletions

View file

@ -2,6 +2,6 @@
# Move to pyproject.toml after: https://gitlab.com/pycqa/flake8/-/issues/428 # Move to pyproject.toml after: https://gitlab.com/pycqa/flake8/-/issues/428
# #
[flake8] [flake8]
exclude = .pytest_cache, .tox, dist, htmlcov, */migrations/* exclude = .pytest_cache, .tox, dist, htmlcov, local_test
#ignore = E402 ignore = F405
max-line-length = 119 max-line-length = 119

7
.gitignore vendored
View file

@ -3,11 +3,8 @@
!.editorconfig !.editorconfig
!.flake8 !.flake8
!.gitignore !.gitignore
coverage.xml
__pycache__ __pycache__
secret.txt secret.txt
/htmlcov/
/local_test/ /local_test/
/dist/ /coverage.xml
/poetry.lock /htmlcov/
*.log

View file

@ -31,7 +31,7 @@ update: install-poetry ## update the sources and installation and generate "con
lint: ## Run code formatters and linter lint: ## Run code formatters and linter
poetry run flynt --fail-on-change --line_length=${MAX_LINE_LENGTH} . poetry run flynt --fail-on-change --line_length=${MAX_LINE_LENGTH} .
poetry run isort --check-only . poetry run isort --check-only .
poetry run flake8 django_ynh poetry run flake8 .
fix-code-style: ## Fix code formatting fix-code-style: ## Fix code formatting
poetry run flynt --line_length=${MAX_LINE_LENGTH} . 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 pytest: install ## Run pytest
poetry run python3 ./run_pytest.py 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 local-test: install ## Run local_test.py to run the project locally
poetry run python3 ./local_test.py poetry run python3 ./local_test.py

View file

@ -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. [![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)
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)
Pull requests welcome ;) Pull requests welcome ;)
@ -92,9 +85,9 @@ and urls made for YunoHost installation.
e.g.: e.g.:
```bash ```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/ ~$ cd django_ynh/
~/django_ynh$ make ~/django_example_ynh$ make
install-poetry install or update poetry install-poetry install or update poetry
install install project via poetry install install project via poetry
update update the sources and installation and generate "conf/requirements.txt" 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-test Run local_test.py to run the project locally
local-diff-settings Run "manage.py diffsettings" with local test local-diff-settings Run "manage.py diffsettings" with local test
~/django_ynh$ make install-poetry ~/django_example_ynh$ make install-poetry
~/django_ynh$ make install ~/django_example_ynh$ make install
~/django_ynh$ make local-test ~/django_example_ynh$ make local-test
``` ```
Notes: Notes:
@ -123,6 +116,10 @@ Notes:
* [compare v0.1.5...master](https://github.com/YunoHost-Apps/django_ynh/compare/v0.1.5...master) **dev** * [compare v0.1.5...master](https://github.com/YunoHost-Apps/django_ynh/compare/v0.1.5...master) **dev**
* tbc * 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) * [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 * 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) * [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`: 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 * https://github.com/YunoHost-Apps/django-for-runners_ynh
--- ---

View file

@ -1,5 +1,5 @@
[Unit] [Unit]
Description=django_ynh DEMO application server Description=PyInventory application server
After=redis.service postgresql.service After=redis.service postgresql.service
[Service] [Service]

View file

@ -5,9 +5,19 @@ location __PATH__/static/ {
expires 30d; expires 30d;
} }
# TODO: django-sendfile2:
#location __PATH__/media/ {
# # DATA_DIR/media/
# alias __PUBLIC_PATH__/media/;
# expires 30d;
#}
location __PATH__/ { location __PATH__/ {
# https://github.com/benoitc/gunicorn/blob/master/examples/nginx.conf # 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_http_version 1.1;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

View file

@ -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:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17 \
--hash=sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0 --hash=sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0
django-axes==5.10.0; 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:8a62cd4cc78ef08007e8102def34be83832995eb6e3e0c814d605741b82a2796 \ --hash=sha256:8f7870dc18ace6155127073bffe7276719c71c5ad4e67b45abedc0207f64a2f6 \
--hash=sha256:3c81ddca8a9d7fd0019cb440f711cc873c3039546f7eacb3f2ec616bf0ec1b32 --hash=sha256:aef814f738742b01cc7730cd2bebe3e5b462b807fd6c2ed609b62437ccc71f80
django-ipware==3.0.2; python_version >= "3.6" and python_version < "4.0" \ django-ipware==3.0.2; python_version >= "3.7" and python_version < "4.0" and python_full_version < "4.0.0" \
--hash=sha256:c7df8e1410a8e5d6b1fbae58728402ea59950f043c3582e033e866f0f0cf5e94 --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:306589c7021e6468b2656edc89f62b8ba67e8d5a1c8877e2688042263daa7a63 \
--hash=sha256:1133b26b75baa3664164c3f44b9d5d133d1b8de45d94d79f38d1adc5b1d502e5 --hash=sha256:1133b26b75baa3664164c3f44b9d5d133d1b8de45d94d79f38d1adc5b1d502e5
django==3.1.4; python_version >= "3.6" \ django-yunohost-integration==0.2.0a0; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2 \ --hash=sha256:a45197a3c4595a496674e7e48a58f58351b6022526893264831e0d6ba463a44f \
--hash=sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03 --hash=sha256:9acdf320537ccce47ceb1d51d2a00fafbf30938152d3b22c59a3a2ead7952227
gunicorn==20.0.4; python_version >= "3.4" \ 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:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c \
--hash=sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626 --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:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0 \
--hash=sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4 \ --hash=sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4 \
--hash=sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db \ --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:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd \
--hash=sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056 \ --hash=sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056 \
--hash=sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6 --hash=sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6
pytz==2020.5; python_version >= "3.6" and python_version < "4.0" \ pytz==2021.1; python_version >= "3.7" and python_full_version < "4.0.0" \
--hash=sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4 \ --hash=sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798 \
--hash=sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5 --hash=sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da
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" \ 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:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24 \
--hash=sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2 --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:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0 \
--hash=sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8 --hash=sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8

View file

@ -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, # Please do not modify this file, it will be reset at the next update.
but you can use the options and documentation in this file to find out # You can edit the file __FINAL_HOME_PATH__/local_settings.py and add/modify the settings you need.
what can be done. # 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 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 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 FINAL_WWW_PATH = __Path('__FINAL_WWW_PATH__') # /var/www/$app
assert FINAL_WWW_PATH.is_dir(), f'Directory not exists: {FINAL_WWW_PATH}' 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}' assert LOG_FILE.is_file(), f'File not exists: {LOG_FILE}'
PATH_URL = '__PATH_URL__' # $YNH_APP_ARG_PATH 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: # 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 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__'),) ADMINS = (('__ADMIN__', '__ADMINMAIL__'),)
MANAGERS = ADMINS MANAGERS = ADMINS
@ -77,14 +103,13 @@ DEFAULT_FROM_EMAIL = '__ADMINMAIL__'
# List of URLs your site is supposed to serve # List of URLs your site is supposed to serve
ALLOWED_HOSTS = ['__DOMAIN__'] ALLOWED_HOSTS = ['__DOMAIN__']
# _____________________________________________________________________________ # _____________________________________________________________________________
# Configuration for caching # Configuration for caching
CACHES = { CACHES = {
'default': { 'default': {
'BACKEND': 'django_redis.cache.RedisCache', 'BACKEND': 'django_redis.cache.RedisCache',
'LOCATION': 'redis://127.0.0.1:6379/__REDIS_DB__', '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: # want to use unix sockets instead:
# 'LOCATION': 'unix:///var/run/redis/redis.sock?db=1', # 'LOCATION': 'unix:///var/run/redis/redis.sock?db=1',
'OPTIONS': { 'OPTIONS': {
@ -94,7 +119,6 @@ CACHES = {
}, },
} }
# _____________________________________________________________________________ # _____________________________________________________________________________
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
@ -109,10 +133,18 @@ else:
STATIC_ROOT = str(FINAL_WWW_PATH / 'static') STATIC_ROOT = str(FINAL_WWW_PATH / 'static')
MEDIA_ROOT = str(FINAL_WWW_PATH / 'media') 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 = { LOGGING = {
'version': 1, 'version': 1,
'disable_existing_loggers': True, 'disable_existing_loggers': True,
@ -142,6 +174,7 @@ LOGGING = {
'axes': {'handlers': ['log_file', 'mail_admins'], 'level': 'WARNING', 'propagate': False}, 'axes': {'handlers': ['log_file', 'mail_admins'], 'level': 'WARNING', 'propagate': False},
'django_tools': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False}, 'django_tools': {'handlers': ['log_file', 'mail_admins'], 'level': 'INFO', 'propagate': False},
'django_ynh': {'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},
}, },
} }

View file

@ -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.is_staff = True
user.save() user.save()

View file

@ -2,7 +2,7 @@ from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.urls import path 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 # settings.PATH_URL is the $YNH_APP_ARG_PATH

View file

@ -6,7 +6,7 @@ import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' 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() application = get_wsgi_application()

View file

@ -1 +0,0 @@
__version__ = '0.1.5'

View file

@ -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},
},
}

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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())

View file

@ -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()

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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}'

View file

@ -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)

View file

@ -1,16 +1,16 @@
{ {
"name": "django_ynh", "name": "Django Example",
"id": "django_ynh", "id": "django_example_ynh",
"packaging_format": 1, "packaging_format": 1,
"description": { "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", "version": "v0.2.0.alpha0~ynh1",
"url": "https://github.com/jedie/django_ynh", "url": "https://github.com/YunoHost-Apps/django_example_ynh",
"license": "GPL-3.0", "license": "GPL-3.0",
"maintainer": { "maintainer": {
"name": "Jens Diemer", "name": "Jens Diemer",
"email": "django_ynh@jensdiemer.de" "email": "django_example_ynh@jensdiemer.de"
}, },
"previous_maintainers": [], "previous_maintainers": [],
"requirements": { "requirements": {
@ -26,8 +26,8 @@
"name": "domain", "name": "domain",
"type": "domain", "type": "domain",
"ask": { "ask": {
"en": "Choose a domain for django_ynh", "en": "Choose a domain for PyInventory",
"fr": "Choisissez un domaine pour django_ynh" "fr": "Choisissez un domaine pour PyInventory"
}, },
"example": "domain.org" "example": "domain.org"
}, },
@ -35,18 +35,18 @@
"name": "path", "name": "path",
"type": "path", "type": "path",
"ask": { "ask": {
"en": "Choose a path for django_ynh", "en": "Choose a path for PyInventory",
"fr": "Choisissez un chemin pour django_ynh" "fr": "Choisissez un chemin pour PyInventory"
}, },
"example": "/django_ynh", "example": "/django_example_ynh",
"default": "/django_ynh" "default": "/django_example_ynh"
}, },
{ {
"name": "admin", "name": "admin",
"type": "user", "type": "user",
"ask": { "ask": {
"en": "Choose an admin user for django_ynh", "en": "Choose an admin user for PyInventory",
"fr": "Choisissez l'administrateur pour django_ynh" "fr": "Choisissez l'administrateur pour PyInventory"
}, },
"example": "johndoe" "example": "johndoe"
}, },
@ -54,8 +54,8 @@
"name": "is_public", "name": "is_public",
"type": "boolean", "type": "boolean",
"ask": { "ask": {
"en": "Should django_ynh be public accessible?", "en": "Should PyInventory be public accessible?",
"fr": "django_ynh doit-il être accessible au public ?" "fr": "PyInventory doit-il être accessible au public ?"
}, },
"help": { "help": {
"en": "Any YunoHost user and anonymous people from the web will be able to access the application", "en": "Any YunoHost user and anonymous people from the web will be able to access the application",

1021
poetry.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -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()

View file

@ -1,28 +1,15 @@
[tool.poetry] [tool.poetry]
name = "django_ynh" name = "django_example_ynh"
version = "0.1.5" version = "v0.2.0.alpha0"
description = "Glue code to package django projects as yunohost apps." description = "Demo YunoHost Application to demonstrate the integration of a Django project under YunoHost."
authors = ["JensDiemer <git@jensdiemer.de>"] authors = ["JensDiemer <git@jensdiemer.de>"]
license = "GPL" license = "GPL"
readme = "README.md"
homepage = "https://github.com/YunoHost-Apps/django_ynh"
packages = [
{ include = "django_ynh" },
]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.7,<4.0.0" python = ">=3.7,<4.0.0"
django = "*" django_yunohost_integration = {version = "*", extras = ["ynh"]}
# 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
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
django-axes = "*" # https://github.com/jazzband/django-axes
poetry-publish = "*" # https://github.com/jedie/poetry-publish
bx_py_utils = "*" bx_py_utils = "*"
tox = "*" tox = "*"
pytest = "*" pytest = "*"
@ -35,23 +22,20 @@ flynt = "*"
black = "*" black = "*"
pyupgrade = "*" pyupgrade = "*"
[tool.poetry.extras]
ynh = ["gunicorn", "psycopg2-binary", "django-redis", "django-axes"] # install as YunoHost app
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.isort] [tool.isort]
# https://pycqa.github.io/isort/docs/configuration/config_files/#pyprojecttoml-preferred-format # https://pycqa.github.io/isort/docs/configuration/config_files/#pyprojecttoml-preferred-format
atomic=true atomic=true
line_length=120 line_length=120
case_sensitive=false case_sensitive=false
skip_glob=["*/htmlcov/*","*/migrations/*","*/volumes/*"] skip_glob=["*/htmlcov/*","*/migrations/*"]
multi_line_output=3 multi_line_output=3
include_trailing_comma=true include_trailing_comma=true
known_first_party=["django_ynh","django_ynh_project","django_ynh_tests"] known_first_party=[]
no_lines_before="LOCALFOLDER" no_lines_before="LOCALFOLDER"
default_section="THIRDPARTY" default_section="THIRDPARTY"
sections=["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"] sections=["FUTURE","STDLIB","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"]
@ -61,7 +45,7 @@ lines_after_imports=2
[tool.pytest.ini_options] [tool.pytest.ini_options]
# https://docs.pytest.org/en/latest/customize.html#pyproject-toml # https://docs.pytest.org/en/latest/customize.html#pyproject-toml
minversion = "6.0" minversion = "6.0"
norecursedirs = ".* .git __pycache__ conf coverage* dist htmlcov volumes" norecursedirs = ".* .git __pycache__ conf coverage* dist htmlcov"
# sometimes helpfull "addopts" arguments: # sometimes helpfull "addopts" arguments:
# -vv # -vv
# --verbose # --verbose
@ -96,7 +80,7 @@ skip_missing_interpreters = True
[testenv] [testenv]
passenv = * passenv = *
whitelist_externals = pytest whitelist_externals = make
commands = commands =
pytest --workers auto --tests-per-worker 1 --pyargs django_ynh django_ynh_project make pytest
""" """

View file

@ -18,7 +18,7 @@ app=$YNH_APP_INSTANCE_NAME
public_path=/var/www/$app public_path=/var/www/$app
final_path=/opt/yunohost/$app final_path=/opt/yunohost/$app
log_path=/var/log/$app log_path=/var/log/$app
log_file="${log_path}/django_ynh.log" log_file="${log_path}/django_example_ynh.log"
#================================================= #=================================================
# COMMON VARIABLES # COMMON VARIABLES
@ -27,9 +27,6 @@ log_file="${log_path}/django_ynh.log"
# dependencies used by the app # dependencies used by the app
pkg_dependencies="build-essential python3-dev python3-pip python3-venv git postgresql postgresql-contrib" 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 # Redis HELPERS
#================================================= #=================================================

View file

@ -113,7 +113,7 @@ fi
#================================================= #=================================================
# MODIFY SETTINGS # 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 # save old settings file
settings="$final_path/settings.py" settings="$final_path/settings.py"

View file

@ -114,13 +114,12 @@ chown -R "$app" "$final_path"
set -o nounset 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 --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 -r "$final_path/requirements.txt"
ynh_exec_as $app $final_path/venv/bin/pip install --upgrade "$pip_install_string"
) )
#================================================= #=================================================
# copy config files # 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" gunicorn_conf="$final_path/gunicorn.conf.py"
cp "../conf/gunicorn.conf.py" "$gunicorn_conf" 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" 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 set +o nounset
@ -176,8 +174,8 @@ ynh_script_progression --message="migrate/collectstatic/create superuser..." --w
./manage.py migrate --no-input ./manage.py migrate --no-input
./manage.py collectstatic --no-input ./manage.py collectstatic --no-input
# Create/update Django superuser (set unusable password, because auth done via SSOwat): # Create/update Django superuser (set unusable password, because auth done via SSOwat):
./manage.py create_superuser --username="$admin" --email="$admin_mail" ./manage.py create_superuser --username="$admin" --email="$admin_mail"
# Check the configuration # Check the configuration
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later. # 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..." ynh_script_progression --message="Configuring a systemd service..."
# https://github.com/YunoHost/yunohost/blob/dev/data/helpers.d/systemd # 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 # SETUP SSOWAT
@ -232,9 +230,9 @@ then
fi 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" ynh_systemd_action --service_name="$app" --action="start"

View file

@ -34,7 +34,7 @@ then
fi fi
#================================================= #=================================================
# STOP django_ynh'S SERVICES # STOP PYINVENTORY'S SERVICES
#================================================= #=================================================
ynh_script_progression --message="Stopping and removing systemd services..." --weight=5 ynh_script_progression --message="Stopping and removing systemd services..." --weight=5

View file

@ -118,7 +118,7 @@ ynh_restore_file --origin_path="/etc/logrotate.d/$app"
#================================================= #=================================================
# GENERIC FINALIZATION # GENERIC FINALIZATION
#================================================= #=================================================
# START django_ynh # START PYINVENTORY
#================================================= #=================================================
ynh_script_progression --message="Starting a systemd service..." --weight=5 ynh_script_progression --message="Starting a systemd service..." --weight=5

View file

@ -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_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 # UPGRADE VENV
@ -98,7 +98,6 @@ chown -R "$app" "$final_path"
set -o nounset 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 --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 -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" 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 set +o nounset
source "${final_path}/venv/bin/activate" source "${final_path}/venv/bin/activate"
set -o nounset set -o nounset
cd "${final_path}" cd "${final_path}"
# Just for debugging: # Just for debugging:
./manage.py diffsettings ./manage.py diffsettings
./manage.py migrate --no-input ./manage.py migrate --no-input
./manage.py collectstatic --no-input ./manage.py collectstatic --no-input
# Create/update Django superuser (set unusable password, because auth done via SSOwat): # Create/update Django superuser (set unusable password, because auth done via SSOwat):
./manage.py create_superuser --username="$admin" --email="$admin_mail" ./manage.py create_superuser --username="$admin" --email="$admin_mail"
# Check the configuration # Check the configuration
# This may fail in some cases with errors, etc., but the app works and the user can fix issues later. # 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 ./manage.py check --deploy || true
) )
#================================================= #=================================================
@ -203,9 +201,9 @@ chown -R "$app" "$public_path"
chown -R "$app" "$final_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" ynh_systemd_action --service_name="$app" --action="start"

View file

@ -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 bx_py_utils.test_utils.html_assertion import HtmlAssertionMixin
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.test import override_settings from django.test import override_settings
from django.test.testcases import TestCase from django.test.testcases import TestCase
from django.urls import NoReverseMatch
from django.urls.base import reverse from django.urls.base import reverse
from django_ynh.test_utils import generate_basic_auth from django_ynh.test_utils import generate_basic_auth
from django_ynh.views import request_media_debug_view from django_ynh.views import request_media_debug_view
import inventory
@override_settings(DEBUG=False) @override_settings(DEBUG=False)
class DjangoYnhTestCase(HtmlAssertionMixin, TestCase): 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_HOME_PATH).endswith('/local_test/opt_yunohost')
assert str(settings.FINAL_WWW_PATH).endswith('/local_test/var_www') 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' assert settings.ROOT_URLCONF == 'urls'
def test_urls(self): def test_urls(self):
assert reverse('admin:index') == '/app_path/' 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): def test_auth(self):
response = self.client.get('/app_path/') response = self.client.get('/app_path/')
@ -55,7 +66,11 @@ class DjangoYnhTestCase(HtmlAssertionMixin, TestCase):
assert user.is_superuser is False assert user.is_superuser is False
self.assert_html_parts( 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): def test_wrong_auth_user(self):

View file

@ -2,10 +2,8 @@ import shutil
import subprocess import subprocess
from pathlib import Path from pathlib import Path
import django_ynh
BASE_PATH = Path(__file__).parent.parent
BASE_PATH = Path(django_ynh.__file__).parent.parent
def test_lint(): def test_lint():

View file

@ -3,10 +3,10 @@ import shutil
import subprocess import subprocess
from pathlib import Path 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): 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} !') raise AssertionError(f'File {file_path} does not contain {string!r} !')
def test_version(package_root=None, version=None): def test_version():
if package_root is None: version = inventory.__version__
package_root = PACKAGE_ROOT
if version is None: assert_file_contains_string(file_path=Path(PACKAGE_ROOT, 'pyproject.toml'), string=f'version = "{version}~ynh')
version = django_ynh.__version__ 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')
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}"'
)
def test_poetry_check(package_root=None): def test_poetry_check():
if package_root is None:
package_root = PACKAGE_ROOT
poerty_bin = shutil.which('poetry') poerty_bin = shutil.which('poetry')
output = subprocess.check_output( output = subprocess.check_output(
@ -47,7 +33,7 @@ def test_poetry_check(package_root=None):
universal_newlines=True, universal_newlines=True,
env=os.environ, env=os.environ,
stderr=subprocess.STDOUT, stderr=subprocess.STDOUT,
cwd=str(package_root), cwd=str(PACKAGE_ROOT),
) )
print(output) print(output)
assert output == 'All set!\n' assert output == 'All set!\n'