start a Publik Django application (#64765)

This commit is contained in:
Thomas NOËL 2022-05-02 23:08:39 +02:00 committed by Thomas NOEL
parent 05fb86a97f
commit dbdf44777f
47 changed files with 1545 additions and 0 deletions

3
.gitignore vendored
View File

@ -11,3 +11,6 @@ lingo.egg-info/
.cache
.coverage
.pytest_cache/
junit*xml
pylint.out
*.swp

49
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,49 @@
@Library('eo-jenkins-lib@main') import eo.Utils
pipeline {
agent any
options {
disableConcurrentBuilds()
timeout(time: 20, unit: 'MINUTES')
}
stages {
stage('Unit Tests') {
steps {
sh 'tox -rv'
}
post {
always {
script {
utils = new Utils()
utils.publish_coverage('coverage.xml')
utils.publish_coverage_native('index.html')
utils.publish_pylint('pylint.out')
}
mergeJunitResults()
}
}
}
stage('Packaging') {
steps {
script {
if (env.JOB_NAME == 'lingo' && env.GIT_BRANCH == 'origin/main') {
sh 'sudo -H -u eobuilder /usr/local/bin/eobuilder -d buster,bullseye lingo'
} else if (env.GIT_BRANCH.startsWith('hotfix/')) {
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d buster,bullseye --branch ${env.GIT_BRANCH} --hotfix lingo"
}
}
}
}
}
post {
always {
script {
utils = new Utils()
utils.mail_notify(currentBuild, env, 'ci+jenkins-lingo@entrouvert.org')
}
}
success {
cleanWs()
}
}
}

12
MANIFEST.in Normal file
View File

@ -0,0 +1,12 @@
# locales
recursive-include lingo/locale *.po *.mo
# static
recursive-include lingo/static *.gif *.png *.css *.js
# templates
recursive-include lingo/templates *.html
include COPYING README
include MANIFEST.in
include VERSION

5
debian/changelog vendored Normal file
View File

@ -0,0 +1,5 @@
lingo (0.0-1) unstable; urgency=low
* Initial release
-- Thomas NOËL <tnoel@entrouvert.com> Mon, 2 May 2022 23:22:44 +0200

33
debian/control vendored Normal file
View File

@ -0,0 +1,33 @@
Source: lingo
Maintainer: Thomas NOËL <tnoel@entrouvert.com>
Section: python
Priority: optional
Build-Depends: python3-setuptools, python3-all, python3-django, debhelper-compat (= 12), dh-python
Standards-Version: 3.9.6
Package: python3-lingo
Architecture: all
Depends: ${misc:Depends}, ${python3:Depends},
python3-distutils,
python3-django,
python3-djangorestframework,
python3-gadjo,
python3-requests,
python3-eopayment
Recommends: python3-django-mellon
Description: Payment and Billing System (Python module)
Package: lingo
Architecture: all
Depends: ${misc:Depends},
python3-lingo (= ${binary:Version}),
python3-hobo,
python3-django-tenant-schemas,
python3-psycopg2,
python3-django-mellon,
uwsgi,
uwsgi-plugin-python3,
python3-uwsgidecorators
Recommends: nginx
Suggests: postgresql
Description: Payment and Billing System

18
debian/debian_config.py vendored Normal file
View File

@ -0,0 +1,18 @@
# This file is sourced by "execfile" from lingo.settings
import os
PROJECT_NAME = 'lingo'
#
# hobotization (multitenant)
#
exec(open('/usr/lib/hobo/debian_config_common.py').read())
#
# local settings
#
exec(open(os.path.join(ETC_DIR, 'settings.py')).read())
# run additional settings snippets
exec(open('/usr/lib/hobo/debian_config_settings_d.py').read())

25
debian/lingo-manage vendored Executable file
View File

@ -0,0 +1,25 @@
#!/bin/sh
NAME=lingo
MANAGE=/usr/lib/$NAME/manage.py
# load Debian default configuration
export LINGO_SETTINGS_FILE=/usr/lib/$NAME/debian_config.py
# check user
if test x$1 = x"--forceuser"
then
shift
elif test $(id -un) != "$NAME"
then
echo "error: must use $0 with user ${NAME}"
exit 1
fi
if test $# -eq 0
then
python3 ${MANAGE} help
exit 1
fi
python3 ${MANAGE} "$@"

7
debian/lingo.dirs vendored Normal file
View File

@ -0,0 +1,7 @@
/etc/lingo
/usr/share/lingo/themes
/usr/lib/lingo
/var/lib/lingo/collectstatic
/var/lib/lingo/tenants
/var/log/lingo
/var/lib/lingo/spooler

3
debian/lingo.docs vendored Normal file
View File

@ -0,0 +1,3 @@
COPYING
README
debian/nginx-example.conf

168
debian/lingo.init vendored Normal file
View File

@ -0,0 +1,168 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: lingo
# Required-Start: $network $local_fs $remote_fs $syslog
# Required-Stop: $network $local_fs $remote_fs $syslog
# Should-start: postgresql
# Should-stop: postgresql
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Billing and Payment System
# Description: Billing and Payment System
### END INIT INFO
# Author: Entr'ouvert <info@entrouvert.com>
set -e
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Billing and Payment System"
NAME=lingo
DAEMON=/usr/bin/uwsgi
RUN_DIR=/run/$NAME
PIDFILE=$RUN_DIR/$NAME.pid
LOG_DIR=/var/log/$NAME
SCRIPTNAME=/etc/init.d/$NAME
BIND=unix:$RUN_DIR/$NAME.sock
LINGO_SETTINGS_FILE=/usr/lib/$NAME/debian_config.py
MANAGE_SCRIPT="/usr/bin/$NAME-manage"
USER=$NAME
GROUP=$NAME
# Exit if the package is not installed
[ -x $MANAGE_SCRIPT ] || exit 0
# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
DAEMON_ARGS=${DAEMON_ARGS:-"--pidfile=$PIDFILE
--uid $USER --gid $GROUP
--ini /etc/$NAME/uwsgi.ini
--spooler /var/lib/$NAME/spooler/
--daemonize /var/log/uwsgi.$NAME.log"}
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
# Define LSB log_* functions.
# Depend on lsb-base (>= 3.0-6) to ensure that this file is present.
. /lib/lsb/init-functions
# Create /run directory
if [ ! -d $RUN_DIR ]; then
install -d -m 755 -o $USER -g $GROUP $RUN_DIR
fi
# environment for wsgi
export LINGO_SETTINGS_FILE
#
# Function that starts the daemon/service
#
do_start()
{
# Return
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
start-stop-daemon --start --quiet --user $USER --exec $DAEMON -- \
$DAEMON_ARGS \
|| return 2
}
#
# Function that stops the daemon/service
#
do_stop()
{
# Return
# 0 if daemon has been stopped
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
$DAEMON --stop $PIDFILE
rm -f $PIDFILE
return 0 # hopefully
}
#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
$DAEMON --reload $PIDFILE
return 0
}
do_migrate() {
log_action_msg "Applying migrations (migrate_schemas).."
su $USER -s /bin/sh -p -c "$MANAGE_SCRIPT migrate_schemas --noinput"
log_action_msg "done"
}
do_collectstatic() {
log_action_msg "Collect static files (collectstatic).."
su $USER -s /bin/sh -p -c "$MANAGE_SCRIPT collectstatic --noinput"
log_action_msg "done"
}
case "$1" in
start)
log_daemon_msg "Starting $DESC " "$NAME"
do_migrate
do_collectstatic
do_start
case "$?" in
0|1) log_end_msg 0 ;;
2) log_end_msg 1 ;;
esac
;;
stop)
log_daemon_msg "Stopping $DESC" "$NAME"
do_stop
case "$?" in
0|1) log_end_msg 0 ;;
2) log_end_msg 1 ;;
esac
;;
status)
status_of_proc -p $PIDFILE "$DAEMON" "$NAME" && exit 0 || exit $?
;;
reload|force-reload)
#
# If do_reload() is not implemented then leave this commented out
# and leave 'force-reload' as an alias for 'restart'.
#
log_daemon_msg "Reloading $DESC" "$NAME"
do_reload
log_end_msg $?
;;
restart|force-reload)
#
# If the "reload" option is implemented then remove the
# 'force-reload' alias
#
log_daemon_msg "Restarting $DESC" "$NAME"
do_stop
case "$?" in
0|1)
do_migrate
do_collectstatic
do_start
case "$?" in
0) log_end_msg 0 ;;
1) log_end_msg 1 ;; # Old process is still running
*) log_end_msg 1 ;; # Failed to start
esac
;;
*)
# Failed to stop
log_end_msg 1
;;
esac
;;
*)
echo "Usage: $SCRIPTNAME {start|stop|status|restart|reload|force-reload}" >&2
exit 3
;;
esac

