Compare commits
95 Commits
wip/38317-
...
main
Author | SHA1 | Date |
---|---|---|
Valentin Deniaud | c3eb561c16 | |
Valentin Deniaud | ebc92cfc7f | |
Thomas NOËL | 6e2eac43a6 | |
Thomas NOËL | 5689cf1f0a | |
Frédéric Péters | a71993b73f | |
Frédéric Péters | 7d6b3549b3 | |
Frédéric Péters | 9d333b9957 | |
Valentin Deniaud | c04a5c50bd | |
Valentin Deniaud | 103dff03cb | |
Valentin Deniaud | 5f5d814feb | |
Thomas NOËL | 2c88fcfa68 | |
Frédéric Péters | a9eab5c891 | |
Agate | 7c9e166981 | |
Frédéric Péters | eb44518502 | |
Frédéric Péters | 2bc91fff29 | |
Frédéric Péters | 8242b1b6b9 | |
Agate | 295e994b83 | |
Agate | ee871ebc33 | |
Agate | ed3fc882cd | |
Agate | d8786aae95 | |
Agate | e1f969f813 | |
Agate | 3d9481f67d | |
Frédéric Péters | f0c32435c6 | |
Frédéric Péters | 9e165aebfc | |
Serghei Mihai | da41d0b6bd | |
Serghei Mihai | e2934f92c6 | |
Frédéric Péters | 549fc3ec00 | |
Thomas NOËL | eebe703fca | |
Frédéric Péters | 40fe3f835f | |
Frédéric Péters | ab9b2f110c | |
Frédéric Péters | a72cc63a7f | |
Frédéric Péters | 3a6215856f | |
Frédéric Péters | 2a287e61b9 | |
Frédéric Péters | f934f78403 | |
Frédéric Péters | 901db27863 | |
Frédéric Péters | 72449ed00d | |
Frédéric Péters | c3fc205a85 | |
Emmanuel Cazenave | 7e8e50816f | |
Emmanuel Cazenave | 6432625ff2 | |
Frédéric Péters | aec0edb0c8 | |
Frédéric Péters | 250ef2e74a | |
Nicolas Roche | 36c0e69cb1 | |
Frédéric Péters | 7abee88825 | |
Frédéric Péters | eef27053e1 | |
Frédéric Péters | 3635a9524d | |
Frédéric Péters | ad46ff8cbb | |
Emmanuel Cazenave | 5a850e2a24 | |
Frédéric Péters | 044de41420 | |
Frédéric Péters | 8bb319ec8a | |
Frédéric Péters | ce7f2dd500 | |
Frédéric Péters | 54aae08cfe | |
Frédéric Péters | 70b6a1258a | |
Frédéric Péters | 718fb058c3 | |
Valentin Deniaud | 8545b579c7 | |
Frédéric Péters | ad2942cdb8 | |
Frédéric Péters | 914591e037 | |
Frédéric Péters | f9f922656c | |
Frédéric Péters | 43c19c2a67 | |
Frédéric Péters | 351b056c74 | |
Frédéric Péters | f71bb417f0 | |
Frédéric Péters | ae981257c3 | |
Frédéric Péters | 8c459f523c | |
Frédéric Péters | 72f356e7c7 | |
Frédéric Péters | 775d3b190b | |
Frédéric Péters | bf1864f324 | |
Frédéric Péters | 72f29fcd68 | |
Frédéric Péters | cd9a25a069 | |
Frédéric Péters | bd09632404 | |
Frédéric Péters | 15ce4e23ea | |
Frédéric Péters | 99fb727020 | |
Frédéric Péters | e6065fb410 | |
Frédéric Péters | aa22e751d7 | |
Nicolas Roche | 44afd83770 | |
Nicolas Roche | 126e137998 | |
Nicolas Roche | 3380e678b6 | |
Nicolas Roche | 563b6ae83c | |
Nicolas Roche | 6ee1285701 | |
Nicolas Roche | 5e731e5b59 | |
Frédéric Péters | 8b939a281e | |
Frédéric Péters | 537e37d371 | |
Frédéric Péters | 62d08f4243 | |
Frédéric Péters | 5e6226e167 | |
Frédéric Péters | a78e567583 | |
Frédéric Péters | e2eb510f87 | |
Frédéric Péters | 9a5edc0b00 | |
Frédéric Péters | b562bef51f | |
Frédéric Péters | a8e12b9630 | |
Frédéric Péters | 90c29f0fa0 | |
Frédéric Péters | e91178a2e1 | |
Frédéric Péters | 8112d57b0a | |
Frédéric Péters | 46113c03dd | |
Frédéric Péters | 27af09b510 | |
Frédéric Péters | 4f9bb8b161 | |
Frédéric Péters | f165c7d12d | |
Thomas NOËL | c94b37c09d |
|
@ -0,0 +1,6 @@
|
|||
# trivial: apply black
|
||||
ce7f2dd5000cf1eb462ae18aeeb5ab66913b452f
|
||||
# trivial: apply isort & pyupgrade
|
||||
250ef2e74ae674f7039318787ba85b63f2825c7d
|
||||
# misc: apply double-quote-string-fixer (#79788)
|
||||
103dff03cb7862a6dde22b1a358a7af7f86424fc
|
|
@ -0,0 +1,22 @@
|
|||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: double-quote-string-fixer
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: ['--target-version', 'py37', '--skip-string-normalization', '--line-length', '110']
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ['--profile', 'black', '--line-length', '110']
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: ['--keep-percent-format', '--py37-plus']
|
|
@ -1,4 +1,4 @@
|
|||
@Library('eo-jenkins-lib@master') import eo.Utils
|
||||
@Library('eo-jenkins-lib@main') import eo.Utils
|
||||
|
||||
pipeline {
|
||||
agent any
|
||||
|
@ -16,17 +16,25 @@ pipeline {
|
|||
utils.publish_coverage_native('index.html')
|
||||
utils.publish_pylint('pylint.out')
|
||||
}
|
||||
junit '*_results.xml'
|
||||
mergeJunitResults()
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Packaging') {
|
||||
steps {
|
||||
script {
|
||||
if (env.JOB_NAME == 'welco' && env.GIT_BRANCH == 'origin/master') {
|
||||
sh 'sudo -H -u eobuilder /usr/local/bin/eobuilder welco'
|
||||
env.SHORT_JOB_NAME=sh(
|
||||
returnStdout: true,
|
||||
// given JOB_NAME=gitea/project/PR-46, returns project
|
||||
// given JOB_NAME=project/main, returns project
|
||||
script: '''
|
||||
echo "${JOB_NAME}" | sed "s/gitea\\///" | awk -F/ '{print $1}'
|
||||
'''
|
||||
).trim()
|
||||
if (env.GIT_BRANCH == 'main' || env.GIT_BRANCH == 'origin/main') {
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm ${SHORT_JOB_NAME}"
|
||||
} else if (env.GIT_BRANCH.startsWith('hotfix/')) {
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d stretch --branch ${env.GIT_BRANCH} --hotfix welco"
|
||||
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm --branch ${env.GIT_BRANCH} --hotfix ${SHORT_JOB_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
21
README
21
README
|
@ -36,6 +36,27 @@ Tests
|
|||
pip install pytest pytest-django pytest-mock
|
||||
DJANGO_SETTINGS_MODULE=welco.settings py.test tests/
|
||||
|
||||
|
||||
Code Style
|
||||
----------
|
||||
|
||||
black is used to format the code, using thoses parameters:
|
||||
|
||||
black --target-version py37 --skip-string-normalization --line-length 110
|
||||
|
||||
isort is used to format the imports, using those parameters:
|
||||
|
||||
isort --profile black --line-length 110
|
||||
|
||||
pyupgrade is used to automatically upgrade syntax, using those parameters:
|
||||
|
||||
pyupgrade --keep-percent-format --py37-plus
|
||||
|
||||
There is .pre-commit-config.yaml to use pre-commit to automatically run black,
|
||||
isort and pyupgrade before commits. (execute `pre-commit install` to install
|
||||
the git hook.)
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
9
|
|
@ -2,34 +2,36 @@ Source: welco
|
|||
Maintainer: Frederic Peters <fpters@entrouvert.com>
|
||||
Section: python
|
||||
Priority: optional
|
||||
Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 9), python-django, dh-python, dh-systemd
|
||||
Build-Depends: python3-setuptools, python3-all, debhelper-compat (= 12), python3-django, dh-python
|
||||
Standards-Version: 3.9.6
|
||||
X-Python-Version: >= 2.7
|
||||
|
||||
Package: python-welco
|
||||
Package: python3-welco
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends}, ${python:Depends},
|
||||
python-django (>= 1.8),
|
||||
python-gadjo,
|
||||
python-requests (>= 2.11),
|
||||
python-django-haystack (>= 2.4.0),
|
||||
python-django-reversion (>= 2.0.12),
|
||||
python-django-taggit (>= 0.17.4),
|
||||
Depends: ${misc:Depends}, ${python3:Depends},
|
||||
python3-django (>= 2:2.2),
|
||||
python3-gadjo,
|
||||
python3-requests (>= 2.11),
|
||||
python3-django-haystack (>= 2.4.0),
|
||||
python3-django-taggit (>= 0.17.4),
|
||||
libjs-pdf (<< 1.1)
|
||||
Recommends: python-django-mellon
|
||||
Recommends: python3-django-mellon
|
||||
Description: Multichannel request processing (Python module)
|
||||
|
||||
Package: welco
|
||||
Architecture: all
|
||||
Depends: ${misc:Depends},
|
||||
python-welco (= ${binary:Version}),
|
||||
python-hobo,
|
||||
python-django-tenant-schemas,
|
||||
python-psycopg2,
|
||||
python-django-mellon,
|
||||
python-xstatic-select2,
|
||||
gunicorn,
|
||||
python3-welco (= ${binary:Version}),
|
||||
python3-hobo,
|
||||
python3-django-tenant-schemas,
|
||||
python3-psycopg2,
|
||||
python3-django-mellon,
|
||||
python3-uwsgidecorators,
|
||||
python3-xstatic-select2,
|
||||
uwsgi,
|
||||
uwsgi-plugin-python3,
|
||||
graphicsmagick
|
||||
Recommends: nginx
|
||||
Suggests: postgresql
|
||||
Breaks: python-welco (<< 0.73.post12)
|
||||
Replaces: python-welco (<< 0.73.post12)
|
||||
Description: Multichannel request processing
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# This file is sourced by "execfile" from welco.settings
|
||||
# This file is sourced by "exec(open(..." from welco.settings
|
||||
|
||||
import os
|
||||
|
||||
|
@ -10,12 +10,12 @@ INSTALLED_APPS += ('mellon',)
|
|||
#
|
||||
# hobotization (multitenant)
|
||||
#
|
||||
execfile('/usr/lib/hobo/debian_config_common.py')
|
||||
exec(open('/usr/lib/hobo/debian_config_common.py').read())
|
||||
|
||||
#
|
||||
# local settings
|
||||
#
|
||||
execfile(os.path.join(ETC_DIR, 'settings.py'))
|
||||
exec(open(os.path.join(ETC_DIR, 'settings.py')).read())
|
||||
|
||||
# run additional settings snippets
|
||||
execfile('/usr/lib/hobo/debian_config_settings_d.py')
|
||||
exec(open('/usr/lib/hobo/debian_config_settings_d.py').read())
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
django_ckeditor python3-django-ckeditor
|
||||
gadjo python3-gadjo
|
||||
xstatic_select2 python3-xstatic-select2
|
|
@ -1 +0,0 @@
|
|||
django_ckeditor python-django-ckeditor
|
|
@ -1 +0,0 @@
|
|||
/usr/lib/welco
|
|
@ -1,2 +0,0 @@
|
|||
usr/bin/manage.py /usr/lib/welco
|
||||
usr/lib/python2*/*-packages
|
|
@ -1 +0,0 @@
|
|||
usr/share/javascript/pdf usr/lib/python2.7/dist-packages/welco/sources/mail/static/pdf
|
|
@ -0,0 +1 @@
|
|||
usr/share/javascript/pdf usr/lib/python3/dist-packages/welco/sources/mail/static/pdf
|
|
@ -1,8 +1,12 @@
|
|||
#!/usr/bin/make -f
|
||||
# -*- makefile -*-
|
||||
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
export PYBUILD_NAME=welco
|
||||
export PYBUILD_DISABLE=test
|
||||
|
||||
%:
|
||||
dh $@ --with python2,systemd
|
||||
dh $@ --with python3 --buildsystem=pybuild
|
||||
|
||||
override_dh_install:
|
||||
dh_install
|
||||
mv $(CURDIR)/debian/python3-welco/usr/bin/manage.py $(CURDIR)/debian/welco/usr/lib/welco/manage.py
|
||||
|
|
|
@ -9,21 +9,22 @@
|
|||
# WARNING! Quick-start development settings unsuitable for production!
|
||||
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
|
||||
|
||||
# This file is sourced by "execfile" from /usr/lib/welco/debian_config.py
|
||||
# This file is sourced by "exec(open(...).read())" from
|
||||
# /usr/lib/welco/debian_config.py
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = False
|
||||
TEMPLATE_DEBUG = False
|
||||
|
||||
#ADMINS = (
|
||||
# ADMINS = (
|
||||
# # ('User 1', 'watchdog@example.net'),
|
||||
# # ('User 2', 'janitor@example.net'),
|
||||
#)
|
||||
# )
|
||||
|
||||
# ALLOWED_HOSTS must be correct in production!
|
||||
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
|
||||
ALLOWED_HOSTS = [
|
||||
'*',
|
||||
'*',
|
||||
]
|
||||
|
||||
# Databases
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
[uwsgi]
|
||||
auto-procname = true
|
||||
procname-prefix-spaced = welco
|
||||
|
||||
plugin = python3
|
||||
module = welco.wsgi:application
|
||||
|
||||
http-socket = /run/welco/welco.sock
|
||||
chmod-socket = 666
|
||||
vacuum = true
|
||||
|
||||
spooler-processes = 3
|
||||
spooler-python-import = hobo.provisionning.spooler
|
||||
spooler-max-tasks = 20
|
||||
|
||||
master = true
|
||||
processes = 5
|
||||
harakiri = 120
|
||||
enable-threads = true
|
||||
|
||||
buffer-size = 32768
|
||||
|
||||
py-tracebacker = /run/welco/py-tracebacker.sock.
|
||||
stats = /run/welco/stats.sock
|
||||
memory-report = true
|
||||
|
||||
ignore-sigpipe = true
|
||||
|
||||
if-file = /etc/welco/uwsgi-local.ini
|
||||
include = /etc/welco/uwsgi-local.ini
|
||||
endif =
|
|
@ -18,9 +18,9 @@ fi
|
|||
|
||||
if test $# -eq 0
|
||||
then
|
||||
python ${MANAGE} help
|
||||
python3 ${MANAGE} help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python ${MANAGE} "$@"
|
||||
python3 ${MANAGE} "$@"
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
SHELL=/bin/sh
|
||||
PATH=/usr/sbin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
*/10 6-22 * * * welco /usr/bin/welco-manage tenant_command feed_mail_maarch --all-tenants
|
||||
*/10 6-22 * * * welco /usr/bin/welco-manage tenant_command feed_mail_maarch --all-tenants -v0
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
/sbin/runuser -u welco /usr/bin/welco-manage -- tenant_command clearsessions --all-tenants
|
||||
/sbin/runuser -u welco /usr/bin/welco-manage -- tenant_command clearsessions --all-tenants -v0
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/etc/welco
|
||||
/usr/lib/welco
|
||||
/var/lib/welco/collectstatic
|
||||
/var/lib/welco/spooler
|
||||
/var/lib/welco/tenants
|
||||
/var/log/welco
|
||||
|
|
|
@ -16,14 +16,12 @@
|
|||
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||
DESC="Multichannel request processing"
|
||||
NAME=welco
|
||||
DAEMON=/usr/bin/gunicorn
|
||||
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
|
||||
WORKERS=5
|
||||
TIMEOUT=30
|
||||
|
||||
WELCO_SETTINGS_FILE=/usr/lib/$NAME/debian_config.py
|
||||
MANAGE_SCRIPT="/usr/bin/$NAME-manage"
|
||||
|
@ -37,17 +35,11 @@ GROUP=$NAME
|
|||
# Read configuration variable file if it is present
|
||||
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
|
||||
|
||||
DAEMON_ARGS=${DAEMON_ARGS:-"--pid $PIDFILE \
|
||||
--user $USER --group $GROUP \
|
||||
--daemon \
|
||||
--access-logfile $LOG_DIR/gunicorn-access.log \
|
||||
--log-file $LOG_DIR/gunicorn-error.log \
|
||||
--bind=$BIND \
|
||||
--workers=$WORKERS \
|
||||
--worker-class=sync \
|
||||
--timeout=$TIMEOUT \
|
||||
--name $NAME \
|
||||
$NAME.wsgi:application"}
|
||||
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
|
||||
|
@ -73,9 +65,7 @@ do_start()
|
|||
# 0 if daemon has been started
|
||||
# 1 if daemon was already running
|
||||
# 2 if daemon could not be started
|
||||
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|
||||
|| return 1
|
||||
start-stop-daemon --start --quiet --exec $DAEMON -- \
|
||||
start-stop-daemon --start --quiet --user $USER --exec $DAEMON -- \
|
||||
$DAEMON_ARGS \
|
||||
|| return 2
|
||||
}
|
||||
|
@ -90,32 +80,16 @@ do_stop()
|
|||
# 1 if daemon was already stopped
|
||||
# 2 if daemon could not be stopped
|
||||
# other if a failure occurred
|
||||
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
|
||||
RETVAL="$?"
|
||||
[ "$RETVAL" = 2 ] && return 2
|
||||
# Wait for children to finish too if this is a daemon that forks
|
||||
# and if the daemon is only ever run from this initscript.
|
||||
# If the above conditions are not satisfied then add some other code
|
||||
# that waits for the process to drop all resources that could be
|
||||
# needed by services started subsequently. A last resort is to
|
||||
# sleep for some time.
|
||||
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
|
||||
[ "$?" = 2 ] && return 2
|
||||
# Many daemons don't delete their pidfiles when they exit.
|
||||
$DAEMON --stop $PIDFILE
|
||||
rm -f $PIDFILE
|
||||
return "$RETVAL"
|
||||
return 0 # hopefully
|
||||
}
|
||||
|
||||
#
|
||||
# Function that sends a SIGHUP to the daemon/service
|
||||
#
|
||||
do_reload() {
|
||||
#
|
||||
# If the daemon can reload its configuration without
|
||||
# restarting (for example, when it is sent a SIGHUP),
|
||||
# then implement that here.
|
||||
#
|
||||
start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name `basename $DAEMON`
|
||||
$DAEMON --reload $PIDFILE
|
||||
return 0
|
||||
}
|
||||
|
||||
|
@ -151,7 +125,7 @@ case "$1" in
|
|||
esac
|
||||
;;
|
||||
status)
|
||||
status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
|
||||
status_of_proc -p $PIDFILE "$DAEMON" "$NAME" && exit 0 || exit $?
|
||||
;;
|
||||
reload|force-reload)
|
||||
#
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
debian/welco-manage /usr/bin
|
||||
debian/settings.py /etc/welco
|
||||
debian/uwsgi.ini /etc/welco
|
||||
debian/debian_config.py /usr/lib/welco
|
||||
debian/welco.service /lib/systemd/system
|
||||
|
|
|
@ -20,6 +20,7 @@ case "$1" in
|
|||
# ensure dirs ownership
|
||||
chown $USER:$GROUP /var/log/$NAME
|
||||
chown $USER:$GROUP /var/lib/$NAME/collectstatic
|
||||
chown $USER:$GROUP /var/lib/$NAME/spooler
|
||||
chown $USER:$GROUP /var/lib/$NAME/tenants
|
||||
# create a secret file
|
||||
SECRET_FILE=$CONFIG_DIR/secret
|
||||
|
|
|
@ -4,23 +4,23 @@ After=network.target postgresql.service
|
|||
Wants=postgresql.service
|
||||
|
||||
[Service]
|
||||
SyslogIdentifier=uwsgi/%p
|
||||
Environment=WELCO_SETTINGS_FILE=/usr/lib/%p/debian_config.py
|
||||
Environment=LANG=C.UTF-8
|
||||
User=%p
|
||||
Group=%p
|
||||
ExecStartPre=/usr/bin/welco-manage migrate_schemas --noinput
|
||||
ExecStartPre=/usr/bin/welco-manage migrate_schemas --noinput --verbosity 1
|
||||
ExecStartPre=/usr/bin/welco-manage collectstatic --noinput
|
||||
ExecStart=/usr/bin/gunicorn \
|
||||
--bind unix:/run/%p/%p.sock \
|
||||
--worker-class=sync \
|
||||
--workers 5 \
|
||||
--timeout=30 \
|
||||
--name %p \
|
||||
%p.wsgi:application
|
||||
ExecStartPre=/bin/mkdir -p /var/lib/welco/spooler/%m/
|
||||
ExecStart=/usr/bin/uwsgi --ini /etc/%p/uwsgi.ini --spooler /var/lib/welco/spooler/%m/
|
||||
ExecReload=/bin/kill -HUP $MAINPID
|
||||
KillSignal=SIGQUIT
|
||||
TimeoutStartSec=0
|
||||
PrivateTmp=true
|
||||
Restart=on-failure
|
||||
RuntimeDirectory=welco
|
||||
Type=notify
|
||||
NotifyAccess=all
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "welco.settings")
|
||||
if __name__ == '__main__':
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'welco.settings')
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
|
||||
|
|
|
@ -3,5 +3,4 @@ gadjo
|
|||
django-select2
|
||||
-e git+https://git.entrouvert.org/debian/django-ckeditor.git#egg=django_ckeditor
|
||||
django-haystack
|
||||
django-reversion
|
||||
django-taggit
|
||||
|
|
41
setup.py
41
setup.py
|
@ -1,15 +1,14 @@
|
|||
#! /usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from setuptools.command.install_lib import install_lib as _install_lib
|
||||
from distutils.cmd import Command
|
||||
from distutils.command.build import build as _build
|
||||
from distutils.command.sdist import sdist
|
||||
from distutils.cmd import Command
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
from setuptools.command.install_lib import install_lib as _install_lib
|
||||
|
||||
|
||||
class eo_sdist(sdist):
|
||||
|
@ -25,16 +24,18 @@ class eo_sdist(sdist):
|
|||
|
||||
|
||||
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.
|
||||
'''
|
||||
"""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', 'r') as v:
|
||||
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)
|
||||
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
|
||||
|
@ -45,9 +46,7 @@ def get_version():
|
|||
version = result
|
||||
return version
|
||||
else:
|
||||
return '0.0.post%s' % len(
|
||||
subprocess.check_output(
|
||||
['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0.post%s' % len(subprocess.check_output(['git', 'rev-list', 'HEAD']).splitlines())
|
||||
return '0.0'
|
||||
|
||||
|
||||
|
@ -65,6 +64,7 @@ class compile_translations(Command):
|
|||
curdir = os.getcwd()
|
||||
try:
|
||||
from django.core.management import call_command
|
||||
|
||||
for path, dirs, files in os.walk('welco'):
|
||||
if 'locale' not in dirs:
|
||||
continue
|
||||
|
@ -106,20 +106,19 @@ setup(
|
|||
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
],
|
||||
install_requires=['django>=1.8,<1.12',
|
||||
install_requires=[
|
||||
'django>=2.2,<3.3',
|
||||
'gadjo',
|
||||
'django-ckeditor<=4.5.3',
|
||||
'django-haystack<2.8',
|
||||
'django-reversion>=2.0',
|
||||
'django-taggit',
|
||||
'djangorestframework>=3.3, <3.7',
|
||||
'django-ckeditor<4.5.4',
|
||||
'django-haystack<3.2.2',
|
||||
'django-taggit<3.1.1',
|
||||
'djangorestframework>=3.3,<3.15',
|
||||
'requests',
|
||||
'whoosh',
|
||||
'XStatic-Select2',
|
||||
'python-dateutil',
|
||||
],
|
||||
],
|
||||
zip_safe=False,
|
||||
cmdclass={
|
||||
'build': build,
|
||||
|
|
|
@ -14,8 +14,8 @@
|
|||
# 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 pytest
|
||||
import django_webtest
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -51,3 +51,47 @@ def mail_group(db, settings, user):
|
|||
mail_roles = channel_roles.setdefault('mail', [])
|
||||
mail_roles.append('mail')
|
||||
return group
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def phone_group(db, settings, user):
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
# add mail group to default user
|
||||
group = Group.objects.create(name='phone')
|
||||
user.groups.add(group)
|
||||
|
||||
# define authorization of phone group on phone channel
|
||||
channel_roles = getattr(settings, 'CHANNEL_ROLES', {})
|
||||
phone_roles = channel_roles.setdefault('phone', [])
|
||||
phone_roles.append('phone')
|
||||
return group
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def counter_group(db, settings, user):
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
# add mail group to default user
|
||||
group = Group.objects.create(name='counter')
|
||||
user.groups.add(group)
|
||||
|
||||
# define authorization of counter group on counter channel
|
||||
channel_roles = getattr(settings, 'CHANNEL_ROLES', {})
|
||||
counter_roles = channel_roles.setdefault('counter', [])
|
||||
counter_roles.append('counter')
|
||||
return group
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def kb_group(db, settings, user):
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
# add mail group to default user
|
||||
group = Group.objects.create(name='kb')
|
||||
user.groups.add(group)
|
||||
|
||||
# define authorization of kb group to manage then knowledge base
|
||||
kb_manage_roles = getattr(settings, 'KB_MANAGE_ROLES', [])
|
||||
kb_manage_roles.append('kb')
|
||||
return group
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
# welco - multichannel request processing
|
||||
# Copyright (C) 2020 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 unittest import mock
|
||||
|
||||
import httmock
|
||||
import pytest
|
||||
import requests
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from welco.sources.mail.models import Mail
|
||||
|
||||
|
||||
def test_get_contacts_zone_view(app, db):
|
||||
resp = app.get('/ajax/contacts', status=200)
|
||||
assert resp.html.find('button')['data-url'] == '/contacts/add/'
|
||||
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), contact_id='42')
|
||||
source_type = ContentType.objects.get_for_model(Mail).pk
|
||||
resp = app.get('/ajax/contacts', params={'source_type': source_type, 'source_pk': mail.pk}, status=200)
|
||||
assert resp.html.find('a').text == '...'
|
||||
assert resp.html.find('a')['data-page-slug'] == '42'
|
||||
|
||||
|
||||
def test_post_contacts_zone_view(app, db):
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
|
||||
assert not mail.contact_id
|
||||
source_type = ContentType.objects.get_for_model(Mail).pk
|
||||
resp = app.post(
|
||||
'/ajax/contacts', params={'source_type': source_type, 'source_pk': mail.pk, 'user_id': 42}, status=200
|
||||
)
|
||||
assert resp.text == 'ok'
|
||||
assert Mail.objects.get(id=mail.pk).contact_id == '42'
|
||||
|
||||
|
||||
def test_search_json_view_without_channel(app):
|
||||
app.get('/contacts/search/json/', status=403)
|
||||
|
||||
|
||||
def test_search_json_view_without_query(app, user, mail_group):
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/contacts/search/json/', status=200)
|
||||
assert resp.content_type == 'application/json'
|
||||
assert resp.json == {'data': []}
|
||||
|
||||
|
||||
def test_search_json_view(settings, app, user, mail_group):
|
||||
settings.KNOWN_SERVICES = {
|
||||
'wcs': {
|
||||
'demarches': {
|
||||
'url': 'http://wcs.example.net/',
|
||||
'orig': 'http://welco.example.net/',
|
||||
'secret': 'xxx',
|
||||
}
|
||||
}
|
||||
}
|
||||
app.set_user(user.username)
|
||||
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/', method='GET')
|
||||
def response(url, request):
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {'err': 1, 'msg': 'oups'}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
with httmock.HTTMock(response):
|
||||
with pytest.raises(Exception, match='oups'):
|
||||
resp = app.get('/contacts/search/json/', params={'q': 'Doe'})
|
||||
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/', method='GET')
|
||||
def response(url, request):
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {
|
||||
'data': [
|
||||
{
|
||||
'user_display_name': 'John Doe',
|
||||
'user_email': 'john@example.net',
|
||||
'user_var_phone': '0123456789',
|
||||
'user_var_mobile': '0612345789',
|
||||
'user_id': '42',
|
||||
'user_roles': [
|
||||
{
|
||||
'name': 'Agent',
|
||||
'text': 'Agent',
|
||||
'slug': 'agent',
|
||||
'id': '8d73434814484aa0b8555ac9c68a9300',
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
'err': 0,
|
||||
}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
with httmock.HTTMock(response):
|
||||
resp = app.get('/contacts/search/json/', params={'q': 'Doe'}, status=200)
|
||||
assert resp.content_type == 'application/json'
|
||||
assert resp.json['data'][0]['user_display_name'] == 'John Doe'
|
||||
|
||||
|
||||
def test_contact_detail_fragment_view(settings, app, db):
|
||||
settings.KNOWN_SERVICES = {
|
||||
'wcs': {
|
||||
'demarches': {
|
||||
'url': 'http://wcs.example.net/',
|
||||
'orig': 'http://welco.example.net/',
|
||||
'secret': 'xxx',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
|
||||
def response(url, request):
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {
|
||||
'user_display_name': 'John Doe',
|
||||
'user_email': 'john@example.net',
|
||||
'user_var_phone': '0123456789',
|
||||
'user_var_mobile': '0612345789',
|
||||
'user_id': '42',
|
||||
'user_roles': [
|
||||
{'name': 'Agent', 'text': 'Agent', 'slug': 'agent', 'id': '8d73434814484aa0b8555ac9c68a9300'}
|
||||
],
|
||||
}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
with httmock.HTTMock(response):
|
||||
resp = app.get('/ajax/contacts/42/', status=200)
|
||||
assert resp.html.find('h3').text == 'John Doe'
|
||||
assert resp.html.find('p').text == 'Agent'
|
||||
assert resp.html.find('li').text == 'Phone: 0123456789'
|
||||
|
||||
# unused 'is_pinned_user' context
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), contact_id='42')
|
||||
source_type = ContentType.objects.get_for_model(Mail).pk
|
||||
with httmock.HTTMock(response):
|
||||
resp = app.get(
|
||||
'/ajax/contacts/42/', params={'source_type': source_type, 'source_pk': mail.pk}, status=200
|
||||
)
|
||||
assert resp.html.find('h3').text == 'John Doe'
|
||||
|
||||
|
||||
def test_get_contact_add_view(app):
|
||||
resp = app.get('/contacts/add/', status=200)
|
||||
assert resp.html.find('select')['name'] == 'title'
|
||||
assert resp.html.find('input', {'id': 'id_first_name'})['name'] == 'first_name'
|
||||
|
||||
|
||||
@mock.patch('welco.contacts.views.time.sleep')
|
||||
def test_post_contact_add_view(mocked_sleep, settings, app, db):
|
||||
settings.CONTACT_SEND_REGISTRATION_EMAIL = True
|
||||
settings.KNOWN_SERVICES = {
|
||||
'authentic': {
|
||||
'connexion': {
|
||||
'url': 'http://authentic.example.net/',
|
||||
'orig': 'http://welco.example.net/',
|
||||
'secret': 'xxx',
|
||||
}
|
||||
},
|
||||
'wcs': {
|
||||
'demarches': {
|
||||
'url': 'http://wcs.example.net/',
|
||||
'orig': 'http://welco.example.net/',
|
||||
'secret': 'xxx',
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
# normal case
|
||||
@httmock.urlmatch(netloc='authentic.example.net', path='/api/users/', method='POST')
|
||||
def authentic_response(url, request):
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {'uuid': '42'}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
|
||||
def wcs_response(url, request):
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {
|
||||
'user_display_name': 'John Doe',
|
||||
'id': '43',
|
||||
}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
with httmock.HTTMock(authentic_response, wcs_response):
|
||||
resp = app.post(
|
||||
'/contacts/add/',
|
||||
params={
|
||||
'title': 'Mr',
|
||||
'first_name': 'John',
|
||||
'last_name': 'Doe',
|
||||
},
|
||||
status=200,
|
||||
)
|
||||
assert resp.content_type == 'application/json'
|
||||
assert resp.json['data']['user_id'] == '43'
|
||||
|
||||
# timeout
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
|
||||
def wcs_no_response(url, request):
|
||||
return httmock.response(404)
|
||||
|
||||
with httmock.HTTMock(authentic_response, wcs_no_response):
|
||||
resp = app.post('/contacts/add/', status=200)
|
||||
assert resp.content_type == 'application/json'
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['data'] == 'timeout when calling wcs'
|
||||
|
||||
# error
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
|
||||
def wcs_no_response(url, request):
|
||||
return httmock.response(500)
|
||||
|
||||
with httmock.HTTMock(authentic_response, wcs_no_response):
|
||||
with pytest.raises(requests.HTTPError):
|
||||
resp = app.post('/contacts/add/', status=200)
|
|
@ -0,0 +1,37 @@
|
|||
# welco - multichannel request processing
|
||||
# Copyright (C) 2020 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 pytest
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
from welco.sources.mail.models import Mail
|
||||
|
||||
|
||||
def test_feed_mail_command(settings, tmpdir, db):
|
||||
settings.MEDIA_ROOT = str(tmpdir)
|
||||
path1 = '%s/mail1.txt' % str(tmpdir)
|
||||
path2 = '%s/mail2.txt' % str(tmpdir)
|
||||
path3 = '%s/mail3.txt' % str(tmpdir)
|
||||
open(path2, 'w').write('not a PDF')
|
||||
open(path3, 'w').write('%PDF- is a PDF')
|
||||
|
||||
call_command('feed_mail', '--category', 'foobar', path1, path2, path3)
|
||||
Mail.objects.count() == 1
|
||||
Mail.objects.all()[0].scanner_category == 'foobar'
|
||||
|
||||
with pytest.raises(CommandError, match='nothing got imported'):
|
||||
call_command('feed_mail', '--category', path1, path2)
|
|
@ -0,0 +1,183 @@
|
|||
# welco - multichannel request processing
|
||||
# Copyright (C) 2020 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
|
||||
|
||||
import httmock
|
||||
import requests
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.encoding import force_str
|
||||
from webtest import Upload
|
||||
|
||||
from welco.sources.mail.models import Mail
|
||||
|
||||
|
||||
def test_viewer_view(app):
|
||||
resp = app.get('/mail/viewer/', status=302)
|
||||
assert resp.location == '?file='
|
||||
resp = resp.follow()
|
||||
assert resp.html.find('title').text == 'PDF.js viewer'
|
||||
|
||||
resp = app.get('/mail/viewer/', {'file': 'tests/test.pdf'}, status=200)
|
||||
assert resp.html.find('title').text == 'PDF.js viewer'
|
||||
|
||||
|
||||
def test_get_feeder_view(app, user):
|
||||
resp = app.get('/mail/feeder/', status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/mail/feeder/', status=200)
|
||||
assert resp.html.find('h2').text == 'Mail Feeder'
|
||||
|
||||
|
||||
def test_post_feeder_view(app, user):
|
||||
app.set_user(user.username)
|
||||
resp = app.post('/mail/feeder/', params={'mail': Upload('filename.txt', b'contents')}, status=302)
|
||||
assert resp.location == '/mail/feeder/'
|
||||
resp = resp.follow()
|
||||
assert resp.html.find('li', {'class': 'info'}).text == '1 files uploaded successfully.'
|
||||
|
||||
|
||||
def test_qualification_save_view(settings, app, db):
|
||||
settings.KNOWN_SERVICES = {
|
||||
'wcs': {
|
||||
'demarches': {
|
||||
'url': 'http://wcs.example.net/',
|
||||
}
|
||||
}
|
||||
}
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), subject='spam')
|
||||
assert not mail.contact_id
|
||||
source_type = ContentType.objects.get_for_model(Mail).pk
|
||||
resp = app.post(
|
||||
'/ajax/qualification-mail-save',
|
||||
params={'source_type': source_type, 'source_pk': mail.pk, 'subject': 'eggs'},
|
||||
status=302,
|
||||
)
|
||||
assert resp.location == '/ajax/qualification?source_type=%s&source_pk=%s' % (source_type, mail.pk)
|
||||
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/formdefs/', method='GET')
|
||||
def response_get(url, request):
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {
|
||||
'err': 0,
|
||||
'data': [
|
||||
{
|
||||
'title': 'Foo',
|
||||
'slug': 'foo',
|
||||
}
|
||||
],
|
||||
}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
with httmock.HTTMock(response_get):
|
||||
resp = resp.follow()
|
||||
assert resp.html.find('option', {'value': 'demarches:foo'}).text == 'Foo'
|
||||
assert Mail.objects.get(id=mail.pk).subject == 'eggs'
|
||||
|
||||
|
||||
def test_edit_note_view(app, user):
|
||||
resp = app.get('/ajax/mail/edit-note/', status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
|
||||
app.set_user(user.username)
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), note='spam')
|
||||
resp = app.get('/ajax/mail/edit-note/', params={'mail': mail.pk}, status=200)
|
||||
assert resp.html.find('h2').text == 'Note'
|
||||
assert resp.html.find('textarea', {'name': 'note'}).text == 'spam'
|
||||
|
||||
resp.form['note'] = 'eggs'
|
||||
resp = resp.form.submit()
|
||||
assert resp.content_type == 'text/html'
|
||||
assert resp.text == '{"result": "ok"}'
|
||||
assert Mail.objects.get(id=mail.pk).note == 'eggs'
|
||||
|
||||
|
||||
def test_note_view(app, user):
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
|
||||
resp = app.get('/ajax/mail/note/%s' % mail.pk, status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/ajax/mail/note/%s' % mail.pk, status=200)
|
||||
assert resp.text == '+'
|
||||
assert not Mail.objects.get(id=mail.pk).note # mail object is unchanged
|
||||
|
||||
|
||||
def test_reject_view(settings, app, user):
|
||||
settings.MAARCH_FEED = {
|
||||
'URL': 'http://maarch.example.net',
|
||||
'ENABLE': True,
|
||||
'USERNAME': 'xxx',
|
||||
'PASSWORD': 'yyy',
|
||||
'STATUS_REFUSED': 'FOO',
|
||||
}
|
||||
resp = app.post('/ajax/mail/reject', status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
|
||||
app.set_user(user.username)
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), external_id='maarch-42')
|
||||
|
||||
@httmock.urlmatch(netloc='maarch.example.net', path='/rest/res/resource/status', method='PUT')
|
||||
def response_ok(url, request):
|
||||
assert json.loads(force_str(request.body)) == {'status': 'FOO', 'resId': ['42']}
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {'maarch_say': 'ok'}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
with httmock.HTTMock(response_ok):
|
||||
resp = app.post('/ajax/mail/reject', params={'source_pk': mail.pk}, status=200)
|
||||
assert Mail.objects.count() == 0
|
||||
|
||||
# errors
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), external_id='maarch-42')
|
||||
|
||||
@httmock.urlmatch(netloc='maarch.example.net', path='/rest/res/resource/status', method='PUT')
|
||||
def response_error1(url, request):
|
||||
raise requests.RequestException
|
||||
|
||||
with httmock.HTTMock(response_error1):
|
||||
resp = app.post('/ajax/mail/reject', params={'source_pk': mail.pk})
|
||||
assert Mail.objects.get(id=mail.pk)
|
||||
|
||||
@httmock.urlmatch(netloc='maarch.example.net', path='/rest/res/resource/status', method='PUT')
|
||||
def response_error2(url, request):
|
||||
return httmock.response(500)
|
||||
|
||||
with httmock.HTTMock(response_error2):
|
||||
resp = app.post('/ajax/mail/reject', params={'source_pk': mail.pk})
|
||||
assert Mail.objects.get(id=mail.pk)
|
||||
|
||||
@httmock.urlmatch(netloc='maarch.example.net', path='/rest/res/resource/status', method='PUT')
|
||||
def response_error3(url, request):
|
||||
return httmock.response(200, 'not a json')
|
||||
|
||||
with httmock.HTTMock(response_error3):
|
||||
resp = app.post('/ajax/mail/reject', params={'source_pk': mail.pk})
|
||||
assert Mail.objects.get(id=mail.pk)
|
||||
|
||||
|
||||
def test_mail_count_view(app, user):
|
||||
resp = app.get('/ajax/count/mail/', status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
|
||||
Mail.objects.create(content=ContentFile('foo', name='bar.txt'), status='done-42')
|
||||
Mail.objects.create(content=ContentFile('foo', name='bar.txt'), status='43')
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/ajax/count/mail/', status=200)
|
||||
assert resp.content_type == 'application/json'
|
||||
assert resp.json == {'count': 1}
|
|
@ -0,0 +1,208 @@
|
|||
# welco - multichannel request processing
|
||||
# Copyright (C) 2020 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 unittest import mock
|
||||
|
||||
import httmock
|
||||
import pytest
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.files.base import ContentFile
|
||||
|
||||
from welco.qualif.models import Association
|
||||
from welco.sources.mail.models import Mail
|
||||
|
||||
|
||||
def login(app, username='toto', password='toto'):
|
||||
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
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def logged_app(app, user):
|
||||
return login(app)
|
||||
|
||||
|
||||
def test_unlogged_access(app):
|
||||
# connect while not being logged in
|
||||
assert app.get('/', status=302).location.endswith('/login/?next=/')
|
||||
|
||||
|
||||
def test_no_channel_access(logged_app):
|
||||
logged_app.get('/', status=403)
|
||||
|
||||
|
||||
def test_access(logged_app, mail_group):
|
||||
resp = logged_app.get('/', status=302)
|
||||
assert resp.location == 'mail/'
|
||||
|
||||
|
||||
def test_logout(logged_app):
|
||||
app = logged_app
|
||||
app.get('/logout/')
|
||||
assert app.get('/', status=302).location.endswith('/login/?next=/')
|
||||
|
||||
|
||||
@mock.patch('welco.views.get_idps', return_value=[{'METADATA': '...'}])
|
||||
@mock.patch('welco.views.resolve_url', return_value='foo-url')
|
||||
def test_mellon_idp_redirections(mocked_resolv_url, mocked_get_idps, app):
|
||||
resp = app.get('/login/', status=302)
|
||||
assert resp.location == 'foo-url'
|
||||
resp = app.get('/login/?next=http://foo/?bar', status=302)
|
||||
assert resp.location == 'foo-url?next=http%3A//foo/%3Fbar'
|
||||
resp = app.get('/logout/', status=302)
|
||||
assert resp.location == 'foo-url'
|
||||
|
||||
|
||||
def test_mail_view(app, user, mail_group):
|
||||
resp = app.get('/mail/', status=302)
|
||||
assert resp.location == '/login/?next=/mail/'
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/mail/', status=200)
|
||||
assert resp.html.find('h2').text == 'Mails'
|
||||
|
||||
|
||||
def test_no_channel_access_on_mail_view(app, user):
|
||||
app.set_user(user.username)
|
||||
app.get('/mail/', status=403)
|
||||
|
||||
|
||||
def test_phone_view(app, user, phone_group):
|
||||
resp = app.get('/phone/', status=302)
|
||||
assert resp.location == '/login/?next=/phone/'
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/phone/', status=200)
|
||||
assert resp.html.find('h2').text == 'Phone Call'
|
||||
|
||||
|
||||
def test_counter_view(app, user, counter_group):
|
||||
resp = app.get('/counter/', status=302)
|
||||
assert resp.location == '/login/?next=/counter/'
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/counter/', status=200)
|
||||
assert resp.html.find('h2').text == 'Counter'
|
||||
|
||||
|
||||
def test_kb_view(app, user, kb_group):
|
||||
resp = app.get('/kb/', status=302)
|
||||
assert resp.location == '/login/?next=/kb/'
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/kb/', status=200)
|
||||
assert resp.html.find('h2').text == 'Knowledge Base'
|
||||
|
||||
|
||||
def test_wcs_summary_view(app, mail_group, user):
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
|
||||
source_type = ContentType.objects.get_for_model(Mail).pk
|
||||
|
||||
resp = app.get('/ajax/summary/%s/%s/?callback=spam' % (source_type, mail.pk), status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/ajax/summary/%s/%s/?callback=spam' % (source_type, mail.pk), status=200)
|
||||
assert resp.content_type == 'application/javascript'
|
||||
assert 'bar' in resp.text
|
||||
assert resp.text.startswith('spam({')
|
||||
|
||||
|
||||
def test_remove_association_view(app, mail_group, user):
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
|
||||
source_type = ContentType.objects.get_for_model(Mail).pk
|
||||
association = Association.objects.create(
|
||||
source_type=ContentType.objects.get(id=source_type), source_pk=mail.pk
|
||||
)
|
||||
assert Association.objects.filter(id=association.pk).count() == 1
|
||||
|
||||
resp = app.get('/ajax/remove-association/%s' % association.pk, status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/ajax/remove-association/%s' % association.pk, status=302)
|
||||
assert resp.location == '/'
|
||||
assert Association.objects.filter(id=association.pk).count() == 0
|
||||
|
||||
|
||||
def test_create_formdata_view(settings, app, mail_group, user):
|
||||
settings.KNOWN_SERVICES = {
|
||||
'wcs': {
|
||||
'demarches': {
|
||||
'url': 'http://wcs.example.net/',
|
||||
}
|
||||
}
|
||||
}
|
||||
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
|
||||
source_type = ContentType.objects.get_for_model(Mail).pk
|
||||
association = Association.objects.create(
|
||||
source_type=ContentType.objects.get(id=source_type), source_pk=mail.pk
|
||||
)
|
||||
|
||||
resp = app.get('/ajax/create-formdata/%s' % association.pk, status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/ajax/create-formdata/%s' % association.pk, status=200)
|
||||
assert resp.content_type == 'application/json'
|
||||
assert resp.json == {'err': 1}
|
||||
|
||||
resp = app.post('/ajax/create-formdata/%s' % association.pk, status=200)
|
||||
assert resp.json == {'err': 1, 'msg': "'NoneType' object has no attribute 'split'"}
|
||||
|
||||
association.formdef_reference = 'demarches:bar'
|
||||
association.save()
|
||||
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/formdefs/bar/schema', method='GET')
|
||||
def response_get(url, request):
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
@httmock.urlmatch(netloc='wcs.example.net', path='/api/formdefs/bar/submit', method='POST')
|
||||
def response_post(url, request):
|
||||
headers = {'content-type': 'application/json'}
|
||||
content = {
|
||||
'err': 0,
|
||||
'data': {
|
||||
'id': 42,
|
||||
'backoffice_url': 'http://example.net',
|
||||
},
|
||||
}
|
||||
return httmock.response(200, content, headers)
|
||||
|
||||
with httmock.HTTMock(response_get, response_post):
|
||||
resp = app.post('/ajax/create-formdata/%s' % association.pk, status=200)
|
||||
assert resp.content_type == 'application/json'
|
||||
assert resp.json == {
|
||||
'result': 'ok',
|
||||
'url': 'http://wcs.example.net/backoffice/management/bar/42/',
|
||||
}
|
||||
|
||||
|
||||
def test_menu_json_view(app, user, mail_group, phone_group, counter_group, kb_group):
|
||||
resp = app.get('/menu.json', status=302)
|
||||
assert resp.location.startswith('/login/?next=')
|
||||
|
||||
app.set_user(user.username)
|
||||
resp = app.get('/menu.json', status=200)
|
||||
assert resp.content_type == 'application/json'
|
||||
assert sorted(x['label'] for x in resp.json) == ['Call Center', 'Counter', 'Knowledge Base', 'Mails']
|
||||
|
||||
resp = app.get('/menu.json?callback=foo', status=200)
|
||||
assert resp.content_type == 'application/javascript'
|
||||
assert resp.text.startswith('foo([{')
|
|
@ -14,36 +14,34 @@
|
|||
# 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 mock
|
||||
import pytest
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.test import override_settings
|
||||
|
||||
from welco.forms import QualificationForm
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
KNOWN_SERVICES = {
|
||||
'wcs': {
|
||||
'eservices': {
|
||||
'url': 'http://localhost/',
|
||||
'title': 'Eservices',
|
||||
'orig': 'welco'
|
||||
}
|
||||
}
|
||||
}
|
||||
KNOWN_SERVICES = {'wcs': {'eservices': {'url': 'http://localhost/', 'title': 'Eservices', 'orig': 'welco'}}}
|
||||
|
||||
|
||||
@mock.patch('welco.utils.requests.get')
|
||||
def test_get_qualification(mocked_get, client):
|
||||
with override_settings(KNOWN_SERVICES=KNOWN_SERVICES):
|
||||
forms = mock.Mock()
|
||||
forms.json.return_value = {'data': [{'category': 'Test',
|
||||
'authentication_required': False,
|
||||
'description': '',
|
||||
'title': 'Test form',
|
||||
'slug': 'test-form'}],
|
||||
'err': 0}
|
||||
forms.json.return_value = {
|
||||
'data': [
|
||||
{
|
||||
'category': 'Test',
|
||||
'authentication_required': False,
|
||||
'description': '',
|
||||
'title': 'Test form',
|
||||
'slug': 'test-form',
|
||||
}
|
||||
],
|
||||
'err': 0,
|
||||
}
|
||||
mocked_get.return_value = forms
|
||||
|
||||
user = mock.Mock()
|
||||
|
|
|
@ -17,13 +17,12 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from httmock import urlmatch, HTTMock
|
||||
from django.utils.encoding import force_str
|
||||
from httmock import HTTMock, urlmatch
|
||||
|
||||
|
||||
class BaseMock(object):
|
||||
class BaseMock:
|
||||
def __init__(self, netloc):
|
||||
self.netloc = netloc
|
||||
self.clear()
|
||||
|
@ -50,32 +49,38 @@ class BaseMock(object):
|
|||
|
||||
class MaarchMock(BaseMock):
|
||||
def list_endpoint(self, url, request):
|
||||
self.requests.append(('list_endpoint', url, request, json.loads(request.body)))
|
||||
self.requests.append(('list_endpoint', url, request, json.loads(force_str(request.body))))
|
||||
return {
|
||||
'content': json.dumps(self.next_response()),
|
||||
'headers': {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
'status_code': 200,
|
||||
}
|
||||
|
||||
list_endpoint.path = '^/rest/res/list$'
|
||||
|
||||
def update_external_infos(self, url, request):
|
||||
self.requests.append(('update_external_infos', url, request, json.loads(request.body)))
|
||||
self.requests.append(('update_external_infos', url, request, json.loads(force_str(request.body))))
|
||||
return json.dumps({})
|
||||
|
||||
update_external_infos.path = '^/rest/res/externalInfos$'
|
||||
|
||||
def update_status(self, url, request):
|
||||
self.requests.append(('update_status', url, request, json.loads(request.body)))
|
||||
self.requests.append(('update_status', url, request, json.loads(force_str(request.body))))
|
||||
return {
|
||||
'content': json.dumps(self.next_response()),
|
||||
'headers': {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
'status_code': 200,
|
||||
}
|
||||
|
||||
update_status.path = '^/rest/res/resource/status$'
|
||||
|
||||
def post_courrier(self, url, request):
|
||||
self.requests.append(('post_courrier', url, request, json.loads(request.body)))
|
||||
self.requests.append(('post_courrier', url, request, json.loads(force_str(request.body))))
|
||||
|
||||
post_courrier.path = '^/rest/res$'
|
||||
|
||||
|
||||
|
@ -93,37 +98,50 @@ def maarch(settings, mail_group):
|
|||
|
||||
class WcsMock(BaseMock):
|
||||
def api_formdefs(self, url, request):
|
||||
return json.dumps({
|
||||
'data': [{
|
||||
'slug': 'slug1',
|
||||
'title': 'title1',
|
||||
}]
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
'data': [
|
||||
{
|
||||
'slug': 'slug1',
|
||||
'title': 'title1',
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
api_formdefs.path = '^/api/formdefs/$'
|
||||
|
||||
def json(self, url, request):
|
||||
return json.dumps({
|
||||
'data': [{
|
||||
'slug': 'slug1',
|
||||
'title': 'title1',
|
||||
'category': 'category1',
|
||||
}]
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
'data': [
|
||||
{
|
||||
'slug': 'slug1',
|
||||
'title': 'title1',
|
||||
'category': 'category1',
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
json.path = '^/json$'
|
||||
|
||||
def api_formdefs_slug1_schema(self, url, request):
|
||||
return json.dumps({
|
||||
})
|
||||
return json.dumps({})
|
||||
|
||||
api_formdefs_slug1_schema.path = '^/api/formdefs/slug-1/schema$'
|
||||
|
||||
def api_formdefs_slug1_submit(self, url, request):
|
||||
return json.dumps({
|
||||
'err': 0,
|
||||
'data': {
|
||||
'id': 1,
|
||||
'backoffice_url': 'http://wcs.example.net/slug-1/1',
|
||||
},
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
'err': 0,
|
||||
'data': {
|
||||
'id': 1,
|
||||
'backoffice_url': 'http://wcs.example.net/slug-1/1',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
api_formdefs_slug1_submit.path = '^/api/formdefs/slug-1/submit$'
|
||||
|
||||
|
||||
|
@ -152,13 +170,15 @@ def test_utils(maarch):
|
|||
assert welco_maarch_obj.grc_refused_status == 'GRCREFUSED'
|
||||
|
||||
|
||||
PDF_MOCK = '%PDF-1.4 ...'
|
||||
PDF_MOCK = b'%PDF-1.4 ...'
|
||||
|
||||
|
||||
def test_feed(settings, app, maarch, wcs, user):
|
||||
import base64
|
||||
from django.core.management import call_command
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management import call_command
|
||||
|
||||
from welco.sources.mail.models import Mail
|
||||
|
||||
app.set_user(user.username)
|
||||
|
@ -169,14 +189,16 @@ def test_feed(settings, app, maarch, wcs, user):
|
|||
# feed mails from maarch
|
||||
with maarch.ctx_manager:
|
||||
# list request
|
||||
maarch.responses.append({
|
||||
'resources': [
|
||||
{
|
||||
'res_id': 1,
|
||||
'fileBase64Content': base64.b64encode(PDF_MOCK),
|
||||
}
|
||||
],
|
||||
})
|
||||
maarch.responses.append(
|
||||
{
|
||||
'resources': [
|
||||
{
|
||||
'res_id': 1,
|
||||
'fileBase64Content': force_str(base64.b64encode(PDF_MOCK)),
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
# update status request
|
||||
maarch.responses.append({})
|
||||
# last list request
|
||||
|
@ -211,20 +233,26 @@ def test_feed(settings, app, maarch, wcs, user):
|
|||
maarch.clear()
|
||||
pk = Mail.objects.get().pk
|
||||
with wcs.ctx_manager, maarch.ctx_manager:
|
||||
source_type = str(ContentType.objects.get_for_model(Mail).pk),
|
||||
source_type = (str(ContentType.objects.get_for_model(Mail).pk),)
|
||||
source_pk = str(pk)
|
||||
|
||||
response = app.get('/ajax/qualification', params={
|
||||
'source_type': source_type,
|
||||
'source_pk': source_pk,
|
||||
})
|
||||
response = app.get(
|
||||
'/ajax/qualification',
|
||||
params={
|
||||
'source_type': source_type,
|
||||
'source_pk': source_pk,
|
||||
},
|
||||
)
|
||||
|
||||
assert len(response.pyquery('a[data-association-pk]')) == 0
|
||||
response = app.post('/ajax/qualification', params={
|
||||
'source_type': source_type,
|
||||
'source_pk': str(pk),
|
||||
'formdef_reference': 'demarches:slug-1',
|
||||
})
|
||||
response = app.post(
|
||||
'/ajax/qualification',
|
||||
params={
|
||||
'source_type': source_type,
|
||||
'source_pk': str(pk),
|
||||
'formdef_reference': 'demarches:slug-1',
|
||||
},
|
||||
)
|
||||
|
||||
# verify qualification was done
|
||||
assert len(response.pyquery('a[data-association-pk]')) == 1
|
||||
|
@ -240,7 +268,7 @@ def test_feed(settings, app, maarch, wcs, user):
|
|||
'external_link': 'http://wcs.example.net/slug-1/1',
|
||||
'res_id': 1,
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
|
||||
# verify we can answer
|
||||
|
@ -250,31 +278,31 @@ def test_feed(settings, app, maarch, wcs, user):
|
|||
user.set_password('test')
|
||||
user.save()
|
||||
# verify authentication error
|
||||
response = app.post_json('/api/mail/response/', params={}, status=403)
|
||||
response = app.post_json('/api/mail/response/', params={}, status=(401, 403))
|
||||
app.authorization = ('Basic', ('test', 'test'))
|
||||
# verify serializer error
|
||||
response = app.post_json('/api/mail/response/', params={}, status=400)
|
||||
assert response.json['err'] == 1
|
||||
# verify error when maarch feed is not configured
|
||||
settings.MAARCH_FEED['ENABLE'] = False
|
||||
response = app.post_json('/api/mail/response/',
|
||||
params={'mail_id': 'maarch-1', 'content': 'coucou'},
|
||||
status=200)
|
||||
response = app.post_json(
|
||||
'/api/mail/response/', params={'mail_id': 'maarch-1', 'content': 'coucou'}, status=200
|
||||
)
|
||||
assert response.json['err'] == 1
|
||||
assert response.json['err_desc'] == 'maarch is unconfigured'
|
||||
settings.MAARCH_FEED['ENABLE'] = True
|
||||
# verify error when mail_id is unknown
|
||||
response = app.post_json('/api/mail/response/',
|
||||
params={'mail_id': 'maarch-231', 'content': 'coucou'},
|
||||
status=404)
|
||||
response = app.post_json(
|
||||
'/api/mail/response/', params={'mail_id': 'maarch-231', 'content': 'coucou'}, status=404
|
||||
)
|
||||
assert response.json['err'] == 1
|
||||
|
||||
# successfull call
|
||||
maarch.responses.append({})
|
||||
with maarch.ctx_manager:
|
||||
response = app.post_json('/api/mail/response/',
|
||||
params={'mail_id': 'maarch-1', 'content': 'coucou'},
|
||||
status=200)
|
||||
response = app.post_json(
|
||||
'/api/mail/response/', params={'mail_id': 'maarch-1', 'content': 'coucou'}, status=200
|
||||
)
|
||||
assert maarch.requests[0][3] == {
|
||||
'historyMessage': 'coucou',
|
||||
'resId': [1],
|
||||
|
|
|
@ -18,9 +18,9 @@ import json
|
|||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.timezone import now, timedelta
|
||||
|
||||
from welco.sources.phone import models
|
||||
|
@ -36,95 +36,104 @@ def test_call_start_stop(client):
|
|||
'callee': '102',
|
||||
'data': {
|
||||
'user': 'boby.lapointe',
|
||||
}
|
||||
},
|
||||
}
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
assert json.loads(response.content) == {'err': 0}
|
||||
assert response.json() == {'err': 0}
|
||||
assert models.PhoneCall.objects.count() == 1
|
||||
assert models.PhoneCall.objects.filter(
|
||||
caller='0033699999999',
|
||||
callee='102',
|
||||
data=json.dumps(payload['data']), stop__isnull=True).count() == 1
|
||||
assert (
|
||||
models.PhoneCall.objects.filter(
|
||||
caller='0033699999999', callee='102', data=json.dumps(payload['data']), stop__isnull=True
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
# new start event
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
assert json.loads(response.content) == {'err': 0}
|
||||
assert response.json() == {'err': 0}
|
||||
assert models.PhoneCall.objects.count() == 2
|
||||
assert models.PhoneCall.objects.filter(
|
||||
caller='0033699999999',
|
||||
callee='102',
|
||||
data=json.dumps(payload['data']), stop__isnull=True).count() == 1
|
||||
assert (
|
||||
models.PhoneCall.objects.filter(
|
||||
caller='0033699999999', callee='102', data=json.dumps(payload['data']), stop__isnull=True
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
# first call has been closed
|
||||
assert models.PhoneCall.objects.filter(
|
||||
caller='0033699999999',
|
||||
callee='102',
|
||||
data=json.dumps(payload['data']), stop__isnull=False).count() == 1
|
||||
assert (
|
||||
models.PhoneCall.objects.filter(
|
||||
caller='0033699999999', callee='102', data=json.dumps(payload['data']), stop__isnull=False
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
payload['event'] = 'stop'
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
assert json.loads(response.content) == {'err': 0}
|
||||
assert response.json() == {'err': 0}
|
||||
assert models.PhoneCall.objects.count() == 2
|
||||
assert models.PhoneCall.objects.filter(
|
||||
caller='0033699999999',
|
||||
callee='102',
|
||||
data=json.dumps(payload['data']), stop__isnull=False).count() == 2
|
||||
assert (
|
||||
models.PhoneCall.objects.filter(
|
||||
caller='0033699999999', callee='102', data=json.dumps(payload['data']), stop__isnull=False
|
||||
).count()
|
||||
== 2
|
||||
)
|
||||
# stop is idempotent
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
assert json.loads(response.content) == {'err': 0}
|
||||
assert response.json() == {'err': 0}
|
||||
assert models.PhoneCall.objects.count() == 2
|
||||
assert models.PhoneCall.objects.filter(
|
||||
caller='0033699999999',
|
||||
callee='102',
|
||||
data=json.dumps(payload['data']), stop__isnull=False).count() == 2
|
||||
assert (
|
||||
models.PhoneCall.objects.filter(
|
||||
caller='0033699999999', callee='102', data=json.dumps(payload['data']), stop__isnull=False
|
||||
).count()
|
||||
== 2
|
||||
)
|
||||
|
||||
|
||||
def test_one_call_per_callee(user, client):
|
||||
assert models.PhoneCall.objects.count() == 0
|
||||
payload = {'event': 'start', 'caller': '0033699999999', 'callee': '102'}
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert models.PhoneCall.objects.filter(callee='102', stop__isnull=True).count() == 1 # active
|
||||
assert models.PhoneCall.objects.filter(callee='102', stop__isnull=False).count() == 0 # inactive
|
||||
assert models.PhoneCall.objects.filter(callee='102', stop__isnull=False).count() == 0 # inactive
|
||||
|
||||
# new caller, same callee: stops the last call, start a new one
|
||||
payload['caller'] = '00337123456789'
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert models.PhoneCall.objects.count() == 2
|
||||
assert models.PhoneCall.objects.filter(
|
||||
caller='00337123456789', callee='102', stop__isnull=True).count() == 1
|
||||
assert models.PhoneCall.objects.filter(
|
||||
caller='0033699999999', callee='102', stop__isnull=False).count() == 1
|
||||
assert (
|
||||
models.PhoneCall.objects.filter(caller='00337123456789', callee='102', stop__isnull=True).count() == 1
|
||||
)
|
||||
assert (
|
||||
models.PhoneCall.objects.filter(caller='0033699999999', callee='102', stop__isnull=False).count() == 1
|
||||
)
|
||||
|
||||
with override_settings(PHONE_ONE_CALL_PER_CALLEE=False):
|
||||
# accept multiple call: start a new one, don't stop anything
|
||||
payload['caller'] = '00221774261500'
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(
|
||||
reverse('phone-call-event'), json.dumps(payload), content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert models.PhoneCall.objects.count() == 3
|
||||
assert models.PhoneCall.objects.filter(callee='102', stop__isnull=True).count() == 2
|
||||
assert models.PhoneCall.objects.filter(callee='102', stop__isnull=False).count() == 1
|
||||
# same caller: stop his last call, add a new one
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(
|
||||
reverse('phone-call-event'), json.dumps(payload), content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert models.PhoneCall.objects.count() == 4
|
||||
assert models.PhoneCall.objects.filter(callee='102', stop__isnull=True).count() == 2
|
||||
assert models.PhoneCall.objects.filter(callee='102', stop__isnull=False).count() == 2
|
||||
|
||||
|
||||
def test_current_calls(user, client):
|
||||
# create some calls
|
||||
for number in range(0, 10):
|
||||
|
@ -134,13 +143,14 @@ def test_current_calls(user, client):
|
|||
'callee': '1%02d' % number,
|
||||
'data': {
|
||||
'user': 'boby.lapointe',
|
||||
}
|
||||
},
|
||||
}
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(
|
||||
reverse('phone-call-event'), json.dumps(payload), content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
assert json.loads(response.content) == {'err': 0}
|
||||
assert response.json() == {'err': 0}
|
||||
|
||||
# register user to some lines
|
||||
# then remove from some
|
||||
|
@ -152,12 +162,12 @@ def test_current_calls(user, client):
|
|||
response = client.get(reverse('phone-current-calls'))
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
payload = json.loads(response.content)
|
||||
payload = response.json()
|
||||
assert isinstance(payload, dict)
|
||||
assert set(payload.keys()) == set(['err', 'data'])
|
||||
assert set(payload.keys()) == {'err', 'data'}
|
||||
assert payload['err'] == 0
|
||||
data = payload['data']
|
||||
assert set(data.keys()) == set(['calls', 'lines', 'all-lines'])
|
||||
assert set(data.keys()) == {'calls', 'lines', 'all-lines'}
|
||||
assert isinstance(data['calls'], list)
|
||||
assert isinstance(data['lines'], list)
|
||||
assert isinstance(data['all-lines'], list)
|
||||
|
@ -165,14 +175,14 @@ def test_current_calls(user, client):
|
|||
assert len(data['lines']) == 5
|
||||
assert len(data['all-lines']) == 10
|
||||
for call in data['calls']:
|
||||
assert set(call.keys()) <= set(['caller', 'callee', 'start', 'data'])
|
||||
assert isinstance(call['caller'], unicode)
|
||||
assert isinstance(call['callee'], unicode)
|
||||
assert isinstance(call['start'], unicode)
|
||||
assert set(call.keys()) <= {'caller', 'callee', 'start', 'data'}
|
||||
assert isinstance(call['caller'], str)
|
||||
assert isinstance(call['callee'], str)
|
||||
assert isinstance(call['start'], str)
|
||||
if 'data' in call:
|
||||
assert isinstance(call['data'], dict)
|
||||
assert len([call for call in data['lines'] if isinstance(call, unicode)]) == 5
|
||||
assert len([call for call in data['all-lines'] if isinstance(call, unicode)]) == 10
|
||||
assert len([call for call in data['lines'] if isinstance(call, str)]) == 5
|
||||
assert len([call for call in data['all-lines'] if isinstance(call, str)]) == 10
|
||||
|
||||
# unregister user to all remaining lines
|
||||
for number in range(0, 5):
|
||||
|
@ -180,11 +190,11 @@ def test_current_calls(user, client):
|
|||
response = client.get(reverse('phone-current-calls'))
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
payload = json.loads(response.content)
|
||||
payload = response.json()
|
||||
assert isinstance(payload, dict)
|
||||
assert set(payload.keys()) == set(['err', 'data'])
|
||||
assert set(payload.keys()) == {'err', 'data'}
|
||||
assert payload['err'] == 0
|
||||
assert set(payload['data'].keys()) == set(['calls', 'lines', 'all-lines'])
|
||||
assert set(payload['data'].keys()) == {'calls', 'lines', 'all-lines'}
|
||||
assert len(payload['data']['calls']) == 0
|
||||
assert len(payload['data']['lines']) == 0
|
||||
assert len(payload['data']['all-lines']) == 10
|
||||
|
@ -197,50 +207,49 @@ def test_take_release_line(user, client):
|
|||
payload = {
|
||||
'callee': '102',
|
||||
}
|
||||
response = client.post(reverse('phone-take-line'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-take-line'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
assert json.loads(response.content) == {'err': 0}
|
||||
assert response.json() == {'err': 0}
|
||||
assert models.PhoneLine.objects.count() == 1
|
||||
assert models.PhoneLine.objects.filter(
|
||||
users=user, callee='102').count() == 1
|
||||
response = client.post(reverse('phone-release-line'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
assert models.PhoneLine.objects.filter(users=user, callee='102').count() == 1
|
||||
response = client.post(
|
||||
reverse('phone-release-line'), json.dumps(payload), content_type='application/json'
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response['content-type'] == 'application/json'
|
||||
assert json.loads(response.content) == {'err': 0}
|
||||
assert response.json() == {'err': 0}
|
||||
assert models.PhoneLine.objects.count() == 1
|
||||
assert models.PhoneLine.objects.filter(
|
||||
users=user, callee='102').count() == 0
|
||||
assert models.PhoneLine.objects.filter(users=user, callee='102').count() == 0
|
||||
|
||||
|
||||
def test_phone_zone(user, client):
|
||||
client.login(username='toto', password='toto')
|
||||
response = client.get(reverse('phone-zone'))
|
||||
assert response.status_code == 200
|
||||
assert 'You do not have a phoneline configured' in response.content
|
||||
assert 'You do not have a phoneline configured' in force_str(response.content)
|
||||
|
||||
models.PhoneLine.take(callee='102', user=user)
|
||||
|
||||
response = client.get(reverse('phone-zone'))
|
||||
assert response.status_code == 200
|
||||
assert 'You do not have a phoneline configured' not in response.content
|
||||
assert '<li>102' in response.content
|
||||
assert 'data-callee="102"' in response.content
|
||||
currents = re.search('<div id="source-mainarea" '
|
||||
'data-current-calls="/api/phone/current-calls/">'
|
||||
'(.*?)</div>', response.content, flags=re.DOTALL)
|
||||
assert 'You do not have a phoneline configured' not in force_str(response.content)
|
||||
assert '<li>102' in force_str(response.content)
|
||||
assert 'data-callee="102"' in force_str(response.content)
|
||||
currents = re.search(
|
||||
'<div id="source-mainarea" ' 'data-current-calls="/api/phone/current-calls/">' '(.*?)</div>',
|
||||
force_str(response.content),
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
assert currents.group(1).strip() == ''
|
||||
|
||||
# create a call
|
||||
payload = {'event': 'start', 'caller': '003369999999', 'callee': '102'}
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
response = client.get(reverse('phone-zone'))
|
||||
assert response.status_code == 200
|
||||
assert '<h1>Current Call: <strong>003369999999</strong></h1>' in response.content
|
||||
assert '<h1>Current Call: <strong>003369999999</strong></h1>' in force_str(response.content)
|
||||
|
||||
# simulate a mellon user
|
||||
session = client.session
|
||||
|
@ -248,27 +257,26 @@ def test_phone_zone(user, client):
|
|||
session.save()
|
||||
response = client.get(reverse('phone-zone'))
|
||||
assert response.status_code == 200
|
||||
assert 'agent007' not in response.content
|
||||
assert 'data-callee="agent007"' not in response.content
|
||||
assert '<li>102' in response.content
|
||||
assert 'data-callee="102"' in response.content
|
||||
assert 'agent007' not in force_str(response.content)
|
||||
assert 'data-callee="agent007"' not in force_str(response.content)
|
||||
assert '<li>102' in force_str(response.content)
|
||||
assert 'data-callee="102"' in force_str(response.content)
|
||||
|
||||
with override_settings(PHONE_AUTOTAKE_MELLON_USERNAME=True):
|
||||
response = client.get(reverse('phone-zone'))
|
||||
assert response.status_code == 200
|
||||
assert '<h1>Current Call: <strong>003369999999</strong></h1>' in response.content
|
||||
assert 'agent007' in response.content
|
||||
assert 'data-callee="agent007"' in response.content
|
||||
assert '<li>102' in response.content
|
||||
assert 'data-callee="102"' in response.content
|
||||
assert '<h1>Current Call: <strong>003369999999</strong></h1>' in force_str(response.content)
|
||||
assert 'agent007' in force_str(response.content)
|
||||
assert 'data-callee="agent007"' in force_str(response.content)
|
||||
assert '<li>102' in force_str(response.content)
|
||||
assert 'data-callee="102"' in force_str(response.content)
|
||||
|
||||
|
||||
def test_call_expiration(user, client):
|
||||
assert models.PhoneCall.objects.count() == 0
|
||||
# create a call
|
||||
payload = {'event': 'start', 'caller': '003369999999', 'callee': '102'}
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload),
|
||||
content_type='application/json')
|
||||
response = client.post(reverse('phone-call-event'), json.dumps(payload), content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
assert models.PhoneCall.objects.filter(stop__isnull=True).count() == 1
|
||||
|
||||
|
@ -277,28 +285,27 @@ def test_call_expiration(user, client):
|
|||
client.login(username='toto', password='toto')
|
||||
response = client.get(reverse('phone-current-calls'))
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.content)
|
||||
payload = response.json()
|
||||
assert payload['err'] == 0
|
||||
assert len(payload['data']['calls']) == 1
|
||||
|
||||
# start call 10 minutes ago
|
||||
models.PhoneCall.objects.filter(stop__isnull=True).update(
|
||||
start=now()-timedelta(minutes=10))
|
||||
models.PhoneCall.objects.filter(stop__isnull=True).update(start=now() - timedelta(minutes=10))
|
||||
|
||||
# get list of calls without expiration
|
||||
response = client.get(reverse('phone-current-calls'))
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.content)
|
||||
payload = response.json()
|
||||
assert payload['err'] == 0
|
||||
assert len(payload['data']['calls']) == 1 # still here
|
||||
assert len(payload['data']['calls']) == 1 # still here
|
||||
|
||||
# get list of calls with an expiration of 2 minutes (< 10 minutes)
|
||||
with override_settings(PHONE_MAX_CALL_DURATION=2):
|
||||
response = client.get(reverse('phone-current-calls'))
|
||||
assert response.status_code == 200
|
||||
payload = json.loads(response.content)
|
||||
payload = response.json()
|
||||
assert payload['err'] == 0
|
||||
assert len(payload['data']['calls']) == 0 # call is expired
|
||||
assert len(payload['data']['calls']) == 0 # call is expired
|
||||
|
||||
assert models.PhoneCall.objects.filter(stop__isnull=True).count() == 0 # active calls
|
||||
assert models.PhoneCall.objects.filter(stop__isnull=False).count() == 1 # stopped calls
|
||||
assert models.PhoneCall.objects.filter(stop__isnull=False).count() == 1 # stopped calls
|
||||
|
|
30
tox.ini
30
tox.ini
|
@ -1,6 +1,8 @@
|
|||
[tox]
|
||||
envlist = py27-django111
|
||||
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/welco/
|
||||
envlist =
|
||||
py3-django32-drf314
|
||||
py3-django32-black-coverage-pylint-drf312
|
||||
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/welco/{env:BRANCH_NAME:}
|
||||
|
||||
[testenv]
|
||||
usedevelop =
|
||||
|
@ -8,23 +10,29 @@ usedevelop =
|
|||
setenv =
|
||||
DJANGO_SETTINGS_MODULE=welco.settings
|
||||
WELCO_SETTINGS_FILE=tests/settings.py
|
||||
SETUPTOOLS_USE_DISTUTILS=stdlib
|
||||
fast: FAST=--nomigrations
|
||||
coverage: COVERAGE=--junitxml=junit-{envname}.xml --cov-report xml --cov-report html --cov=welco/
|
||||
deps =
|
||||
django111: django>=1.11,<1.12
|
||||
django32: django>=3.2,<3.3
|
||||
pytest-cov
|
||||
pytest-django
|
||||
pytest<4.1
|
||||
attrs<19.2
|
||||
pytest!=5.3.3
|
||||
WebTest
|
||||
mock
|
||||
mock<4
|
||||
httmock
|
||||
python-dateutil
|
||||
pylint<1.8
|
||||
pylint-django<0.8.1
|
||||
django-webtest>=1.9.6
|
||||
pylint<3
|
||||
astroid<3
|
||||
pylint-django
|
||||
django-webtest
|
||||
pyquery
|
||||
lxml
|
||||
git+https://git.entrouvert.org/debian/django-ckeditor.git
|
||||
black: pre-commit
|
||||
drf312: djangorestframework>=3.12,<3.13
|
||||
drf314: djangorestframework>=3.14,<3.15
|
||||
commands =
|
||||
django111: ./pylint.sh welco/
|
||||
django111: py.test {posargs: --junitxml=test_{envname}_results.xml --cov-report xml --cov-report html --cov=welco/ tests/}
|
||||
pylint: ./pylint.sh welco/
|
||||
py.test {env:COVERAGE:} {posargs:tests/}
|
||||
black: pre-commit run black --all-files --show-diff-on-failure
|
||||
|
|
|
@ -15,7 +15,8 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf.urls import include, url
|
||||
from django.urls import include, re_path
|
||||
|
||||
|
||||
def register_urls(urlpatterns):
|
||||
pre_urls = []
|
||||
|
@ -24,9 +25,9 @@ def register_urls(urlpatterns):
|
|||
if hasattr(app, 'get_before_urls'):
|
||||
urls = app.get_before_urls()
|
||||
if urls:
|
||||
pre_urls.append(url('^', include(urls)))
|
||||
pre_urls.append(re_path('^', include(urls)))
|
||||
if hasattr(app, 'get_after_urls'):
|
||||
urls = app.get_after_urls()
|
||||
if urls:
|
||||
post_urls.append(url('^', include(urls)))
|
||||
post_urls.append(re_path('^', include(urls)))
|
||||
return pre_urls + urlpatterns + post_urls
|
||||
|
|
|
@ -15,8 +15,8 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _, pgettext_lazy
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy
|
||||
|
||||
DEFAULT_TITLE_CHOICES = (
|
||||
('', ''),
|
||||
|
@ -24,14 +24,15 @@ DEFAULT_TITLE_CHOICES = (
|
|||
(pgettext_lazy('title', 'Mr'), pgettext_lazy('title', 'Mr')),
|
||||
)
|
||||
|
||||
|
||||
class ContactAddForm(forms.Form):
|
||||
title = forms.CharField(label=_('Title'),
|
||||
required=False,
|
||||
widget=forms.Select(choices=DEFAULT_TITLE_CHOICES))
|
||||
title = forms.CharField(
|
||||
label=_('Title'), required=False, widget=forms.Select(choices=DEFAULT_TITLE_CHOICES)
|
||||
)
|
||||
first_name = forms.CharField(label=_('First Name'), required=False)
|
||||
last_name = forms.CharField(label=_('Last Name'),
|
||||
required=True,
|
||||
widget=forms.TextInput(attrs={'required': 'required'}))
|
||||
last_name = forms.CharField(
|
||||
label=_('Last Name'), required=True, widget=forms.TextInput(attrs={'required': 'required'})
|
||||
)
|
||||
email = forms.CharField(label=_('Email'), required=False)
|
||||
address = forms.CharField(label=_('Address'), required=False)
|
||||
zipcode = forms.CharField(label=_('Zip Code'), required=False)
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
import json
|
||||
import logging
|
||||
import random
|
||||
import requests
|
||||
import time
|
||||
|
||||
import requests
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
@ -27,13 +27,14 @@ from django.core.exceptions import PermissionDenied
|
|||
from django.http import HttpResponse
|
||||
from django.template import RequestContext
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import TemplateView, FormView
|
||||
from django.views.generic import FormView, TemplateView
|
||||
|
||||
from welco.utils import get_wcs_data, sign_url
|
||||
|
||||
from .forms import ContactAddForm
|
||||
|
||||
class HomeZone(object):
|
||||
|
||||
class HomeZone:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
|
@ -46,29 +47,28 @@ class ContactsZone(TemplateView):
|
|||
template_name = 'contacts/zone.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ContactsZone, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['source_pk'] = self.request.GET.get('source_pk')
|
||||
if 'source_pk' in self.request.GET:
|
||||
source_class = ContentType.objects.get(
|
||||
id=self.request.GET['source_type']).model_class()
|
||||
source_class = ContentType.objects.get(id=self.request.GET['source_type']).model_class()
|
||||
source_object = source_class.objects.get(id=self.request.GET['source_pk'])
|
||||
context['contact_user_id'] = source_object.contact_id
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if 'user_id' in request.POST:
|
||||
source_class = ContentType.objects.get(
|
||||
id=self.request.POST['source_type']).model_class()
|
||||
source_class = ContentType.objects.get(id=self.request.POST['source_type']).model_class()
|
||||
source_object = source_class.objects.get(id=self.request.POST['source_pk'])
|
||||
source_object.contact_id = request.POST['user_id']
|
||||
source_object.save()
|
||||
return HttpResponse('ok')
|
||||
|
||||
|
||||
zone = csrf_exempt(ContactsZone.as_view())
|
||||
|
||||
|
||||
def search_json(request):
|
||||
user_groups = set([x.name for x in request.user.groups.all()])
|
||||
user_groups = {x.name for x in request.user.groups.all()}
|
||||
for channel in settings.CHANNEL_ROLES:
|
||||
channel_groups = set(settings.CHANNEL_ROLES[channel])
|
||||
if user_groups.intersection(channel_groups):
|
||||
|
@ -83,8 +83,12 @@ def search_json(request):
|
|||
raise Exception('error %r' % result)
|
||||
for user in result.get('data'):
|
||||
user['title'] = user['user_display_name']
|
||||
more = [user.get('user_var_address'), user.get('user_var_phone'),
|
||||
user.get('user_var_mobile'), user.get('user_var_email')]
|
||||
more = [
|
||||
user.get('user_var_address'),
|
||||
user.get('user_var_phone'),
|
||||
user.get('user_var_mobile'),
|
||||
user.get('user_var_email'),
|
||||
]
|
||||
user['more'] = ' / '.join([x for x in more if x])
|
||||
if user.get('user_roles'):
|
||||
user['roles'] = ' / '.join([r['text'] for r in user['user_roles']])
|
||||
|
@ -100,7 +104,7 @@ class ContactDetailFragmentView(TemplateView):
|
|||
template_name = 'contacts/contact_detail_fragment.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ContactDetailFragmentView, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
user_id = self.kwargs.get('slug').split('-')[-1]
|
||||
user_details = get_wcs_data('api/users/%s/' % user_id)
|
||||
|
@ -109,13 +113,13 @@ class ContactDetailFragmentView(TemplateView):
|
|||
context['user_id'] = user_id
|
||||
|
||||
if 'source_pk' in self.request.GET:
|
||||
source_class = ContentType.objects.get(
|
||||
id=self.request.GET['source_type']).model_class()
|
||||
source_class = ContentType.objects.get(id=self.request.GET['source_type']).model_class()
|
||||
source_object = source_class.objects.get(id=self.request.GET['source_pk'])
|
||||
context['is_pinned_user'] = bool(source_object.contact_id == user_id)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
contact_detail_fragment = ContactDetailFragmentView.as_view()
|
||||
|
||||
|
||||
|
@ -133,7 +137,7 @@ class ContactAdd(FormView):
|
|||
msg['password'] = str(random.SystemRandom().random())
|
||||
msg['send_registration_email'] = getattr(settings, 'CONTACT_SEND_REGISTRATION_EMAIL', True)
|
||||
|
||||
authentic_site = settings.KNOWN_SERVICES.get('authentic').values()[0]
|
||||
authentic_site = list(settings.KNOWN_SERVICES.get('authentic').values())[0]
|
||||
authentic_url = authentic_site.get('url')
|
||||
authentic_orig = authentic_site.get('orig')
|
||||
authentic_secret = authentic_site.get('secret')
|
||||
|
@ -144,9 +148,8 @@ class ContactAdd(FormView):
|
|||
logger = logging.getLogger(__name__)
|
||||
logger.info('POST to authentic (%r)', json.dumps(msg))
|
||||
authentic_response = requests.post(
|
||||
signed_url,
|
||||
data=json.dumps(msg),
|
||||
headers={'Content-type': 'application/json'})
|
||||
signed_url, data=json.dumps(msg), headers={'Content-type': 'application/json'}
|
||||
)
|
||||
logger.info('Got authentic response (%r)', authentic_response.text)
|
||||
|
||||
user_uuid = authentic_response.json().get('uuid')
|
||||
|
@ -165,4 +168,5 @@ class ContactAdd(FormView):
|
|||
json.dump(result, response, indent=2)
|
||||
return response
|
||||
|
||||
|
||||
contact_add = csrf_exempt(ContactAdd.as_view())
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .utils import get_wcs_options
|
||||
|
||||
|
@ -24,7 +24,7 @@ class QualificationForm(forms.Form):
|
|||
formdef_reference = forms.CharField(label=_('Associated Form'))
|
||||
|
||||
def __init__(self, user, *args, **kwargs):
|
||||
super(QualificationForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
params = {'backoffice-submission': 'on'}
|
||||
if hasattr(user, 'saml_identifiers') and user.saml_identifiers.exists():
|
||||
params['NameID'] = user.saml_identifiers.first().name_id
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KbAppConfig(AppConfig):
|
||||
name = 'welco.kb'
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ from django.utils.text import slugify
|
|||
|
||||
from .models import Page
|
||||
|
||||
|
||||
class PageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Page
|
||||
|
@ -37,4 +38,4 @@ class PageForm(forms.ModelForm):
|
|||
i += 1
|
||||
slug = '%s-%s' % (base_slug, i)
|
||||
self.instance.slug = slug
|
||||
return super(PageForm, self).save(commit=commit)
|
||||
return super().save(commit=commit)
|
||||
|
|
|
@ -1,20 +1,19 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import ckeditor.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Page',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('title', models.CharField(max_length=200, verbose_name='Title')),
|
||||
('slug', models.SlugField(verbose_name='Slug')),
|
||||
('content', ckeditor.fields.RichTextField(verbose_name='Text')),
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import taggit.managers
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -16,7 +13,13 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='page',
|
||||
name='tags',
|
||||
field=taggit.managers.TaggableManager(to='taggit.Tag', through='taggit.TaggedItem', blank=True, help_text='A comma-separated list of tags.', verbose_name='Keywords'),
|
||||
field=taggit.managers.TaggableManager(
|
||||
to='taggit.Tag',
|
||||
through='taggit.TaggedItem',
|
||||
blank=True,
|
||||
help_text='A comma-separated list of tags.',
|
||||
verbose_name='Keywords',
|
||||
),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -14,22 +14,18 @@
|
|||
# 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.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ckeditor.fields import RichTextField
|
||||
import reversion
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
|
||||
@reversion.register
|
||||
class Page(models.Model):
|
||||
title = models.CharField(_('Title'), max_length=200)
|
||||
slug = models.SlugField(_('Slug'))
|
||||
content = RichTextField(_('Text'))
|
||||
tags = TaggableManager(_('Keywords'), blank=True,
|
||||
help_text=_('A comma-separated list of tags.'))
|
||||
tags = TaggableManager(_('Keywords'), blank=True, help_text=_('A comma-separated list of tags.'))
|
||||
|
||||
class Meta:
|
||||
ordering = ['title']
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
# 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 HTMLParser import HTMLParser
|
||||
import html
|
||||
|
||||
from django.utils.html import strip_tags
|
||||
|
||||
from haystack import indexes
|
||||
|
||||
from .models import Page
|
||||
|
||||
|
||||
class PageIndex(indexes.SearchIndex, indexes.Indexable):
|
||||
title = indexes.CharField(model_attr='title', boost=3)
|
||||
text = indexes.CharField(document=True)
|
||||
|
@ -33,7 +33,7 @@ class PageIndex(indexes.SearchIndex, indexes.Indexable):
|
|||
return Page
|
||||
|
||||
def prepare_text(self, obj):
|
||||
return obj.title + ' ' + self.prepare_tags(obj) + ' ' + HTMLParser().unescape(strip_tags(obj.content))
|
||||
return obj.title + ' ' + self.prepare_tags(obj) + ' ' + html.unescape(strip_tags(obj.content))
|
||||
|
||||
def prepare_text_auto(self, obj):
|
||||
return self.prepare_text(obj)
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
<h2>{% trans 'Knowledge Base' %} - {{ object.title }}</h2>
|
||||
{% if can_manage %}
|
||||
<a rel="popup" href="{% url 'kb-page-delete' slug=object.slug %}">{% trans 'Delete' %}</a>
|
||||
<a href="{% url 'kb-page-history' slug=object.slug %}">{% trans 'History' %}</a>
|
||||
<a href="{% url 'kb-page-edit' slug=object.slug %}">{% trans 'Edit' %}</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
{% extends "kb/page_detail.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Knowledge Base' %} - {{ object.title }}</h2>
|
||||
<a href="{% url 'kb-page-view' slug=object.slug %}">{% trans 'Back to page' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block breadcrumb %}
|
||||
{{ block.super }}
|
||||
<a href=".">{% trans 'History' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div id="page-history">
|
||||
<ul>
|
||||
{% for version in versions_list %}
|
||||
<li>{{ version.revision.date_created }}, <a href="{% url 'kb-page-version' slug=object.slug version=version.id %}">{% trans 'view' %}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,26 +0,0 @@
|
|||
{% extends "kb/page_detail.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block appbar %}
|
||||
<h2>{% trans 'Knowledge Base' %} - {{ object.title }}</h2>
|
||||
<a href="{% url 'kb-page-view' slug=object.slug %}">{% trans 'Back to page' %}</a>
|
||||
<a href="{% url 'kb-page-history' slug=object.slug %}">{% trans 'History' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="old-version warning-notice">
|
||||
<p>
|
||||
{% trans 'Warning: this is an old version of this page.' %}
|
||||
</p>
|
||||
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<button>{% trans 'Revert to this version' %}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{block.super}}
|
||||
|
||||
{% endblock %}
|
||||
|
|
@ -20,32 +20,31 @@ from django import template
|
|||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required, user_passes_test
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.db.models import Count
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.template import RequestContext
|
||||
from django.urls import reverse_lazy
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import (DetailView, CreateView, UpdateView,
|
||||
ListView, DeleteView, TemplateView)
|
||||
|
||||
from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView
|
||||
from haystack.forms import SearchForm
|
||||
from haystack.generic_views import SearchView
|
||||
from haystack.query import SearchQuerySet
|
||||
from reversion.models import Version
|
||||
from taggit.models import Tag
|
||||
|
||||
from .models import Page
|
||||
from .forms import PageForm
|
||||
from .models import Page
|
||||
|
||||
|
||||
def check_user_perms(user, access=False):
|
||||
allowed_roles = settings.KB_MANAGE_ROLES[:]
|
||||
if access:
|
||||
allowed_roles.extend(settings.KB_ACCESS_ROLES)
|
||||
if settings.KB_ROLE:
|
||||
allowed_roles.append(settings.KB_ROLE) # legacy
|
||||
user_groups = set([x.name for x in user.groups.all()])
|
||||
allowed_roles.append(settings.KB_ROLE) # legacy
|
||||
user_groups = {x.name for x in user.groups.all()}
|
||||
return user_groups.intersection(allowed_roles)
|
||||
|
||||
|
||||
def check_request_perms(request, access=False):
|
||||
if not check_user_perms(request.user, access=access):
|
||||
raise PermissionDenied()
|
||||
|
@ -56,14 +55,15 @@ class PageListView(ListView):
|
|||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_request_perms(request, access=True)
|
||||
return super(PageListView, self).dispatch(request, *args, **kwargs)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(PageListView, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['form'] = SearchForm()
|
||||
context['can_manage'] = check_user_perms(self.request.user)
|
||||
return context
|
||||
|
||||
|
||||
page_list = login_required(PageListView.as_view())
|
||||
|
||||
|
||||
|
@ -73,7 +73,8 @@ class PageAddView(CreateView):
|
|||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_request_perms(request)
|
||||
return super(PageAddView, self).dispatch(request, *args, **kwargs)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
page_add = login_required(PageAddView.as_view())
|
||||
|
||||
|
@ -84,7 +85,8 @@ class PageEditView(UpdateView):
|
|||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_request_perms(request)
|
||||
return super(PageEditView, self).dispatch(request, *args, **kwargs)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
page_edit = login_required(PageEditView.as_view())
|
||||
|
||||
|
@ -94,10 +96,10 @@ class PageDetailView(DetailView):
|
|||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_request_perms(request, access=True)
|
||||
return super(PageDetailView, self).dispatch(request, *args, **kwargs)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(PageDetailView, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['can_manage'] = check_user_perms(self.request.user)
|
||||
return context
|
||||
|
||||
|
@ -109,6 +111,7 @@ class PageDetailFragmentView(DetailView):
|
|||
model = Page
|
||||
template_name = 'kb/page_detail_fragment.html'
|
||||
|
||||
|
||||
page_detail_fragment = PageDetailFragmentView.as_view()
|
||||
|
||||
|
||||
|
@ -118,7 +121,8 @@ class PageDeleteView(DeleteView):
|
|||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_request_perms(request)
|
||||
return super(PageDeleteView, self).dispatch(request, *args, **kwargs)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
page_delete = login_required(PageDeleteView.as_view())
|
||||
|
||||
|
@ -129,58 +133,22 @@ class PageSearchView(SearchView):
|
|||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_request_perms(request, access=True)
|
||||
return super(PageSearchView, self).dispatch(request, *args, **kwargs)
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
page_search = login_required(PageSearchView.as_view())
|
||||
|
||||
|
||||
class PageHistoryView(DetailView):
|
||||
model = Page
|
||||
template_name = 'kb/page_history.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_request_perms(request)
|
||||
return super(PageHistoryView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(PageHistoryView, self).get_context_data(**kwargs)
|
||||
context['versions_list'] = Version.objects.get_for_object(self.get_object())
|
||||
return context
|
||||
|
||||
page_history = login_required(PageHistoryView.as_view())
|
||||
|
||||
|
||||
class PageVersionView(DetailView):
|
||||
model = Page
|
||||
template_name = 'kb/page_version.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_request_perms(request)
|
||||
return super(PageVersionView, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(PageVersionView, self).get_context_data(**kwargs)
|
||||
context['object'] = Version.objects.get(id=self.kwargs.get('version')).object
|
||||
self.kwargs.get('version')
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
version = Version.objects.get(id=self.kwargs.get('version'))
|
||||
version.revision.revert()
|
||||
return HttpResponseRedirect(self.get_object().get_absolute_url())
|
||||
|
||||
page_version = login_required(PageVersionView.as_view())
|
||||
|
||||
|
||||
class KbZone(TemplateView):
|
||||
template_name = 'kb/zone.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(KbZone, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['source_pk'] = self.request.GET.get('source_pk')
|
||||
context['form'] = SearchForm()
|
||||
context['tags'] = Tag.objects.all().annotate(
|
||||
num_times=Count('taggit_taggeditem_items')).filter(num_times__gt=0)
|
||||
context['tags'] = (
|
||||
Tag.objects.all().annotate(num_times=Count('taggit_taggeditem_items')).filter(num_times__gt=0)
|
||||
)
|
||||
num_times = context['tags'].values_list('num_times', flat=True)
|
||||
if not num_times:
|
||||
num_times = [0]
|
||||
|
@ -202,6 +170,7 @@ class KbZone(TemplateView):
|
|||
tag.font_size = 'x-large'
|
||||
return context
|
||||
|
||||
|
||||
zone = csrf_exempt(KbZone.as_view())
|
||||
|
||||
|
||||
|
@ -220,7 +189,7 @@ def page_search_json(request):
|
|||
return response
|
||||
|
||||
|
||||
class HomeZone(object):
|
||||
class HomeZone:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ msgstr ""
|
|||
"Project-Id-Version: welco 0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2018-08-30 10:57+0200\n"
|
||||
"PO-Revision-Date: 2017-01-13 16:08+0100\n"
|
||||
"PO-Revision-Date: 2020-04-14 09:29+0200\n"
|
||||
"Last-Translator: Frederic Peters <fpeters@entrouvert.com>\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -107,7 +107,7 @@ msgstr "Nom, téléphone, etc."
|
|||
|
||||
#: contacts/templates/contacts/zone.html:18
|
||||
msgid "Back to search to search for another user"
|
||||
msgstr "Retourner à la recherche d'un autre usager"
|
||||
msgstr "Retourner à la recherche d’un autre usager"
|
||||
|
||||
#: contrib/alfortville/models.py:40
|
||||
#: contrib/alfortville/templates/alfortville/mail-table.html:44
|
||||
|
@ -155,7 +155,7 @@ msgstr "Cliquer pour (dé)sélectionner tous les courriers"
|
|||
#: contrib/alfortville/templates/alfortville/mail-table-waiting.html:39
|
||||
#: contrib/alfortville/templates/alfortville/mail-table.html:68
|
||||
msgid "There is currently no mail in this list."
|
||||
msgstr "Il n'y a pour le moment aucun courrier dans cette liste."
|
||||
msgstr "Il n’y a pour le moment aucun courrier dans cette liste."
|
||||
|
||||
#: contrib/alfortville/templates/alfortville/mail-table-waiting.html:11
|
||||
msgid "Pending Mails"
|
||||
|
@ -379,7 +379,7 @@ msgstr "Avis requis"
|
|||
#: sources/mail/templates/welco/mail_summary.html:46
|
||||
#: sources/mail/templates/welco/mail_summary.html:58
|
||||
msgid "Waiting for avis."
|
||||
msgstr "Pas d'avis rédigé."
|
||||
msgstr "Pas d’avis rédigé."
|
||||
|
||||
#: sources/mail/templates/welco/mail_summary.html:54
|
||||
msgid "Avis"
|
||||
|
@ -487,7 +487,7 @@ msgstr "Transmettre"
|
|||
|
||||
#: views.py:236
|
||||
msgid "Call Center"
|
||||
msgstr "Centre d'appels"
|
||||
msgstr "Centre d’appels"
|
||||
|
||||
#~ msgid "Log In"
|
||||
#~ msgstr "Connexion"
|
||||
|
|
|
@ -14,17 +14,17 @@
|
|||
# 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.core.urlresolvers import reverse
|
||||
import ckeditor.widgets
|
||||
from django.forms.utils import flatatt
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.encoding import force_text
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.html import conditional_escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language
|
||||
|
||||
import ckeditor.widgets
|
||||
|
||||
def ckeditor_render(self, name, value, attrs=None):
|
||||
def ckeditor_render(self, name, value, attrs=None, renderer=None):
|
||||
if value is None:
|
||||
value = ''
|
||||
final_attrs = {'name': name}
|
||||
|
@ -40,14 +40,22 @@ def ckeditor_render(self, name, value, attrs=None):
|
|||
self.config['language'] = get_language()
|
||||
|
||||
# Force to text to evaluate possible lazy objects
|
||||
external_plugin_resources = [[force_text(a), force_text(b), force_text(c)] for a, b, c in self.external_plugin_resources]
|
||||
external_plugin_resources = [
|
||||
[force_str(a), force_str(b), force_str(c)] for a, b, c in self.external_plugin_resources
|
||||
]
|
||||
|
||||
return mark_safe(
|
||||
render_to_string(
|
||||
'ckeditor/widget.html',
|
||||
{
|
||||
'final_attrs': flatatt(final_attrs),
|
||||
'value': conditional_escape(force_str(value)),
|
||||
'id': final_attrs['id'],
|
||||
'config': ckeditor.widgets.json_encode(self.config),
|
||||
'external_plugin_resources': ckeditor.widgets.json_encode(external_plugin_resources),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
return mark_safe(render_to_string('ckeditor/widget.html', {
|
||||
'final_attrs': flatatt(final_attrs),
|
||||
'value': conditional_escape(force_text(value)),
|
||||
'id': final_attrs['id'],
|
||||
'config': ckeditor.widgets.json_encode(self.config),
|
||||
'external_plugin_resources' : ckeditor.widgets.json_encode(external_plugin_resources)
|
||||
}))
|
||||
|
||||
ckeditor.widgets.CKEditorWidget.render = ckeditor_render
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -14,31 +11,37 @@ class Migration(migrations.Migration):
|
|||
migrations.CreateModel(
|
||||
name='Association',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('source_pk', models.PositiveIntegerField()),
|
||||
],
|
||||
options={
|
||||
},
|
||||
options={},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormdataReference',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('reference', models.CharField(max_length=250)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
options={},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormdefReference',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('reference', models.CharField(max_length=250)),
|
||||
],
|
||||
options={
|
||||
},
|
||||
options={},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
migrations.AddField(
|
||||
|
@ -56,7 +59,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='association',
|
||||
name='source_type',
|
||||
field=models.ForeignKey(to='contenttypes.ContentType'),
|
||||
field=models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE),
|
||||
preserve_default=True,
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
|
@ -14,17 +14,17 @@
|
|||
# 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.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from welco.utils import get_wcs_formdef_details, push_wcs_formdata, get_wcs_services
|
||||
from welco.utils import get_wcs_formdef_details, get_wcs_services, push_wcs_formdata
|
||||
|
||||
|
||||
class Association(models.Model):
|
||||
source_type = models.ForeignKey(ContentType)
|
||||
source_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
||||
source_pk = models.PositiveIntegerField()
|
||||
source = GenericForeignKey('source_type', 'source_pk')
|
||||
comments = models.TextField(blank=True, verbose_name=_('Comments'))
|
||||
|
@ -38,10 +38,12 @@ class Association(models.Model):
|
|||
if self.source.contact_id:
|
||||
context['user_id'] = self.source.contact_id
|
||||
context['summary_url'] = request.build_absolute_uri(
|
||||
reverse('wcs-summary', kwargs={'source_type': self.source_type_id,
|
||||
'source_pk': self.source_pk}))
|
||||
reverse('wcs-summary', kwargs={'source_type': self.source_type_id, 'source_pk': self.source_pk})
|
||||
)
|
||||
context.update(self.source.get_source_context(request))
|
||||
self.formdata_id, self.formdata_url_backoffice = push_wcs_formdata(request, self.formdef_reference, context)
|
||||
self.formdata_id, self.formdata_url_backoffice = push_wcs_formdata(
|
||||
request, self.formdef_reference, context
|
||||
)
|
||||
self.save()
|
||||
|
||||
@property
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Django settings for welco project.
|
||||
|
||||
|
@ -11,8 +9,9 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
|
|||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.conf import global_settings
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
@ -41,7 +40,6 @@ INSTALLED_APPS = (
|
|||
'django.contrib.staticfiles',
|
||||
'ckeditor',
|
||||
'haystack',
|
||||
'reversion',
|
||||
'taggit',
|
||||
'welco.sources.counter',
|
||||
'welco.sources.mail',
|
||||
|
@ -59,10 +57,8 @@ MIDDLEWARE = (
|
|||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'reversion.middleware.RevisionMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'welco.urls'
|
||||
|
@ -93,7 +89,7 @@ USE_L10N = True
|
|||
|
||||
USE_TZ = True
|
||||
|
||||
LOCALE_PATHS = (os.path.join(BASE_DIR, 'welco', 'locale'), )
|
||||
LOCALE_PATHS = (os.path.join(BASE_DIR, 'welco', 'locale'),)
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.7/howto/static-files/
|
||||
|
@ -102,9 +98,7 @@ STATIC_URL = '/static/'
|
|||
|
||||
STATICFILES_FINDERS = tuple(global_settings.STATICFILES_FINDERS) + ('gadjo.finders.XStaticFinder',)
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'welco', 'static'),
|
||||
)
|
||||
STATICFILES_DIRS = (os.path.join(BASE_DIR, 'welco', 'static'),)
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||
MEDIA_URL = '/media/'
|
||||
|
@ -134,12 +128,18 @@ CKEDITOR_UPLOAD_PATH = 'uploads/'
|
|||
|
||||
CKEDITOR_CONFIGS = {
|
||||
'default': {
|
||||
'toolbar_Own': [['Source', 'Format', '-', 'Bold', 'Italic'],
|
||||
['NumberedList', 'BulletedList'],
|
||||
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
|
||||
['Link', 'Unlink'],
|
||||
['Image',],
|
||||
['RemoveFormat',]],
|
||||
'toolbar_Own': [
|
||||
['Source', 'Format', '-', 'Bold', 'Italic'],
|
||||
['NumberedList', 'BulletedList'],
|
||||
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
|
||||
['Link', 'Unlink'],
|
||||
[
|
||||
'Image',
|
||||
],
|
||||
[
|
||||
'RemoveFormat',
|
||||
],
|
||||
],
|
||||
'toolbar': 'Own',
|
||||
},
|
||||
}
|
||||
|
@ -193,7 +193,7 @@ CHANNEL_ROLES = {
|
|||
}
|
||||
|
||||
# role allowed to manage knowledge base
|
||||
KB_ROLE = None # deprecated
|
||||
KB_ROLE = None # deprecated
|
||||
KB_MANAGE_ROLES = []
|
||||
|
||||
# roles allowed to visit knowledge base
|
||||
|
@ -203,9 +203,7 @@ KB_ACCESS_ROLES = []
|
|||
SCREEN_PANELS = ['contacts', 'qualif']
|
||||
|
||||
# useful links for counter
|
||||
COUNTER_LINKS = [
|
||||
{'label': 'Wikipedia', 'url': 'https://fr.wikipedia.org'}
|
||||
]
|
||||
COUNTER_LINKS = [{'label': 'Wikipedia', 'url': 'https://fr.wikipedia.org'}]
|
||||
|
||||
# phone system
|
||||
PHONE_ONE_CALL_PER_CALLEE = True
|
||||
|
@ -217,7 +215,8 @@ PHONE_AUTOTAKE_MELLON_USERNAME = False
|
|||
REST_FRAMEWORK = {}
|
||||
REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] = ['rest_framework.authentication.BasicAuthentication']
|
||||
|
||||
local_settings_file = os.environ.get('WELCO_SETTINGS_FILE',
|
||||
os.path.join(os.path.dirname(__file__), 'local_settings.py'))
|
||||
local_settings_file = os.environ.get(
|
||||
'WELCO_SETTINGS_FILE', os.path.join(os.path.dirname(__file__), 'local_settings.py')
|
||||
)
|
||||
if os.path.exists(local_settings_file):
|
||||
execfile(local_settings_file)
|
||||
exec(open(local_settings_file).read())
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CounterPresence',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('status', models.CharField(max_length=50, verbose_name='Status', blank=True)),
|
||||
('contact_id', models.CharField(max_length=50, null=True)),
|
||||
('creation_timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
|
|
|
@ -16,20 +16,19 @@
|
|||
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from welco.qualif.models import Association
|
||||
|
||||
class CounterPresence(models.Model):
|
||||
|
||||
class CounterPresence(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _('Counter Presence')
|
||||
|
||||
# common to all source types:
|
||||
status = models.CharField(_('Status'), blank=True, max_length=50)
|
||||
contact_id = models.CharField(max_length=50, null=True)
|
||||
associations = GenericRelation(Association,
|
||||
content_type_field='source_type', object_id_field='source_pk')
|
||||
associations = GenericRelation(Association, content_type_field='source_type', object_id_field='source_pk')
|
||||
|
||||
creation_timestamp = models.DateTimeField(auto_now_add=True)
|
||||
last_update_timestamp = models.DateTimeField(auto_now=True)
|
||||
|
|
|
@ -14,10 +14,10 @@
|
|||
# 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 django.urls import path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^ajax/counter/zone/$', views.zone, name='counter-zone'),
|
||||
path('ajax/counter/zone/', views.zone, name='counter-zone'),
|
||||
]
|
||||
|
|
|
@ -18,17 +18,17 @@ import json
|
|||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import HttpResponse
|
||||
from django.template import RequestContext
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponse
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from .models import CounterPresence
|
||||
|
||||
|
||||
class Home(object):
|
||||
class Home:
|
||||
source_key = 'counter'
|
||||
|
||||
def __init__(self, request, **kwargs):
|
||||
|
@ -46,7 +46,7 @@ class CounterZone(TemplateView):
|
|||
template_name = 'welco/counter_home.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CounterZone, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['source_type'] = ContentType.objects.get_for_model(CounterPresence)
|
||||
new_source = CounterPresence()
|
||||
new_source.save()
|
||||
|
@ -54,6 +54,7 @@ class CounterZone(TemplateView):
|
|||
context['useful_links'] = settings.COUNTER_LINKS
|
||||
return context
|
||||
|
||||
|
||||
zone = csrf_exempt(CounterZone.as_view())
|
||||
|
||||
|
||||
|
|
|
@ -22,14 +22,15 @@ class AppConfig(django.apps.AppConfig):
|
|||
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
|
||||
return urls.urlpatterns
|
||||
|
||||
def ready(self):
|
||||
from welco.qualif.models import Association
|
||||
from django.db.models import signals
|
||||
|
||||
signals.post_save.connect(self.association_post_save,
|
||||
sender=Association)
|
||||
from welco.qualif.models import Association
|
||||
|
||||
signals.post_save.connect(self.association_post_save, sender=Association)
|
||||
|
||||
def association_post_save(self, sender, instance, **kwargs):
|
||||
from .utils import get_maarch
|
||||
|
@ -47,6 +48,8 @@ class AppConfig(django.apps.AppConfig):
|
|||
maarch.set_grc_sent_status(
|
||||
mail_pk=maarch_pk,
|
||||
formdata_id=instance.formdata_id,
|
||||
formdata_url_backoffice=instance.formdata_url_backoffice)
|
||||
formdata_url_backoffice=instance.formdata_url_backoffice,
|
||||
)
|
||||
|
||||
|
||||
default_app_config = 'welco.sources.mail.AppConfig'
|
||||
|
|
|
@ -15,8 +15,9 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class MailQualificationForm(forms.Form):
|
||||
post_date = forms.DateTimeField(label=_('Post Date (*)'), required=False)
|
||||
|
|
|
@ -14,12 +14,11 @@
|
|||
# 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 urlparse
|
||||
import base64
|
||||
|
||||
from dateutil.parser import parse as parse_datetime
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
from dateutil.parser import parse as parse_datetime
|
||||
from requests.adapters import HTTPAdapter
|
||||
from requests.packages.urllib3.util.retry import Retry
|
||||
|
||||
|
@ -28,7 +27,7 @@ class MaarchError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class MaarchCourrier(object):
|
||||
class MaarchCourrier:
|
||||
url = None
|
||||
username = None
|
||||
password = None
|
||||
|
@ -43,7 +42,7 @@ class MaarchCourrier(object):
|
|||
def __repr__(self):
|
||||
return '<MaarchCourrier url:%s>' % self.url
|
||||
|
||||
class Courrier(object):
|
||||
class Courrier:
|
||||
content = None
|
||||
format = None
|
||||
status = None
|
||||
|
@ -87,8 +86,8 @@ class MaarchCourrier(object):
|
|||
excluded_keys = ['content', 'format', 'status', 'maarch_courrier', 'pk']
|
||||
data = {key: self.__dict__[key] for key in self.__dict__ if key not in excluded_keys}
|
||||
if data:
|
||||
for key, value in data.iteritems():
|
||||
if isinstance(value, basestring):
|
||||
for key, value in data.items():
|
||||
if isinstance(value, str):
|
||||
d.append({'column': key, 'value': value, 'type': 'string'})
|
||||
elif isinstance(value, int):
|
||||
d.append({'column': key, 'value': str(value), 'type': 'int'})
|
||||
|
@ -124,7 +123,7 @@ class MaarchCourrier(object):
|
|||
read=self.max_retries,
|
||||
connect=self.max_retries,
|
||||
backoff_factor=0.5,
|
||||
status_forcelist=(500, 502, 504)
|
||||
status_forcelist=(500, 502, 504),
|
||||
)
|
||||
adapter = HTTPAdapter(max_retries=retry)
|
||||
s.mount('http://', adapter)
|
||||
|
@ -152,19 +151,19 @@ class MaarchCourrier(object):
|
|||
|
||||
@property
|
||||
def list_url(self):
|
||||
return urlparse.urljoin(self.url, 'rest/res/list')
|
||||
return urllib.parse.urljoin(self.url, 'rest/res/list')
|
||||
|
||||
@property
|
||||
def update_external_infos_url(self):
|
||||
return urlparse.urljoin(self.url, 'rest/res/externalInfos')
|
||||
return urllib.parse.urljoin(self.url, 'rest/res/externalInfos')
|
||||
|
||||
@property
|
||||
def update_status_url(self):
|
||||
return urlparse.urljoin(self.url, 'rest/res/resource/status')
|
||||
return urllib.parse.urljoin(self.url, 'rest/res/resource/status')
|
||||
|
||||
@property
|
||||
def post_courrier_url(self):
|
||||
return urlparse.urljoin(self.url, 'rest/res')
|
||||
return urllib.parse.urljoin(self.url, 'rest/res')
|
||||
|
||||
def get_courriers(self, clause, fields=None, limit=None, include_file=False, order_by=None):
|
||||
if fields:
|
||||
|
@ -174,13 +173,16 @@ class MaarchCourrier(object):
|
|||
fields = ','.join(fields) if fields else '*'
|
||||
limit = limit or self.default_limit
|
||||
order_by = order_by or []
|
||||
response = self.post_json(self.list_url, {
|
||||
'select': fields,
|
||||
'clause': clause,
|
||||
'limit': limit,
|
||||
'withFile': include_file,
|
||||
'orderBy': order_by,
|
||||
})
|
||||
response = self.post_json(
|
||||
self.list_url,
|
||||
{
|
||||
'select': fields,
|
||||
'clause': clause,
|
||||
'limit': limit,
|
||||
'withFile': include_file,
|
||||
'orderBy': order_by,
|
||||
},
|
||||
)
|
||||
if not hasattr(response.get('resources'), 'append'):
|
||||
raise MaarchError('missing resources field or bad type', response)
|
||||
return [self.Courrier(self, **resource) for resource in response['resources']]
|
||||
|
|
|
@ -14,18 +14,18 @@
|
|||
# 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 optparse import make_option
|
||||
import os
|
||||
from optparse import make_option
|
||||
|
||||
from django.core.files import File
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
|
||||
from ...models import Mail
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
'--category', metavar='CATEGORY', default=None)
|
||||
parser.add_argument('--category', metavar='CATEGORY', default=None)
|
||||
parser.add_argument('filenames', metavar='FILENAME', nargs='+')
|
||||
|
||||
def handle(self, filenames, *args, **kwargs):
|
||||
|
@ -35,7 +35,7 @@ class Command(BaseCommand):
|
|||
continue
|
||||
if not open(filepath).read(5) == '%PDF-':
|
||||
continue
|
||||
mail = Mail(content=File(open(filepath)))
|
||||
mail = Mail(content=ContentFile(open(filepath).read(), name=os.path.basename(filepath)))
|
||||
mail.scanner_category = kwargs.get('category')
|
||||
mail.save()
|
||||
count += 1
|
||||
|
|
|
@ -14,25 +14,26 @@
|
|||
# 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 optparse import make_option
|
||||
import os
|
||||
from optparse import make_option
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
|
||||
from ...models import Mail
|
||||
from ...utils import get_maarch
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Inject mail coming from Maarch into welco.
|
||||
|
||||
Only mail with a status "GRC" are injected,
|
||||
After injection, their status is immediately changed to "GRC_TRT".
|
||||
After injection in w.c.s., their status is changed to "GRCSENT" and an
|
||||
id and an URL of the request in w.c.s. is attached to the mail in
|
||||
Maarch.
|
||||
Only mail with a status "GRC" are injected,
|
||||
After injection, their status is immediately changed to "GRC_TRT".
|
||||
After injection in w.c.s., their status is changed to "GRCSENT" and an
|
||||
id and an URL of the request in w.c.s. is attached to the mail in
|
||||
Maarch.
|
||||
"""
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Mail',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('content', models.FileField(upload_to=b'', verbose_name='Content')),
|
||||
('triaged', models.BooleanField(default=False)),
|
||||
('creation_timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
@ -18,7 +15,6 @@ class Migration(migrations.Migration):
|
|||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='mail',
|
||||
options={'ordering': ['post_date', 'creation_timestamp'],
|
||||
'verbose_name': 'Mail'},
|
||||
options={'ordering': ['post_date', 'creation_timestamp'], 'verbose_name': 'Mail'},
|
||||
),
|
||||
]
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
|
|
|
@ -15,32 +15,30 @@
|
|||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import re
|
||||
import requests
|
||||
import subprocess
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from welco.qualif.models import Association
|
||||
from welco.utils import get_wcs_data
|
||||
|
||||
|
||||
class Mail(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Mail')
|
||||
ordering = ['post_date', 'creation_timestamp']
|
||||
|
||||
content = models.FileField(_('Content'))
|
||||
post_date = models.DateField(_('Post Date'), null=True)
|
||||
registered_mail_number = models.CharField(_('Registered Mail Number'),
|
||||
null=True, max_length=50)
|
||||
registered_mail_number = models.CharField(_('Registered Mail Number'), null=True, max_length=50)
|
||||
note = models.TextField(_('Note'), null=True)
|
||||
external_id = models.CharField(_('External Id'), null=True, max_length=32)
|
||||
|
||||
|
@ -52,8 +50,7 @@ class Mail(models.Model):
|
|||
# common to all source types:
|
||||
status = models.CharField(_('Status'), blank=True, max_length=50)
|
||||
contact_id = models.CharField(max_length=50, null=True)
|
||||
associations = GenericRelation(Association,
|
||||
content_type_field='source_type', object_id_field='source_pk')
|
||||
associations = GenericRelation(Association, content_type_field='source_type', object_id_field='source_pk')
|
||||
|
||||
creation_timestamp = models.DateTimeField(auto_now_add=True)
|
||||
last_update_timestamp = models.DateTimeField(auto_now=True)
|
||||
|
@ -61,6 +58,7 @@ class Mail(models.Model):
|
|||
@classmethod
|
||||
def get_qualification_form_class(cls):
|
||||
from .forms import MailQualificationForm
|
||||
|
||||
return MailQualificationForm
|
||||
|
||||
def get_qualification_form(self):
|
||||
|
@ -111,5 +109,13 @@ class Mail(models.Model):
|
|||
def create_thumbnail(sender, instance, created, **kwargs):
|
||||
if not created:
|
||||
return
|
||||
subprocess.call(['gm', 'convert', '-geometry', '200x',
|
||||
instance.content.file.name, instance.content.file.name + '.png'])
|
||||
subprocess.call(
|
||||
[
|
||||
'gm',
|
||||
'convert',
|
||||
'-geometry',
|
||||
'200x',
|
||||
instance.content.file.name,
|
||||
instance.content.file.name + '.png',
|
||||
]
|
||||
)
|
||||
|
|
|
@ -14,18 +14,17 @@
|
|||
# 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 django.urls import path, re_path
|
||||
|
||||
from .views import (viewer, feeder, qualification_save, edit_note, note,
|
||||
reject, mail_count, mail_response)
|
||||
from .views import edit_note, feeder, mail_count, mail_response, note, qualification_save, reject, viewer
|
||||
|
||||
urlpatterns = [
|
||||
url('viewer/$', viewer, name='mail-viewer'),
|
||||
url('mail/feeder/$', feeder, name='mail-feeder'),
|
||||
url(r'^ajax/mail/reject$', reject, name='mail-reject'),
|
||||
url(r'^ajax/qualification-mail-save$', qualification_save, name='qualif-mail-save'),
|
||||
url(r'^ajax/mail/edit-note/$', edit_note, name='mail-edit-note'),
|
||||
url(r'^ajax/mail/note/(?P<pk>\w+)$', note, name='mail-note'),
|
||||
url(r'^ajax/count/mail/$', mail_count, name='mail-count'),
|
||||
url(r'^api/mail/response/$', mail_response, name='mail-api-response'),
|
||||
path('mail/viewer/', viewer, name='mail-viewer'),
|
||||
path('mail/feeder/', feeder, name='mail-feeder'),
|
||||
path('ajax/mail/reject', reject, name='mail-reject'),
|
||||
path('ajax/qualification-mail-save', qualification_save, name='qualif-mail-save'),
|
||||
path('ajax/mail/edit-note/', edit_note, name='mail-edit-note'),
|
||||
re_path(r'^ajax/mail/note/(?P<pk>\w+)$', note, name='mail-note'),
|
||||
path('ajax/count/mail/', mail_count, name='mail-count'),
|
||||
path('api/mail/response/', mail_response, name='mail-api-response'),
|
||||
]
|
||||
|
|
|
@ -20,10 +20,19 @@ from .maarch import MaarchCourrier, MaarchError
|
|||
|
||||
|
||||
class WelcoMaarchCourrier(MaarchCourrier):
|
||||
def __init__(self, url, username, password, grc_status,
|
||||
grc_received_status, grc_send_status, grc_refused_status,
|
||||
grc_response_status, batch_size=10):
|
||||
super(WelcoMaarchCourrier, self).__init__(url, username, password)
|
||||
def __init__(
|
||||
self,
|
||||
url,
|
||||
username,
|
||||
password,
|
||||
grc_status,
|
||||
grc_received_status,
|
||||
grc_send_status,
|
||||
grc_refused_status,
|
||||
grc_response_status,
|
||||
batch_size=10,
|
||||
):
|
||||
super().__init__(url, username, password)
|
||||
self.grc_status = grc_status
|
||||
self.grc_received_status = grc_received_status
|
||||
self.grc_send_status = grc_send_status
|
||||
|
@ -36,10 +45,11 @@ class WelcoMaarchCourrier(MaarchCourrier):
|
|||
clause="status='%s'" % self.grc_status,
|
||||
include_file=True,
|
||||
order_by=['res_id'],
|
||||
limit=self.batch_size)
|
||||
limit=self.batch_size,
|
||||
)
|
||||
|
||||
def get_mail(self, mail_id):
|
||||
return self.get_courriers(clause="res_id=%s" % mail_id)[0]
|
||||
return self.get_courriers(clause='res_id=%s' % mail_id)[0]
|
||||
|
||||
def set_grc_received_status(self, mails):
|
||||
self.update_status(mails, self.grc_received_status)
|
||||
|
@ -74,5 +84,5 @@ def get_maarch():
|
|||
grc_received_status=config.get('STATUS_RECEIVED', 'GRC_TRT'),
|
||||
grc_send_status=config.get('STATUS_SEND', 'GRCSENT'),
|
||||
grc_refused_status=config.get('STATUS_REFUSED', 'GRCREFUSED'),
|
||||
grc_response_status=config.get('STATUS_RESPONSE', 'GRC_RESPONSE'))
|
||||
|
||||
grc_response_status=config.get('STATUS_RESPONSE', 'GRC_RESPONSE'),
|
||||
)
|
||||
|
|
|
@ -18,35 +18,33 @@ import json
|
|||
import logging
|
||||
|
||||
from django import template
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.template import RequestContext
|
||||
from django.db.transaction import atomic
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.template import RequestContext
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import TemplateView
|
||||
from django.db.transaction import atomic
|
||||
|
||||
from rest_framework import authentication, serializers, permissions, status
|
||||
from rest_framework import authentication, permissions, serializers, status
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
from welco.utils import response_for_json
|
||||
|
||||
from .models import Mail
|
||||
from .forms import MailQualificationForm
|
||||
from .utils import get_maarch, MaarchError
|
||||
|
||||
from .models import Mail
|
||||
from .utils import MaarchError, get_maarch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def viewer(request, *args, **kwargs):
|
||||
if not 'file' in request.GET:
|
||||
return HttpResponseRedirect('?file=')
|
||||
body = template.loader.get_template('welco/mail_viewer.html').render(
|
||||
request=request)
|
||||
body = template.loader.get_template('welco/mail_viewer.html').render(request=request)
|
||||
return HttpResponse(body)
|
||||
|
||||
|
||||
|
@ -57,13 +55,14 @@ class Feeder(TemplateView):
|
|||
for upload in request.FILES.getlist('mail'):
|
||||
mail = Mail(content=upload)
|
||||
mail.save()
|
||||
messages.info(request, _('%d files uploaded successfully.') %
|
||||
len(request.FILES.getlist('mail')))
|
||||
messages.info(request, _('%d files uploaded successfully.') % len(request.FILES.getlist('mail')))
|
||||
return HttpResponseRedirect(reverse('mail-feeder'))
|
||||
|
||||
|
||||
feeder = login_required(csrf_exempt(Feeder.as_view()))
|
||||
|
||||
class Home(object):
|
||||
|
||||
class Home:
|
||||
source_key = 'mail'
|
||||
display_filter = True
|
||||
allow_reject = True
|
||||
|
@ -101,16 +100,17 @@ def qualification_save(request, *args, **kwargs):
|
|||
mail.reference = form.cleaned_data['reference']
|
||||
mail.subject = form.cleaned_data['subject']
|
||||
mail.save()
|
||||
return HttpResponseRedirect(reverse('qualif-zone') +
|
||||
'?source_type=%s&source_pk=%s' % (request.POST['source_type'],
|
||||
request.POST['source_pk']))
|
||||
return HttpResponseRedirect(
|
||||
reverse('qualif-zone')
|
||||
+ '?source_type=%s&source_pk=%s' % (request.POST['source_type'], request.POST['source_pk'])
|
||||
)
|
||||
|
||||
|
||||
class EditNote(TemplateView):
|
||||
template_name = 'welco/mail_edit_note.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EditNote, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['mail'] = Mail.objects.get(id=self.request.GET['mail'])
|
||||
return context
|
||||
|
||||
|
@ -120,6 +120,7 @@ class EditNote(TemplateView):
|
|||
mail.save()
|
||||
return HttpResponse(json.dumps({'result': 'ok'}))
|
||||
|
||||
|
||||
edit_note = login_required(csrf_exempt(EditNote.as_view()))
|
||||
|
||||
|
||||
|
@ -192,4 +193,5 @@ class MailResponseAPIView(GenericAPIView):
|
|||
return Response({'err': 1, 'err_desc': str(e)})
|
||||
return Response({'err': 0})
|
||||
|
||||
|
||||
mail_response = MailResponseAPIView.as_view()
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
dependencies = []
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PhoneCall',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('number', models.CharField(max_length=20, verbose_name='Number')),
|
||||
('creation_timestamp', models.DateTimeField(auto_now_add=True)),
|
||||
('last_update_timestamp', models.DateTimeField(auto_now=True)),
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.utils.timezone import utc
|
||||
from django.utils.timezone import now
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.utils.timezone import now, utc
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
@ -19,7 +16,10 @@ class Migration(migrations.Migration):
|
|||
migrations.CreateModel(
|
||||
name='PhoneLine',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
(
|
||||
'id',
|
||||
models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True),
|
||||
),
|
||||
('callee', models.CharField(unique=True, max_length=20, verbose_name='Callee')),
|
||||
('users', models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
|
|
@ -18,15 +18,15 @@ import logging
|
|||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import now, timedelta
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from welco.qualif.models import Association
|
||||
|
||||
class PhoneCall(models.Model):
|
||||
|
||||
class PhoneCall(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _('Phone Call')
|
||||
|
||||
|
@ -39,8 +39,7 @@ class PhoneCall(models.Model):
|
|||
# common to all source types:
|
||||
status = models.CharField(_('Status'), blank=True, max_length=50)
|
||||
contact_id = models.CharField(max_length=50, null=True)
|
||||
associations = GenericRelation(Association,
|
||||
content_type_field='source_type', object_id_field='source_pk')
|
||||
associations = GenericRelation(Association, content_type_field='source_type', object_id_field='source_pk')
|
||||
|
||||
creation_timestamp = models.DateTimeField(auto_now_add=True)
|
||||
last_update_timestamp = models.DateTimeField(auto_now=True)
|
||||
|
@ -61,14 +60,13 @@ class PhoneCall(models.Model):
|
|||
if settings.PHONE_MAX_CALL_DURATION:
|
||||
logger = logging.getLogger(__name__)
|
||||
start_after = now() - timedelta(minutes=settings.PHONE_MAX_CALL_DURATION)
|
||||
for call in cls.objects.filter(callee__in=PhoneLine.get_callees(user),
|
||||
stop__isnull=True,
|
||||
start__lt=start_after):
|
||||
for call in cls.objects.filter(
|
||||
callee__in=PhoneLine.get_callees(user), stop__isnull=True, start__lt=start_after
|
||||
):
|
||||
logger.info('stop expired call from %s to %s', call.caller, call.callee)
|
||||
call.stop = now()
|
||||
call.save()
|
||||
return cls.objects.filter(callee__in=PhoneLine.get_callees(user),
|
||||
stop__isnull=True).order_by('start')
|
||||
return cls.objects.filter(callee__in=PhoneLine.get_callees(user), stop__isnull=True).order_by('start')
|
||||
|
||||
@classmethod
|
||||
def get_all_callees(cls):
|
||||
|
@ -80,15 +78,15 @@ class PhoneCall(models.Model):
|
|||
}
|
||||
|
||||
def previous_calls(self):
|
||||
return PhoneCall.objects.filter(caller=self.caller).exclude(
|
||||
id=self.id).order_by('-start')[:5]
|
||||
return PhoneCall.objects.filter(caller=self.caller).exclude(id=self.id).order_by('-start')[:5]
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
if not self.stop:
|
||||
return 'n.a.'
|
||||
seconds = (self.stop - self.start).seconds
|
||||
return '%02d:%02d' % (seconds//60, seconds%60)
|
||||
return '%02d:%02d' % (seconds // 60, seconds % 60)
|
||||
|
||||
|
||||
class PhoneLine(models.Model):
|
||||
callee = models.CharField(_('Callee'), unique=True, max_length=80)
|
||||
|
|
|
@ -14,15 +14,15 @@
|
|||
# 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 django.urls import path, re_path
|
||||
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^ajax/phone/zone/$', views.zone, name='phone-zone'),
|
||||
url(r'^api/phone/call-event/$', views.call_event, name='phone-call-event'),
|
||||
url(r'^api/phone/active-call/(?P<pk>\w+)/$', views.active_call, name='phone-active-call'),
|
||||
url(r'^api/phone/current-calls/$', views.current_calls, name='phone-current-calls'),
|
||||
url(r'^api/phone/take-line/$', views.take_line, name='phone-take-line'),
|
||||
url(r'^api/phone/release-line/$', views.release_line, name='phone-release-line'),
|
||||
path('ajax/phone/zone/', views.zone, name='phone-zone'),
|
||||
path('api/phone/call-event/', views.call_event, name='phone-call-event'),
|
||||
re_path(r'^api/phone/active-call/(?P<pk>\w+)/$', views.active_call, name='phone-active-call'),
|
||||
path('api/phone/current-calls/', views.current_calls, name='phone-current-calls'),
|
||||
path('api/phone/take-line/', views.take_line, name='phone-take-line'),
|
||||
path('api/phone/release-line/', views.release_line, name='phone-release-line'),
|
||||
]
|
||||
|
|
|
@ -19,18 +19,19 @@ import logging
|
|||
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template import RequestContext
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpResponseBadRequest, HttpResponse
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.template import RequestContext
|
||||
from django.utils.encoding import force_str
|
||||
from django.utils.timezone import now
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from .models import PhoneCall, PhoneLine
|
||||
|
||||
|
||||
class Home(object):
|
||||
class Home:
|
||||
source_key = 'phone'
|
||||
|
||||
def __init__(self, request, **kwargs):
|
||||
|
@ -55,59 +56,63 @@ class PhoneZone(TemplateView):
|
|||
username = self.request.session.get('mellon_session', {}).get('username')
|
||||
if username:
|
||||
# user is from SSO, username is a phone line (callee), create a link to it
|
||||
username = username[0].split('@', 1)[0][:80] # remove realm
|
||||
username = username[0].split('@', 1)[0][:80] # remove realm
|
||||
if username:
|
||||
PhoneLine.take(callee=username, user=self.request.user)
|
||||
context = super(PhoneZone, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['source_type'] = ContentType.objects.get_for_model(PhoneCall)
|
||||
context['phonelines'] = PhoneLine.objects.filter(users__id=self.request.user.id)
|
||||
context['phonecalls'] = PhoneCall.get_current_calls(self.request.user)
|
||||
return context
|
||||
|
||||
zone = csrf_exempt(PhoneZone.as_view())
|
||||
|
||||
zone = csrf_exempt(PhoneZone.as_view())
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def call_event(request):
|
||||
'''Log a new call start or stop, input is JSON:
|
||||
"""Log a new call start or stop, input is JSON:
|
||||
|
||||
{
|
||||
'event': 'start' or 'stop',
|
||||
'caller': '003399999999',
|
||||
'callee': '102',
|
||||
'data': {
|
||||
'user': 'zozo',
|
||||
},
|
||||
}
|
||||
'''
|
||||
{
|
||||
'event': 'start' or 'stop',
|
||||
'caller': '003399999999',
|
||||
'callee': '102',
|
||||
'data': {
|
||||
'user': 'zozo',
|
||||
},
|
||||
}
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
payload = json.loads(force_str(request.body))
|
||||
assert isinstance(payload, dict), 'payload is not a JSON object'
|
||||
assert set(payload.keys()) <= set(['event', 'caller', 'callee', 'data']), \
|
||||
'payload keys must be "event", "caller", "callee" and optionnaly "data"'
|
||||
assert set(['event', 'caller', 'callee']) <= set(payload.keys()), \
|
||||
'payload keys must be "event", "caller", "callee" and optionnaly "data"'
|
||||
assert set(payload.keys()) <= {
|
||||
'event',
|
||||
'caller',
|
||||
'callee',
|
||||
'data',
|
||||
}, 'payload keys must be "event", "caller", "callee" and optionnaly "data"'
|
||||
assert {'event', 'caller', 'callee'} <= set(
|
||||
payload.keys()
|
||||
), 'payload keys must be "event", "caller", "callee" and optionnaly "data"'
|
||||
assert payload['event'] in ('start', 'stop'), 'event must be "start" or "stop"'
|
||||
assert isinstance(payload['caller'], unicode), 'caller must be a string'
|
||||
assert isinstance(payload['callee'], unicode), 'callee must be a string'
|
||||
assert isinstance(payload['caller'], str), 'caller must be a string'
|
||||
assert isinstance(payload['callee'], str), 'callee must be a string'
|
||||
if 'data' in payload:
|
||||
assert isinstance(payload['data'], dict), 'data must be a JSON object'
|
||||
except (TypeError, ValueError, AssertionError), e:
|
||||
return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
|
||||
unicode(e)}),
|
||||
content_type='application/json')
|
||||
except (TypeError, ValueError, AssertionError) as e:
|
||||
return HttpResponseBadRequest(
|
||||
json.dumps({'err': 1, 'msg': force_str(e)}), content_type='application/json'
|
||||
)
|
||||
# janitoring: stop active calls to the callee
|
||||
if settings.PHONE_ONE_CALL_PER_CALLEE:
|
||||
logger.info('stop all calls to %s', payload['callee'])
|
||||
PhoneCall.objects.filter(callee=payload['callee'],
|
||||
stop__isnull=True).update(stop=now())
|
||||
PhoneCall.objects.filter(callee=payload['callee'], stop__isnull=True).update(stop=now())
|
||||
else:
|
||||
logger.info('stop call from %s to %s', payload['caller'], payload['callee'])
|
||||
PhoneCall.objects.filter(caller=payload['caller'],
|
||||
callee=payload['callee'],
|
||||
stop__isnull=True).update(stop=now())
|
||||
PhoneCall.objects.filter(
|
||||
caller=payload['caller'], callee=payload['callee'], stop__isnull=True
|
||||
).update(stop=now())
|
||||
if payload['event'] == 'start':
|
||||
# start a new call
|
||||
kwargs = {
|
||||
|
@ -127,40 +132,39 @@ def active_call(request, *args, **kwargs):
|
|||
result = {
|
||||
'caller': call.caller,
|
||||
'callee': call.callee,
|
||||
'active': not(bool(call.stop)),
|
||||
'active': not (bool(call.stop)),
|
||||
'start_timestamp': call.start.strftime('%Y-%m-%dT%H:%M:%S'),
|
||||
}
|
||||
return HttpResponse(json.dumps(result, indent=2),
|
||||
content_type='application/json')
|
||||
}
|
||||
return HttpResponse(json.dumps(result, indent=2), content_type='application/json')
|
||||
|
||||
|
||||
@login_required
|
||||
def current_calls(request):
|
||||
'''Returns the list of current calls for current user as JSON:
|
||||
"""Returns the list of current calls for current user as JSON:
|
||||
|
||||
{
|
||||
'err': 0,
|
||||
'data': {
|
||||
'calls': [
|
||||
{
|
||||
'caller': '00334545445',
|
||||
'callee': '102',
|
||||
'data': { ... },
|
||||
},
|
||||
...
|
||||
],
|
||||
'lines': [
|
||||
'102',
|
||||
],
|
||||
'all-lines': [
|
||||
'102',
|
||||
],
|
||||
}
|
||||
}
|
||||
{
|
||||
'err': 0,
|
||||
'data': {
|
||||
'calls': [
|
||||
{
|
||||
'caller': '00334545445',
|
||||
'callee': '102',
|
||||
'data': { ... },
|
||||
},
|
||||
...
|
||||
],
|
||||
'lines': [
|
||||
'102',
|
||||
],
|
||||
'all-lines': [
|
||||
'102',
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
lines are number the user is currently watching, all-lines is all
|
||||
registered numbers.
|
||||
'''
|
||||
lines are number the user is currently watching, all-lines is all
|
||||
registered numbers.
|
||||
"""
|
||||
all_callees = PhoneCall.get_all_callees()
|
||||
callees = PhoneLine.get_callees(request.user)
|
||||
phonecalls = PhoneCall.get_current_calls(request.user)
|
||||
|
@ -175,11 +179,13 @@ def current_calls(request):
|
|||
},
|
||||
}
|
||||
for call in phonecalls:
|
||||
calls.append({
|
||||
'caller': call.caller,
|
||||
'callee': call.callee,
|
||||
'start': call.start.isoformat('T').split('.')[0],
|
||||
})
|
||||
calls.append(
|
||||
{
|
||||
'caller': call.caller,
|
||||
'callee': call.callee,
|
||||
'start': call.start.isoformat('T').split('.')[0],
|
||||
}
|
||||
)
|
||||
if call.data:
|
||||
calls[-1]['data'] = json.loads(call.data)
|
||||
response = HttpResponse(content_type='application/json')
|
||||
|
@ -190,40 +196,40 @@ def current_calls(request):
|
|||
@csrf_exempt
|
||||
@login_required
|
||||
def take_line(request):
|
||||
'''Take a line, input is JSON:
|
||||
"""Take a line, input is JSON:
|
||||
|
||||
{ 'callee': '003369999999' }
|
||||
'''
|
||||
{ 'callee': '003369999999' }
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
payload = json.loads(force_str(request.body))
|
||||
assert isinstance(payload, dict), 'payload is not a JSON object'
|
||||
assert payload.keys() == ['callee'], 'payload must have only one key: callee'
|
||||
except (TypeError, ValueError, AssertionError), e:
|
||||
return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
|
||||
unicode(e)}),
|
||||
content_type='application/json')
|
||||
assert list(payload.keys()) == ['callee'], 'payload must have only one key: callee'
|
||||
except (TypeError, ValueError, AssertionError) as e:
|
||||
return HttpResponseBadRequest(
|
||||
json.dumps({'err': 1, 'msg': force_str(e)}), content_type='application/json'
|
||||
)
|
||||
PhoneLine.take(payload['callee'], request.user)
|
||||
logger.info(u'user %s took line %s', request.user, payload['callee'])
|
||||
logger.info('user %s took line %s', request.user, payload['callee'])
|
||||
return HttpResponse(json.dumps({'err': 0}), content_type='application/json')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def release_line(request):
|
||||
'''Release a line, input is JSON:
|
||||
"""Release a line, input is JSON:
|
||||
|
||||
{ 'callee': '003369999999' }
|
||||
'''
|
||||
{ 'callee': '003369999999' }
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
payload = json.loads(force_str(request.body))
|
||||
assert isinstance(payload, dict), 'payload is not a JSON object'
|
||||
assert payload.keys() == ['callee'], 'payload must have only one key: callee'
|
||||
except (TypeError, ValueError, AssertionError), e:
|
||||
return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
|
||||
unicode(e)}),
|
||||
content_type='application/json')
|
||||
assert list(payload.keys()) == ['callee'], 'payload must have only one key: callee'
|
||||
except (TypeError, ValueError, AssertionError) as e:
|
||||
return HttpResponseBadRequest(
|
||||
json.dumps({'err': 1, 'msg': force_str(e)}), content_type='application/json'
|
||||
)
|
||||
PhoneLine.release(payload['callee'], request.user)
|
||||
logger.info(u'user %s released line %s', request.user, payload['callee'])
|
||||
logger.info('user %s released line %s', request.user, payload['callee'])
|
||||
return HttpResponse(json.dumps({'err': 0}), content_type='application/json')
|
||||
|
|
|
@ -9,7 +9,7 @@ body.welco-home div#main-content {
|
|||
height: calc(100vh - 61px); /* #top 60px + #top border 1px */
|
||||
}
|
||||
|
||||
body[data-environment-label] div#main-content {
|
||||
body.welco-home[data-environment-label] div#main-content {
|
||||
height: calc(100vh - 71px); /* #top 60px + #top border 1px + #header bottom-border 10px */
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
{% extends "welco/base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ form }}
|
||||
<button>{% trans 'Log in' %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -14,68 +14,69 @@
|
|||
# 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 ckeditor import views as ckeditor_views
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
|
||||
from django.urls import include, path, re_path
|
||||
from django.views.decorators.cache import never_cache
|
||||
|
||||
from ckeditor import views as ckeditor_views
|
||||
import welco.contacts.views
|
||||
import welco.kb.views
|
||||
import welco.views
|
||||
|
||||
from . import apps
|
||||
from .kb.views import kb_manager_required
|
||||
|
||||
import welco.views
|
||||
import welco.contacts.views
|
||||
import welco.kb.views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', welco.views.home, name='home'),
|
||||
url(r'^mail/$', welco.views.home_mail, name='home-mail'),
|
||||
url(r'^phone/$', welco.views.home_phone, name='home-phone'),
|
||||
url(r'^counter/$', welco.views.home_counter, name='home-counter'),
|
||||
url(r'^', include('welco.sources.phone.urls')),
|
||||
url(r'^', include('welco.sources.counter.urls')),
|
||||
url(r'^ajax/qualification$', welco.views.qualification, name='qualif-zone'),
|
||||
url(r'^ajax/remove-association/(?P<pk>\w+)$',
|
||||
welco.views.remove_association, name='ajax-remove-association'),
|
||||
url(r'^ajax/create-formdata/(?P<pk>\w+)$',
|
||||
welco.views.create_formdata, name='ajax-create-formdata'),
|
||||
|
||||
url(r'^ajax/kb$', welco.kb.views.zone, name='kb-zone'),
|
||||
url(r'^kb/$', welco.kb.views.page_list, name='kb-home'),
|
||||
url(r'^kb/add/$', welco.kb.views.page_add, name='kb-page-add'),
|
||||
url(r'^kb/search/$', welco.kb.views.page_search, name='kb-page-search'),
|
||||
url(r'^kb/search/json/$', welco.kb.views.page_search_json, name='kb-page-search-json'),
|
||||
url(r'^kb/(?P<slug>[\w-]+)/$', welco.kb.views.page_detail, name='kb-page-view'),
|
||||
url(r'^ajax/kb/(?P<slug>[\w-]+)/$', welco.kb.views.page_detail_fragment, name='kb-page-fragment'),
|
||||
url(r'^kb/(?P<slug>[\w-]+)/edit$', welco.kb.views.page_edit, name='kb-page-edit'),
|
||||
url(r'^kb/(?P<slug>[\w-]+)/delete$', welco.kb.views.page_delete, name='kb-page-delete'),
|
||||
url(r'^kb/(?P<slug>[\w-]+)/history$', welco.kb.views.page_history, name='kb-page-history'),
|
||||
url(r'^kb/(?P<slug>[\w-]+)/version/(?P<version>\w+)/$', welco.kb.views.page_version, name='kb-page-version'),
|
||||
|
||||
url(r'^ajax/contacts$', welco.contacts.views.zone, name='contacts-zone'),
|
||||
url(r'^contacts/search/json/$', welco.contacts.views.search_json, name='contacts-search-json'),
|
||||
url(r'^ajax/contacts/(?P<slug>[\w-]+)/$',
|
||||
welco.contacts.views.contact_detail_fragment, name='contact-page-fragment'),
|
||||
url(r'^contacts/add/$', welco.contacts.views.contact_add, name='contacts-add'),
|
||||
|
||||
url(r'^ajax/summary/(?P<source_type>\w+)/(?P<source_pk>\w+)/$',
|
||||
welco.views.wcs_summary, name='wcs-summary'),
|
||||
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^logout/$', welco.views.logout, name='auth_logout'),
|
||||
url(r'^login/$', welco.views.login, name='auth_login'),
|
||||
url(r'^menu.json$', welco.views.menu_json, name='menu_json'),
|
||||
|
||||
url(r'^ckeditor/upload/', kb_manager_required(ckeditor_views.upload), name='ckeditor_upload'),
|
||||
url(r'^ckeditor/browse/', never_cache(kb_manager_required(ckeditor_views.browse)), name='ckeditor_browse'),
|
||||
path('', welco.views.home, name='home'),
|
||||
path('mail/', welco.views.home_mail, name='home-mail'),
|
||||
path('phone/', welco.views.home_phone, name='home-phone'),
|
||||
path('counter/', welco.views.home_counter, name='home-counter'),
|
||||
re_path(r'^', include('welco.sources.phone.urls')),
|
||||
re_path(r'^', include('welco.sources.counter.urls')),
|
||||
path('ajax/qualification', welco.views.qualification, name='qualif-zone'),
|
||||
re_path(
|
||||
r'^ajax/remove-association/(?P<pk>\w+)$',
|
||||
welco.views.remove_association,
|
||||
name='ajax-remove-association',
|
||||
),
|
||||
re_path(r'^ajax/create-formdata/(?P<pk>\w+)$', welco.views.create_formdata, name='ajax-create-formdata'),
|
||||
path('ajax/kb', welco.kb.views.zone, name='kb-zone'),
|
||||
path('kb/', welco.kb.views.page_list, name='kb-home'),
|
||||
path('kb/add/', welco.kb.views.page_add, name='kb-page-add'),
|
||||
path('kb/search/', welco.kb.views.page_search, name='kb-page-search'),
|
||||
path('kb/search/json/', welco.kb.views.page_search_json, name='kb-page-search-json'),
|
||||
re_path(r'^kb/(?P<slug>[\w-]+)/$', welco.kb.views.page_detail, name='kb-page-view'),
|
||||
re_path(r'^ajax/kb/(?P<slug>[\w-]+)/$', welco.kb.views.page_detail_fragment, name='kb-page-fragment'),
|
||||
re_path(r'^kb/(?P<slug>[\w-]+)/edit$', welco.kb.views.page_edit, name='kb-page-edit'),
|
||||
re_path(r'^kb/(?P<slug>[\w-]+)/delete$', welco.kb.views.page_delete, name='kb-page-delete'),
|
||||
path('ajax/contacts', welco.contacts.views.zone, name='contacts-zone'),
|
||||
path('contacts/search/json/', welco.contacts.views.search_json, name='contacts-search-json'),
|
||||
re_path(
|
||||
r'^ajax/contacts/(?P<slug>[\w-]+)/$',
|
||||
welco.contacts.views.contact_detail_fragment,
|
||||
name='contact-page-fragment',
|
||||
),
|
||||
path('contacts/add/', welco.contacts.views.contact_add, name='contacts-add'),
|
||||
re_path(
|
||||
r'^ajax/summary/(?P<source_type>\w+)/(?P<source_pk>\w+)/$',
|
||||
welco.views.wcs_summary,
|
||||
name='wcs-summary',
|
||||
),
|
||||
re_path(r'^admin/', admin.site.urls),
|
||||
path('logout/', welco.views.logout, name='auth_logout'),
|
||||
path('login/', welco.views.login, name='auth_login'),
|
||||
re_path(r'^menu.json$', welco.views.menu_json, name='menu_json'),
|
||||
re_path(r'^ckeditor/upload/', kb_manager_required(ckeditor_views.upload), name='ckeditor_upload'),
|
||||
re_path(
|
||||
r'^ckeditor/browse/', never_cache(kb_manager_required(ckeditor_views.browse)), name='ckeditor_browse'
|
||||
),
|
||||
]
|
||||
|
||||
if 'mellon' in settings.INSTALLED_APPS:
|
||||
urlpatterns.append(url(r'^accounts/mellon/', include('mellon.urls')))
|
||||
urlpatterns.append(re_path(r'^accounts/mellon/', include('mellon.urls')))
|
||||
|
||||
# static and media files
|
||||
urlpatterns += staticfiles_urlpatterns()
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue