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 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 +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 +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 \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 . + +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 . + +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 . + +""" +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 %} +

{% trans 'Payments' %}

+{% 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 %} +
+{% csrf_token %} +{{ form.as_p }} + +
+{% 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 . + +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 . + +# Decorating URL includes, + +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 . + +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 . + +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..0ee0275 --- /dev/null +++ b/setup.py @@ -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, + }, +) 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/