4
debian/lingo.install vendored Normal file
View File

@ -0,0 +1,4 @@
debian/lingo-manage /usr/bin
debian/settings.py /etc/lingo
debian/uwsgi.ini /etc/lingo
debian/debian_config.py /usr/lib/lingo

50
debian/lingo.postinst vendored Normal file
View File

@ -0,0 +1,50 @@
#! /bin/sh
set -e
NAME="lingo"
USER=$NAME
GROUP=$NAME
CONFIG_DIR="/etc/$NAME"
MANAGE_SCRIPT="/usr/bin/$NAME-manage"
case "$1" in
configure)
# make sure the administrative user exists
if ! getent passwd $USER >/dev/null; then
adduser --disabled-password --quiet --system \
--no-create-home --home /var/lib/$NAME \
--gecos "$NAME user" --group $USER
fi
# ensure dirs ownership
chown $USER:$GROUP /var/log/$NAME
chown $USER:$GROUP /var/lib/$NAME/collectstatic
chown $USER:$GROUP /var/lib/$NAME/tenants
chown $USER:$GROUP /var/lib/$NAME/spooler
# create a secret file
SECRET_FILE=$CONFIG_DIR/secret
if [ ! -f $SECRET_FILE ]; then
echo -n "Generating Django secret..." >&2
cat /dev/urandom | tr -dc [:alnum:]-_\!\%\^:\; | head -c70 > $SECRET_FILE
chown root:$GROUP $SECRET_FILE
chmod 0440 $SECRET_FILE
fi
;;
triggered)
su -s /bin/sh -c "$MANAGE_SCRIPT hobo_deploy --redeploy" $USER
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0

