From db6cbb712a9180a09872aac81781f6e491046003 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Thomas=20NO=C3=8BL?= <tnoel@entrouvert.com>
Date: Mon, 2 May 2022 23:08:39 +0200
Subject: [PATCH] start a Publik Django application (#64765)

---
 .gitignore                                  |   3 +
 Jenkinsfile                                 |  49 +++++
 MANIFEST.in                                 |  12 ++
 debian/changelog                            |   5 +
 debian/control                              |  33 ++++
 debian/debian_config.py                     |  18 ++
 debian/lingo-manage                         |  25 +++
 debian/lingo.dirs                           |   7 +
 debian/lingo.docs                           |   3 +
 debian/lingo.init                           | 168 +++++++++++++++++
 debian/lingo.install                        |   4 +
 debian/lingo.postinst                       |  50 ++++++
 debian/lingo.service                        |  26 +++
 debian/lingo.triggers                       |   1 +
 debian/nginx-example.conf                   |  44 +++++
 debian/py3dist-overrides                    |   2 +
 debian/python3-lingo.dirs                   |   1 +
 debian/python3-lingo.docs                   |   2 +
 debian/rules                                |  12 ++
 debian/settings.py                          |  26 +++
 debian/source/format                        |   1 +
 debian/uwsgi.ini                            |  51 ++++++
 getlasso3.sh                                |  22 +++
 lingo/__init__.py                           |   0
 lingo/locale/fr/LC_MESSAGES/django.po       |  24 +++
 lingo/manager/__init__.py                   |   0
 lingo/manager/urls.py                       |  24 +++
 lingo/manager/views.py                      |  50 ++++++
 lingo/settings.py                           | 190 ++++++++++++++++++++
 lingo/templates/lingo/base.html             |   9 +
 lingo/templates/lingo/homepage.html         |   1 +
 lingo/templates/lingo/manager_homepage.html |   5 +
 lingo/templates/registration/login.html     |  10 ++
 lingo/urls.py                               |  53 ++++++
 lingo/urls_utils.py                         |  65 +++++++
 lingo/views.py                              |  62 +++++++
 lingo/wsgi.py                               |  23 +++
 manage.py                                   |  10 ++
 pylint.rc                                   | 130 ++++++++++++++
 pylint.sh                                   |   5 +
 setup.py                                    | 177 ++++++++++++++++++
 tests/__init__.py                           |   0
 tests/conftest.py                           |  28 +++
 tests/settings.py                           |  20 +++
 tests/test_homepage.py                      |   7 +
 tests/test_manager.py                       |  29 +++
 tox.ini                                     |  59 ++++++
 47 files changed, 1546 insertions(+)
 create mode 100644 Jenkinsfile
 create mode 100644 MANIFEST.in
 create mode 100644 debian/changelog
 create mode 100644 debian/control
 create mode 100644 debian/debian_config.py
 create mode 100755 debian/lingo-manage
 create mode 100644 debian/lingo.dirs
 create mode 100644 debian/lingo.docs
 create mode 100644 debian/lingo.init
 create mode 100644 debian/lingo.install
 create mode 100644 debian/lingo.postinst
 create mode 100644 debian/lingo.service
 create mode 100644 debian/lingo.triggers
 create mode 100644 debian/nginx-example.conf
 create mode 100644 debian/py3dist-overrides
 create mode 100644 debian/python3-lingo.dirs
 create mode 100644 debian/python3-lingo.docs
 create mode 100755 debian/rules
 create mode 100644 debian/settings.py
 create mode 100644 debian/source/format
 create mode 100644 debian/uwsgi.ini
 create mode 100755 getlasso3.sh
 create mode 100644 lingo/__init__.py
 create mode 100644 lingo/locale/fr/LC_MESSAGES/django.po
 create mode 100644 lingo/manager/__init__.py
 create mode 100644 lingo/manager/urls.py
 create mode 100644 lingo/manager/views.py
 create mode 100644 lingo/settings.py
 create mode 100644 lingo/templates/lingo/base.html
 create mode 100644 lingo/templates/lingo/homepage.html
 create mode 100644 lingo/templates/lingo/manager_homepage.html
 create mode 100644 lingo/templates/registration/login.html
 create mode 100644 lingo/urls.py
 create mode 100644 lingo/urls_utils.py
 create mode 100644 lingo/views.py
 create mode 100644 lingo/wsgi.py
 create mode 100755 manage.py
 create mode 100644 pylint.rc
 create mode 100755 pylint.sh
 create mode 100644 setup.py
 create mode 100644 tests/__init__.py
 create mode 100644 tests/conftest.py
 create mode 100644 tests/settings.py
 create mode 100644 tests/test_homepage.py
 create mode 100644 tests/test_manager.py
 create mode 100644 tox.ini

diff --git a/.gitignore b/.gitignore
index e2f25a5..1d3398c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,3 +11,6 @@ lingo.egg-info/
 .cache
 .coverage
 .pytest_cache/
+junit*xml
+pylint.out
+*.swp
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..cc38316
--- /dev/null
+++ b/Jenkinsfile
@@ -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()
+        }
+    }
+}
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..acad800
--- /dev/null
+++ b/MANIFEST.in
@@ -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
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..b0f1859
--- /dev/null
+++ b/debian/changelog
@@ -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
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..d1b8b51
--- /dev/null
+++ b/debian/control
@@ -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
diff --git a/debian/debian_config.py b/debian/debian_config.py
new file mode 100644
index 0000000..919c14a
--- /dev/null
+++ b/debian/debian_config.py
@@ -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())
diff --git a/debian/lingo-manage b/debian/lingo-manage
new file mode 100755
index 0000000..5677908
--- /dev/null
+++ b/debian/lingo-manage
@@ -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} "$@"
diff --git a/debian/lingo.dirs b/debian/lingo.dirs
new file mode 100644
index 0000000..0d4edb5
--- /dev/null
+++ b/debian/lingo.dirs
@@ -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
diff --git a/debian/lingo.docs b/debian/lingo.docs
new file mode 100644
index 0000000..68b8457
--- /dev/null
+++ b/debian/lingo.docs
@@ -0,0 +1,3 @@
+COPYING
+README
+debian/nginx-example.conf
diff --git a/debian/lingo.init b/debian/lingo.init
new file mode 100644
index 0000000..c1ad345
--- /dev/null
+++ b/debian/lingo.init
@@ -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
diff --git a/debian/lingo.install b/debian/lingo.install
new file mode 100644
index 0000000..32808fb
--- /dev/null
+++ b/debian/lingo.install
@@ -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
diff --git a/debian/lingo.postinst b/debian/lingo.postinst
new file mode 100644
index 0000000..1129ecf
--- /dev/null
+++ b/debian/lingo.postinst
@@ -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
diff --git a/debian/lingo.service b/debian/lingo.service
new file mode 100644
index 0000000..01cc6f3
--- /dev/null
+++ b/debian/lingo.service
@@ -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
diff --git a/debian/lingo.triggers b/debian/lingo.triggers
new file mode 100644
index 0000000..718b667
--- /dev/null
+++ b/debian/lingo.triggers
@@ -0,0 +1 @@
+interest-noawait hobo-redeploy
diff --git a/debian/nginx-example.conf b/debian/nginx-example.conf
new file mode 100644
index 0000000..518cfbe
--- /dev/null
+++ b/debian/nginx-example.conf
@@ -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;
+}
diff --git a/debian/py3dist-overrides b/debian/py3dist-overrides
new file mode 100644
index 0000000..a965dc5
--- /dev/null
+++ b/debian/py3dist-overrides
@@ -0,0 +1,2 @@
+eopayment python3-eopayment
+gadjo python3-gadjo
diff --git a/debian/python3-lingo.dirs b/debian/python3-lingo.dirs
new file mode 100644
index 0000000..ca1d841
--- /dev/null
+++ b/debian/python3-lingo.dirs
@@ -0,0 +1 @@
+/usr/lib/lingo
diff --git a/debian/python3-lingo.docs b/debian/python3-lingo.docs
new file mode 100644
index 0000000..2e3abae
--- /dev/null
+++ b/debian/python3-lingo.docs
@@ -0,0 +1,2 @@
+COPYING
+README
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..47678ac
--- /dev/null
+++ b/debian/rules
@@ -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
diff --git a/debian/settings.py b/debian/settings.py
new file mode 100644
index 0000000..89f8ee6
--- /dev/null
+++ b/debian/settings.py
@@ -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'
diff --git a/debian/source/format b/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/debian/uwsgi.ini b/debian/uwsgi.ini
new file mode 100644
index 0000000..5c938af
--- /dev/null
+++ b/debian/uwsgi.ini
@@ -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 =
diff --git a/getlasso3.sh b/getlasso3.sh
new file mode 100755
index 0000000..9266a72
--- /dev/null
+++ b/getlasso3.sh
@@ -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
diff --git a/lingo/__init__.py b/lingo/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lingo/locale/fr/LC_MESSAGES/django.po b/lingo/locale/fr/LC_MESSAGES/django.po
new file mode 100644
index 0000000..85799cf
--- /dev/null
+++ b/lingo/locale/fr/LC_MESSAGES/django.po
@@ -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 "S’identifier"
diff --git a/lingo/manager/__init__.py b/lingo/manager/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/lingo/manager/urls.py b/lingo/manager/urls.py
new file mode 100644
index 0000000..2aca3ae
--- /dev/null
+++ b/lingo/manager/urls.py
@@ -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),
+]
diff --git a/lingo/manager/views.py b/lingo/manager/views.py
new file mode 100644
index 0000000..4a6bc31
--- /dev/null
+++ b/lingo/manager/views.py
@@ -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
diff --git a/lingo/settings.py b/lingo/settings.py
new file mode 100644
index 0000000..c57343e
--- /dev/null
+++ b/lingo/settings.py
@@ -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())
diff --git a/lingo/templates/lingo/base.html b/lingo/templates/lingo/base.html
new file mode 100644
index 0000000..3df457f
--- /dev/null
+++ b/lingo/templates/lingo/base.html
@@ -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 %}
diff --git a/lingo/templates/lingo/homepage.html b/lingo/templates/lingo/homepage.html
new file mode 100644
index 0000000..985ec62
--- /dev/null
+++ b/lingo/templates/lingo/homepage.html
@@ -0,0 +1 @@
+{% extends "lingo/base.html" %}
diff --git a/lingo/templates/lingo/manager_homepage.html b/lingo/templates/lingo/manager_homepage.html
new file mode 100644
index 0000000..deba865
--- /dev/null
+++ b/lingo/templates/lingo/manager_homepage.html
@@ -0,0 +1,5 @@
+{% extends "lingo/base.html" %}
+{% load i18n %}
+{% block appbar %}
+  <h2>{% trans 'Payments' %}</h2>
+{% endblock %}
diff --git a/lingo/templates/registration/login.html b/lingo/templates/registration/login.html
new file mode 100644
index 0000000..e4fc477
--- /dev/null
+++ b/lingo/templates/registration/login.html
@@ -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 %}
diff --git a/lingo/urls.py b/lingo/urls.py
new file mode 100644
index 0000000..c13674b
--- /dev/null
+++ b/lingo/urls.py
@@ -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
diff --git a/lingo/urls_utils.py b/lingo/urls_utils.py
new file mode 100644
index 0000000..05a863f
--- /dev/null
+++ b/lingo/urls_utils.py
@@ -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
diff --git a/lingo/views.py b/lingo/views.py
new file mode 100644
index 0000000..d0f4f28
--- /dev/null
+++ b/lingo/views.py
@@ -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()
diff --git a/lingo/wsgi.py b/lingo/wsgi.py
new file mode 100644
index 0000000..2847f27
--- /dev/null
+++ b/lingo/wsgi.py
@@ -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()
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..edab5bb
--- /dev/null
+++ b/manage.py
@@ -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)
diff --git a/pylint.rc b/pylint.rc
new file mode 100644
index 0000000..4299676
--- /dev/null
+++ b/pylint.rc
@@ -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
diff --git a/pylint.sh b/pylint.sh
new file mode 100755
index 0000000..ba7a6c3
--- /dev/null
+++ b/pylint.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+set -e -x
+env
+
+pylint -f parseable --rcfile pylint.rc "$@" | tee pylint.out; test $PIPESTATUS -eq 0
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..860413b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,177 @@
+#! /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), ('compile_scss', 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_scss': compile_scss,
+        'compile_translations': compile_translations,
+        'install_lib': install_lib,
+        'sdist': eo_sdist,
+    },
+)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..c287eea
--- /dev/null
+++ b/tests/conftest.py
@@ -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')
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644
index 0000000..a696325
--- /dev/null
+++ b/tests/settings.py
@@ -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],
+        },
+    }
+}
diff --git a/tests/test_homepage.py b/tests/test_homepage.py
new file mode 100644
index 0000000..1d0dfe0
--- /dev/null
+++ b/tests/test_homepage.py
@@ -0,0 +1,7 @@
+import pytest
+
+pytestmark = pytest.mark.django_db
+
+
+def test_homepage(app):
+    assert app.get('/', status=200)
diff --git a/tests/test_manager.py b/tests/test_manager.py
new file mode 100644
index 0000000..bbcd3f3
--- /dev/null
+++ b/tests/test_manager.py
@@ -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)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..a5a1773
--- /dev/null
+++ b/tox.ini
@@ -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/
-- 
2.35.1