26
debian/lingo.service vendored Normal file
View File

@ -0,0 +1,26 @@
[Unit]
Description=Lingo
After=network.target syslog.target postgresql.service
Wants=postgresql.service
[Service]
Environment=LINGO_SETTINGS_FILE=/usr/lib/%p/debian_config.py
Environment=LANG=C.UTF-8
User=%p
Group=%p
ExecStartPre=/usr/bin/lingo-manage migrate_schemas --noinput --verbosity 1
ExecStartPre=/usr/bin/lingo-manage collectstatic --noinput --link
ExecStartPre=/bin/mkdir -p /var/lib/lingo/spooler/%m/
ExecStart=/usr/bin/uwsgi --ini /etc/%p/uwsgi.ini --spooler /var/lib/lingo/spooler/%m/
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGQUIT
TimeoutStartSec=0
PrivateTmp=true
Restart=on-failure
RuntimeDirectory=lingo
Type=notify
StandardError=syslog
NotifyAccess=all
[Install]
WantedBy=multi-user.target

1
debian/lingo.triggers vendored Normal file
View File

@ -0,0 +1 @@
interest-noawait hobo-redeploy

44
debian/nginx-example.conf vendored Normal file
View File

@ -0,0 +1,44 @@
server {
listen 443;
server_name *-lingo.example.org;
ssl on;
ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;
ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;
access_log /var/log/nginx/lingo.example.org-access.log combined;
error_log /var/log/nginx/lingo.example.org-error.log;
location ~ ^/static/(.+)$ {
root /;
try_files /var/lib/lingo/tenants/$host/static/$1
/var/lib/lingo/tenants/$host/theme/static/$1
/var/lib/lingo/collectstatic/$1
=404;
add_header Access-Control-Allow-Origin *;
}
location ~ ^/media/(.+)$ {
alias /var/lib/lingo/tenants/$host/media/$1;
}
location / {
proxy_pass http://unix:/var/run/lingo/lingo.sock;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-SSL on;
proxy_set_header X-Forwarded-Protocol ssl;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name *-lingo.example.org;
access_log /var/log/nginx/lingo.example.org-access.log combined;
error_log /var/log/nginx/lingo.example.org-error.log;
return 302 https://$host$request_uri;
}

2
debian/py3dist-overrides vendored Normal file
View File

@ -0,0 +1,2 @@
eopayment python3-eopayment
gadjo python3-gadjo

1
debian/python3-lingo.dirs vendored Normal file
View File

@ -0,0 +1 @@
/usr/lib/lingo

2
debian/python3-lingo.docs vendored Normal file
View File

@ -0,0 +1,2 @@
COPYING
README

12
debian/rules vendored Executable file
View File

@ -0,0 +1,12 @@
#!/usr/bin/make -f
# -*- makefile -*-
export PYBUILD_NAME=lingo
export PYBUILD_DISABLE=test
%:
dh $@ --with python3 --buildsystem=pybuild
override_dh_install:
dh_install
mv $(CURDIR)/debian/python3-lingo/usr/bin/manage.py $(CURDIR)/debian/lingo/usr/lib/lingo/manage.py

26
debian/settings.py vendored Normal file
View File

@ -0,0 +1,26 @@
# Configuration for lingo.
# Override with /etc/lingo/settings.d/ files
# Lingo is a Django application: for the full list of settings and their
# values, see https://docs.djangoproject.com/en/3.2/ref/settings/
# For more information on settings see
# https://docs.djangoproject.com/en/3.2/topics/settings/
# WARNING! Quick-start development settings unsuitable for production!
# See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/
# This file is sourced by "exec(open(...).read())" from
# /usr/lib/lingo/debian_config.py
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
# ALLOWED_HOSTS must be correct in production!
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = [
'*',
]
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'

1
debian/source/format vendored Normal file
View File

@ -0,0 +1 @@
3.0 (quilt)

51
debian/uwsgi.ini vendored Normal file
View File

@ -0,0 +1,51 @@
[uwsgi]
auto-procname = true
procname-prefix-spaced = lingo
strict = true
plugin = python3
single-interpreter = true
module = lingo.wsgi:application
need-app = true
http-socket = /run/lingo/lingo.sock
chmod-socket = 666
vacuum = true
spooler-processes = 3
spooler-python-import = lingo.utils.spooler
spooler-python-import = hobo.provisionning.spooler
spooler-max-tasks = 20
master = true
enable-threads = true
harakiri = 120
processes = 500
plugin = cheaper_busyness
cheaper-algo = busyness
cheaper = 5
cheaper-initial = 10
cheaper-overload = 5
cheaper-step = 10
cheaper-busyness-multiplier = 30
cheaper-busyness-min = 20
cheaper-busyness-max = 70
cheaper-busyness-backlog-alert = 16
cheaper-busyness-backlog-step = 2
max-requests = 500
max-worker-lifetime = 7200
buffer-size = 32768
py-tracebacker = /run/lingo/py-tracebacker.sock.
stats = /run/lingo/stats.sock
ignore-sigpipe = true
disable-write-exception = true
if-file = /etc/lingo/uwsgi-local.ini
include = /etc/lingo/uwsgi-local.ini
endif =

22
getlasso3.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/sh
# Get venv site-packages path
DSTDIR=`python3 -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())'`
# Get not venv site-packages path
# Remove first path (assuming that is the venv path)
NONPATH=`echo $PATH | sed 's/^[^:]*://'`
SRCDIR=`PATH=$NONPATH python3 -c 'from distutils.sysconfig import get_python_lib; print(get_python_lib())'`
# Clean up
rm -f $DSTDIR/lasso.*
rm -f $DSTDIR/_lasso.*
# Link
ln -sv /usr/lib/python3/dist-packages/lasso.py $DSTDIR/
for SOFILE in /usr/lib/python3/dist-packages/_lasso.cpython-*.so
do
ln -sv $SOFILE $DSTDIR/
done
exit 0

0
lingo/__init__.py Normal file
View File

View File

@ -0,0 +1,24 @@
# lingo - payment and billing system, french translation
# Copyright (C) 2022 Entr'ouvert
# This file is distributed under the same license as the lingo package.
#
msgid ""
msgstr ""
"Project-Id-Version: lingo 0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-05-02 22:19+0000\n"
"PO-Revision-Date: 2022-05-02 22:13+0000\n"
"Last-Translator: Thomas NOËL <tnoel@entrouvert.com>\n"
"Language: French\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: manager/views.py templates/lingo/manager_homepage.html
msgid "Payments"
msgstr "Paiements"
#: templates/registration/login.html
msgid "Log in"
msgstr "Sidentifier"

View File

24
lingo/manager/urls.py Normal file
View File

@ -0,0 +1,24 @@
# lingo - billing and payment system
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^$', views.homepage, name='manager-homepage'),
url(r'^menu.json$', views.menu_json),
]

50
lingo/manager/views.py Normal file
View File

@ -0,0 +1,50 @@
# lingo - payment and billing system
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
from django.http import HttpResponse
from django.urls import reverse
from django.views.generic import TemplateView
class HomepageView(TemplateView):
template_name = 'lingo/manager_homepage.html'
homepage = HomepageView.as_view()
def menu_json(request):
label = _('Payments')
json_str = json.dumps(
[
{
'label': label,
'slug': 'lingo',
'url': request.build_absolute_uri(reverse('manage-homepage')),
}
]
)
content_type = 'application/json'
for variable in ('jsonpCallback', 'callback'):
if variable in request.GET:
json_str = '%s(%s);' % (request.GET[variable], json_str)
content_type = 'application/javascript'
break
response = HttpResponse(content_type=content_type)
response.write(json_str)
return response

190
lingo/settings.py Normal file
View File

@ -0,0 +1,190 @@
# lingo - payment and bill system
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
Django settings file; it loads the default settings, and local settings
(from a local_settings.py file, or a configuration file set in the
LINGO_SETTINGS_FILE environment variable).
The local settings file should exist, at least to set a suitable SECRET_KEY,
and to disable DEBUG mode in production.
"""
import os
from django.conf import global_settings
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'r^(w+o4*txe1=t+0w*w3*9%idij!yeq1#axpsi4%5*u#3u&)1t'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.humanize',
'eopayment',
'gadjo',
)
MIDDLEWARE = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
# Serve xstatic files, required for gadjo
STATICFILES_FINDERS = list(global_settings.STATICFILES_FINDERS) + ['gadjo.finders.XStaticFinder']
# Templates
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'lingo', 'templates'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.media',
'django.template.context_processors.request',
'django.template.context_processors.static',
'django.template.context_processors.tz',
'django.contrib.messages.context_processors.messages',
],
'builtins': [
'django.contrib.humanize.templatetags.humanize',
],
},
},
]
ROOT_URLCONF = 'lingo.urls'
WSGI_APPLICATION = 'lingo.wsgi.application'
# Database
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
}
}
# Internationalization
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
LOCALE_PATHS = (os.path.join(BASE_DIR, 'lingo', 'locale'),)
# Static files (CSS, JavaScript, Images)
STATIC_URL = '/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
# mode for newly updated files
FILE_UPLOAD_PERMISSIONS = 0o644
# extra variables for templates
TEMPLATE_VARS = {}
# Authentication settings
try:
import mellon
except ImportError:
mellon = None
if mellon is not None:
INSTALLED_APPS += ('mellon',)
AUTHENTICATION_BACKENDS = (
'mellon.backends.SAMLBackend',
'django.contrib.auth.backends.ModelBackend',
)
LOGIN_URL = '/login/'
LOGIN_REDIRECT_URL = '/'
LOGOUT_URL = '/logout/'
MELLON_ATTRIBUTE_MAPPING = {
'email': '{attributes[email][0]}',
'first_name': '{attributes[first_name][0]}',
'last_name': '{attributes[last_name][0]}',
}
MELLON_SUPERUSER_MAPPING = {
'is_superuser': 'true',
}
MELLON_USERNAME_TEMPLATE = '{attributes[name_id_content]}'
MELLON_IDENTITY_PROVIDERS = []
# default site
SITE_BASE_URL = 'http://localhost'
# known services
KNOWN_SERVICES = {}
def debug_show_toolbar(request):
from debug_toolbar.middleware import show_toolbar as dt_show_toolbar # pylint: disable=import-error
return dt_show_toolbar(request) and not request.path.startswith('/__skeleton__/')
DEBUG_TOOLBAR_CONFIG = {'SHOW_TOOLBAR_CALLBACK': debug_show_toolbar}
local_settings_file = os.environ.get(
'LINGO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
)
if os.path.exists(local_settings_file):
with open(local_settings_file) as fd:
exec(fd.read())

View File

@ -0,0 +1,9 @@
{% extends "gadjo/base.html" %}
{% load i18n staticfiles gadjo %}
{% block page-title %}Lingo{% endblock %}
{% block site-title %}Lingo{% endblock %}
{% block logout-url %}{% url "auth_logout" %}{% endblock %}
{% block content %}
{% endblock %}

View File

@ -0,0 +1 @@
{% extends "lingo/base.html" %}

View File

@ -0,0 +1,5 @@
{% extends "lingo/base.html" %}
{% load i18n %}
{% block appbar %}
<h2>{% trans 'Payments' %}</h2>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "lingo/base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button>{% trans 'Log in' %}</button>
</form>
{% endblock %}

53
lingo/urls.py Normal file
View File

@ -0,0 +1,53 @@
# lingo - payment and billing system
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from .manager.urls import urlpatterns as lingo_manager_urls
from .urls_utils import decorated_includes, manager_required
from .views import homepage, login, logout
urlpatterns = [
url(r'^$', homepage, name='homepage'),
url(r'^manage/', decorated_includes(manager_required, include(lingo_manager_urls))),
url(r'^login/$', login, name='auth_login'),
url(r'^logout/$', logout, name='auth_logout'),
]
if 'mellon' in settings.INSTALLED_APPS:
urlpatterns.append(
url(
r'^accounts/mellon/',
include('mellon.urls'),
kwargs={
'template_base': 'lingo/base.html',
},
)
)
# static and media files
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS:
import debug_toolbar # pylint: disable=import-error
urlpatterns = [
url(r'^__debug__/', include(debug_toolbar.urls)),
] + urlpatterns

65
lingo/urls_utils.py Normal file
View File

@ -0,0 +1,65 @@
# lingo - payment and billing system
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Decorating URL includes, <https://djangosnippets.org/snippets/2532/>
from django.contrib.auth.decorators import user_passes_test
from django.core.exceptions import PermissionDenied
from django.urls.resolvers import URLPattern, URLResolver
class DecoratedURLPattern(URLPattern):
def resolve(self, *args, **kwargs):
result = super().resolve(*args, **kwargs)
if result:
result.func = self._decorate_with(result.func)
return result
class DecoratedURLResolver(URLResolver):
def resolve(self, *args, **kwargs):
result = super().resolve(*args, **kwargs)
if result:
result.func = self._decorate_with(result.func)
return result
def decorated_includes(func, includes, *args, **kwargs):
urlconf_module, app_name, namespace = includes
for item in urlconf_module:
if isinstance(item, URLResolver):
item.__class__ = DecoratedURLResolver
else:
item.__class__ = DecoratedURLPattern
item._decorate_with = func
return urlconf_module, app_name, namespace
def manager_required(function=None, login_url=None):
def check_manager(user):
if user and user.is_staff:
return True
if user and not user.is_anonymous:
raise PermissionDenied()
# As the last resort, show the login form
return False
actual_decorator = user_passes_test(check_manager, login_url=login_url)
if function:
return actual_decorator(function)
return actual_decorator

62
lingo/views.py Normal file
View File

@ -0,0 +1,62 @@
# lingo - payment and billing system
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import views as auth_views
from django.http import HttpResponseRedirect
from django.shortcuts import resolve_url
from django.utils.http import quote
from django.views.generic import TemplateView
if 'mellon' in settings.INSTALLED_APPS:
from mellon.utils import get_idps # pylint: disable=import-error
else:
def get_idps():
return []
class LoginView(auth_views.LoginView):
def dispatch(self, request, *args, **kwargs):
if any(get_idps()):
if 'next' not in request.GET:
return HttpResponseRedirect(resolve_url('mellon_login'))
return HttpResponseRedirect(
resolve_url('mellon_login') + '?next=' + quote(request.GET.get('next'))
)
return super().dispatch(request, *args, **kwargs)
login = LoginView.as_view()
def logout(request, next_page=None):
if any(get_idps()):
return HttpResponseRedirect(resolve_url('mellon_logout'))
auth_logout(request)
if next_page is not None:
next_page = resolve_url(next_page)
else:
next_page = '/'
return HttpResponseRedirect(next_page)
class HomepageView(TemplateView):
template_name = 'lingo/homepage.html'
homepage = HomepageView.as_view()

23
lingo/wsgi.py Normal file
View File

@ -0,0 +1,23 @@
# lingo - payment and billing system
# Copyright (C) 2022 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lingo.settings")
application = get_wsgi_application()

10
manage.py Executable file
View File

@ -0,0 +1,10 @@
#!/usr/bin/env python3
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "lingo.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

130
pylint.rc Normal file
View File

@ -0,0 +1,130 @@
[MASTER]
profile=no
persistent=yes
ignore=vendor,Bouncers,ezt.py
cache-size=500
[MESSAGES CONTROL]
disable=
abstract-method,
arguments-differ,
assignment-from-none,
attribute-defined-outside-init,
bad-super-call,
broad-except,
consider-using-dict-comprehension,
consider-using-f-string,
consider-using-set-comprehension,
cyclic-import,
duplicate-code,
exec-used,
fixme,
global-variable-undefined,
import-outside-toplevel,
inconsistent-return-statements,
invalid-name,
keyword-arg-before-vararg,
missing-class-docstring,
missing-function-docstring,
missing-module-docstring,
no-else-return,
no-member,
no-self-use,
non-parent-init-called,
not-callable,
possibly-unused-variable,
protected-access,
raise-missing-from,
redefined-argument-from-local,
redefined-builtin,
redefined-outer-name,
signature-differs,
stop-iteration-return,
super-init-not-called,
superfluous-parens,
too-many-ancestors,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-lines,
too-many-locals,
too-many-nested-blocks,
too-many-return-statements,
too-many-statements,
undefined-loop-variable,
unnecessary-comprehension,
unspecified-encoding,
unsubscriptable-object,
unsupported-membership-test,
unused-argument,
use-a-generator,
use-implicit-booleaness-not-comparison
[REPORTS]
output-format=parseable
include-ids=yes
[BASIC]
no-docstring-rgx=__.*__|_.*
class-rgx=[A-Z_][a-zA-Z0-9_]+$
function-rgx=[a-zA_][a-zA-Z0-9_]{2,70}$
method-rgx=[a-z_][a-zA-Z0-9_]{2,70}$
const-rgx=(([A-Z_][A-Z0-9_]*)|([a-z_][a-z0-9_]*)|(__.*__)|register|urlpatterns)$
good-names=_,i,j,k,e,x,Run,,setUp,tearDown,r,p,s,v,fd
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject,WSGIRequest,Publisher,NullSessionManager
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed.
generated-members=objects,DoesNotExist,id,pk,_meta,base_fields,context
# List of method names used to declare (i.e. assign) instance attributes
defining-attr-methods=__init__,__new__,setUp
[VARIABLES]
init-import=no
dummy-variables-rgx=_|dummy
additional-builtins=_,N_,ngettext
good-names=_,i,j,k,e,x,Run,,setUp,tearDown,r,p,s,v,fd
[SIMILARITIES]
min-similarity-lines=6
ignore-comments=yes
ignore-docstrings=yes
[MISCELLANEOUS]
notes=FIXME,XXX,TODO
[FORMAT]
max-line-length=160
max-module-lines=2000
indent-string=' '
[DESIGN]
max-args=10
max-locals=15
max-returns=6
max-branchs=12
max-statements=50
max-parents=7
max-attributes=7
min-public-methods=0
max-public-methods=50

5
pylint.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -e -x
env
pylint -f parseable --rcfile pylint.rc "$@" | tee pylint.out; test $PIPESTATUS -eq 0

176
setup.py Normal file
View File

@ -0,0 +1,176 @@
#! /usr/bin/env python
import glob
import itertools
import os
import re
import subprocess
import sys
from distutils.cmd import Command
from distutils.command.build import build as _build
from distutils.command.sdist import sdist
from distutils.errors import CompileError
from distutils.spawn import find_executable
from setuptools import find_packages, setup
from setuptools.command.install_lib import install_lib as _install_lib
class eo_sdist(sdist):
def run(self):
if os.path.exists('VERSION'):
os.remove('VERSION')
version = get_version()
with open('VERSION', 'w') as fd:
fd.write(version)
with open('lingo/version.py', 'w') as fd:
fd.write('VERSION = %r' % version)
sdist.run(self)
if os.path.exists('VERSION'):
os.remove('VERSION')
def get_version():
"""Use the VERSION, if absent generates a version with git describe, if not
tag exists, take 0.0- and add the length of the commit log.
"""
if os.path.exists('VERSION'):
with open('VERSION') as v:
return v.read()
if os.path.exists('.git'):
p = subprocess.Popen(
['git', 'describe', '--dirty=.dirty', '--match=v*'],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
result = p.communicate()[0]
if p.returncode == 0:
result = result.decode('ascii').strip()[1:] # strip spaces/newlines and initial v
if '-' in result: # not a tagged version
real_number, commit_count, commit_hash = result.split('-', 2)
version = '%s.post%s+%s' % (real_number, commit_count, commit_hash)
else:
version = result
return version
else:
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
return '0.0'
def data_tree(destdir, sourcedir):
extensions = ['.css', '.png', '.jpeg', '.jpg', '.gif', '.xml', '.html', '.js']
r = []
for root, dirs, files in os.walk(sourcedir):
l = [os.path.join(root, x) for x in files if os.path.splitext(x)[1] in extensions]
r.append((root.replace(sourcedir, destdir, 1), l))
return r
class compile_translations(Command):
description = 'compile message catalogs to MO files via django compilemessages'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
orig_dir = os.getcwd()
try:
from django.core.management import call_command
for path, dirs, files in os.walk('lingo'):
if 'locale' not in dirs:
continue
curdir = os.getcwd()
os.chdir(os.path.realpath(path))
call_command('compilemessages')
os.chdir(curdir)
except ImportError:
sys.stderr.write('!!! Please install Django >= 1.4 to build translations\n')
os.chdir(orig_dir)
class compile_scss(Command):
description = 'compile scss files into css files'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
sass_bin = None
for program in ('sassc', 'sass'):
sass_bin = find_executable(program)
if sass_bin:
break
if not sass_bin:
raise CompileError(
'A sass compiler is required but none was found. See sass-lang.com for choices.'
)
for path, dirnames, filenames in os.walk('lingo'):
for filename in filenames:
if not filename.endswith('.scss'):
continue
if filename.startswith('_'):
continue
subprocess.check_call(
[
sass_bin,
'%s/%s' % (path, filename),
'%s/%s' % (path, filename.replace('.scss', '.css')),
]
)
class build(_build):
sub_commands = [('compile_translations', None)] + _build.sub_commands
class install_lib(_install_lib):
def run(self):
self.run_command('compile_translations')
_install_lib.run(self)
setup(
name='lingo',
version=get_version(),
description='Payments and Bills System',
author='Thomas NOËL',
author_email='tnoel@entrouvert.com',
packages=find_packages(exclude=['tests']),
include_package_data=True,
scripts=('manage.py',),
url='https://dev.entrouvert.org/projects/lingo/',
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
],
install_requires=[
'django>=2.2, <2.3',
'gadjo>=0.53',
'requests',
'eopayment>=1.60',
'djangorestframework>=3.3, <3.10',
],
zip_safe=False,
cmdclass={
'build': build,
'compile_translations': compile_translations,
'install_lib': install_lib,
'sdist': eo_sdist,
},
)

0
tests/__init__.py Normal file
View File

28
tests/conftest.py Normal file
View File

@ -0,0 +1,28 @@
import django_webtest
import pytest
from django.contrib.auth.models import User
from django.core.cache import cache
@pytest.fixture(autouse=True)
def media(settings, tmpdir):
settings.MEDIA_ROOT = str(tmpdir.mkdir('media'))
@pytest.fixture
def app(request):
wtm = django_webtest.WebTestMixin()
wtm._patch_settings()
request.addfinalizer(wtm._unpatch_settings)
cache.clear()
return django_webtest.DjangoTestApp()
@pytest.fixture
def simple_user():
return User.objects.create_user('user', password='user')
@pytest.fixture
def admin_user():
return User.objects.create_superuser('admin', email=None, password='admin')

20
tests/settings.py Normal file
View File

@ -0,0 +1,20 @@
import os
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
},
'dummy': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'},
}
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'TEST': {
'NAME': ('lingo-test-%s' % os.environ.get("BRANCH_NAME", "").replace('/', '-'))[:63],
},
}
}

7
tests/test_homepage.py Normal file
View File

@ -0,0 +1,7 @@
import pytest
pytestmark = pytest.mark.django_db
def test_homepage(app):
assert app.get('/', status=200)

29
tests/test_manager.py Normal file
View File

@ -0,0 +1,29 @@
import pytest
pytestmark = pytest.mark.django_db
def login(app, username='admin', password='admin'):
login_page = app.get('/login/')
login_form = login_page.forms[0]
login_form['username'] = username
login_form['password'] = password
resp = login_form.submit()
assert resp.status_int == 302
return app
def test_unlogged_access(app):
# connect while not being logged in
assert app.get('/manage/', status=302).location.endswith('/login/?next=/manage/')
def test_simple_user_access(app, simple_user):
# connect while being logged as a simple user
app = login(app, username='user', password='user')
assert app.get('/manage/', status=403)
def test_access(app, admin_user):
app = login(app)
assert app.get('/manage/', status=200)

59
tox.ini Normal file
View File

@ -0,0 +1,59 @@
[tox]
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/lingo/{env:BRANCH_NAME:}
envlist = coverage-py3-django22-codestyle, pylint
[testenv]
usedevelop = True
setenv =
DJANGO_SETTINGS_MODULE=lingo.settings
LINGO_SETTINGS_FILE=tests/settings.py
TOX_WORK_DIR={toxworkdir}
SETUPTOOLS_USE_DISTUTILS=stdlib
coverage: COVERAGE=--cov-report xml --cov-report html --cov=lingo/ --cov-config .coveragerc -v
DB_ENGINE=django.db.backends.postgresql_psycopg2
passenv =
BRANCH_NAME
deps =
django22: django>=2.2,<2.3
pytest-cov
pytest-django
pytest-freezegun
pytest!=5.3.3
WebTest
mock<4
httmock
pylint
pylint-django
django-webtest<1.9.3
pyquery
psycopg2-binary<2.9
django-mellon>=1.13
pre-commit
commands =
./getlasso3.sh
python manage.py compilemessages
py.test {env:COVERAGE:} {posargs: --junitxml=junit-{envname}.xml tests/}
codestyle: pre-commit run --all-files --show-diff-on-failure
[testenv:pylint]
setenv =
DJANGO_SETTINGS_MODULE=lingo.settings
LINGO_SETTINGS_FILE=tests/settings.py
TOX_WORK_DIR={toxworkdir}
SETUPTOOLS_USE_DISTUTILS=stdlib
DB_ENGINE=django.db.backends.postgresql_psycopg2
deps =
pytest-django
pytest!=5.3.3
WebTest
mock<4
httmock
django-mellon>=1.13
pylint
pylint-django
django-webtest<1.9.3
pyquery
psycopg2-binary<2.9
commands =
./getlasso3.sh
./pylint.sh lingo/ tests/