Compare commits

...

95 Commits

Author SHA1 Message Date
Valentin Deniaud c3eb561c16 setup: allow django-taggit 3.1.0 (#81954)
gitea/welco/pipeline/head This commit looks good Details
2023-12-18 12:41:09 +01:00
Valentin Deniaud ebc92cfc7f setup: allow django-haystack 3.2.1 (#81953)
gitea/welco/pipeline/head This commit looks good Details
2023-12-18 11:04:48 +01:00
Thomas NOËL 6e2eac43a6 debian: add back memory-report to uwsgi default configuration (#80451)
gitea/welco/pipeline/head This commit looks good Details
2023-11-13 11:35:35 +01:00
Thomas NOËL 5689cf1f0a debian: add uwsgi/welco SyslogIdentifier in service (#82977)
gitea/welco/pipeline/head This commit looks good Details
2023-10-31 13:22:20 +01:00
Frédéric Péters a71993b73f tox: run tests against djangorestframework 3.14 (#81950)
gitea/welco/pipeline/head This commit looks good Details
2023-10-03 17:02:52 +02:00
Frédéric Péters 7d6b3549b3 tox: stop testing django 2.2 (#81950) 2023-10-03 17:02:40 +02:00
Frédéric Péters 9d333b9957 ci: keep on using pylint 2 while pylint-django is not ready (#81905)
gitea/welco/pipeline/head This commit looks good Details
2023-10-03 06:16:49 +02:00
Valentin Deniaud c04a5c50bd misc: update git-blame-ignore-revs to ignore quote changes (#79788)
gitea/welco/pipeline/head This commit looks good Details
2023-08-16 11:53:33 +02:00
Valentin Deniaud 103dff03cb misc: apply double-quote-string-fixer (#79788) 2023-08-16 11:53:33 +02:00
Valentin Deniaud 5f5d814feb misc: add pre commit hook to force single quotes (#79788) 2023-08-16 11:53:32 +02:00
Thomas NOËL 2c88fcfa68 debian: remove memory-report from uwsgi default configuration (#79890)
gitea/welco/pipeline/head This commit looks good Details
2023-07-20 18:00:26 +02:00
Frédéric Péters a9eab5c891 ci: build deb package for bookworm (#78968)
gitea/welco/pipeline/head This commit looks good Details
2023-06-23 17:58:32 +02:00
Agate 7c9e166981 Prepare Jenkinsfile for Gitea migration (#74572)
gitea/welco/pipeline/head This commit looks good Details
2023-02-20 15:18:04 +01:00
Frédéric Péters eb44518502 ci: upgrade isort (#74044) 2023-02-01 09:29:09 +01:00
Frédéric Péters 2bc91fff29 ci: only build package for bullseye (#72729) 2022-12-22 17:21:30 +01:00
Frédéric Péters 8242b1b6b9 ci: update pyupgrade to 3.1.0 (#70693) 2022-10-28 08:11:42 +02:00
Agate 295e994b83 Revert "django4: fix default AppConfig deprecation warnings (#68573)"
This reverts commit e1f969f813.
2022-09-05 16:16:43 +02:00
Agate ee871ebc33 django4: replaced ugettext* calls with corresponding gettext* calls (#68573) 2022-08-31 09:49:42 +02:00
Agate ed3fc882cd django4: replaced force_text with equivalent force_str (#68573) 2022-08-31 09:49:42 +02:00
Agate d8786aae95 django4: fix urls deprecation warnings (#68573) 2022-08-31 09:49:42 +02:00
Agate e1f969f813 django4: fix default AppConfig deprecation warnings (#68573) 2022-08-31 09:49:42 +02:00
Agate 3d9481f67d enabled django 3.2 testing in tox file (#68060) 2022-08-09 15:59:45 +02:00
Frédéric Péters f0c32435c6 debian: make cron quiet (#67897) 2022-08-03 09:59:55 +02:00
Frédéric Péters 9e165aebfc debian: remove obsolete standard error output config from systemd unit (#65101) 2022-08-02 10:01:55 +02:00
Serghei Mihai da41d0b6bd tox: add test environments relying on djangorestframework 3.12 (#64290) 2022-04-20 12:14:21 +02:00
Serghei Mihai e2934f92c6 setup: allow djangorestframework 3.12 (#64290) 2022-04-20 12:08:12 +02:00
Frédéric Péters 549fc3ec00 misc: remove usage of django.utils.six (#63686) 2022-04-15 18:12:53 +02:00
Thomas NOËL eebe703fca trivial: bump black version to 22.3.0 2022-03-31 12:32:44 +02:00
Frédéric Péters 40fe3f835f trivial: bump black version to 22.1.0 (#62312) 2022-03-01 19:30:48 +01:00
Frédéric Péters ab9b2f110c debian: update django dependency to 2.2 2022-02-18 10:09:12 +01:00
Frédéric Péters a72cc63a7f trivial: remove python 2 from classifiers 2022-02-02 08:09:21 +01:00
Frédéric Péters 3a6215856f mail: use absolute path in url patterns (#59894) 2021-12-19 18:07:31 +01:00
Frédéric Péters 2a287e61b9 feed mail: use FileContent to avoid SuspiciousOperation (#59894) 2021-12-19 17:46:51 +01:00
Frédéric Péters f934f78403 build: update setup.py to require at least django 2.2 2021-12-19 16:42:14 +01:00
Frédéric Péters 901db27863 jenkins: build packages for buster & bullseye 2021-12-12 10:52:18 +01:00
Frédéric Péters 72449ed00d build: bump black version 2021-11-22 22:09:58 +01:00
Frédéric Péters c3fc205a85 debian: switch to debhelper-compat 12 (#57538) 2021-10-10 10:50:56 +02:00
Emmanuel Cazenave 7e8e50816f uwsgi: enable provisionning spooler (#55092) 2021-08-31 12:14:22 +02:00
Emmanuel Cazenave 6432625ff2 debian: add uwsgi spooler (#55570) 2021-08-26 11:14:39 +02:00
Frédéric Péters aec0edb0c8 build: document and use isort and pyupgrade 2021-07-13 11:42:39 +02:00
Frédéric Péters 250ef2e74a trivial: apply isort & pyupgrade 2021-07-13 11:41:37 +02:00
Nicolas Roche 36c0e69cb1 kb: use html.unescape (#55535) 2021-07-13 11:24:39 +02:00
Frédéric Péters 7abee88825 misc: only force height of home page (#55546) 2021-07-12 19:55:57 +02:00
Frédéric Péters eef27053e1 tox: stop testing against django 1.11 2021-07-03 14:48:09 +02:00
Frédéric Péters 3635a9524d misc: monkeypatch django-ckeditor support for django 2.2 (#55347) 2021-07-02 14:16:26 +02:00
Frédéric Péters ad46ff8cbb debian: enable uwsgi memory reports (#54610) 2021-06-06 10:34:45 +02:00
Emmanuel Cazenave 5a850e2a24 misc: allow djangorestframework 3.9.x (#50105) 2021-02-16 12:58:10 +01:00
Frédéric Péters 044de41420 tox: add black (via pre-commit) to tests (#50927) 2021-02-15 17:52:32 +01:00
Frédéric Péters 8bb319ec8a misc: add black files/notes 2021-01-11 20:11:07 +01:00
Frédéric Péters ce7f2dd500 trivial: apply black 2021-01-11 20:10:12 +01:00
Frédéric Péters 54aae08cfe build: update to use origin/main 2020-12-26 15:21:15 +01:00
Frédéric Péters 70b6a1258a tox: limit mock version for compatibility with python 3.5 2020-10-06 09:22:21 +02:00
Frédéric Péters 718fb058c3 jenkins: don't limit hotfix builds to stretch 2020-09-25 16:37:34 +02:00
Valentin Deniaud 8545b579c7 tox: tell setuptools to use distutils from stdlib (#46252) 2020-09-01 14:40:08 +02:00
Frédéric Péters ad2942cdb8 tox: lift pylint* version limits 2020-07-28 13:18:49 +02:00
Frédéric Péters 914591e037 tox: stop running against python 2 2020-07-28 13:17:54 +02:00
Frédéric Péters f9f922656c misc: remove django-reversion dependency (#41641) 2020-04-24 08:30:38 +02:00
Frédéric Péters 43c19c2a67 translation update 2020-04-14 09:29:53 +02:00
Frédéric Péters 351b056c74 debian: update build-dependencies to use python3 version of django (#41581) 2020-04-13 14:20:38 +02:00
Frédéric Péters f71bb417f0 debian: switch to Python 3 (#41581) 2020-04-11 20:14:07 +02:00
Frédéric Péters ae981257c3 tox: add tests against django 2.2 (#41286) 2020-04-03 08:26:20 +02:00
Frédéric Péters 8c459f523c setup: allow djangorestframework 3.7, for django 2.2 compatibility 2020-04-03 08:26:20 +02:00
Frédéric Péters 72f356e7c7 setup: allow django 2.2 (#41286) 2020-04-03 08:26:20 +02:00
Frédéric Péters 775d3b190b tests: declare missing attributes when mocking known_services (#41286) 2020-04-03 08:26:20 +02:00
Frédéric Péters bf1864f324 misc: use new login class based view (#41286) 2020-04-03 08:26:20 +02:00
Frédéric Péters 72f29fcd68 misc: import mellon only if declared in installed apps (#41286) 2020-04-03 08:26:20 +02:00
Frédéric Péters cd9a25a069 trivial: import reverse from django.urls (#41286) 2020-04-02 21:33:56 +02:00
Frédéric Péters bd09632404 misc: add on_delete to foreign key (#41286) 2020-04-02 21:33:55 +02:00
Frédéric Péters 15ce4e23ea settings: remove unnecessary middleware (#41286) 2020-04-02 21:13:56 +02:00
Frédéric Péters 99fb727020 misc: upgrade import of admin site URLs for 2.2 compatibility (#41286) 2020-04-02 21:13:52 +02:00
Frédéric Péters e6065fb410 setup: allow micro django-ckeditor updates (#41233) 2020-04-01 16:38:19 +02:00
Frédéric Péters aa22e751d7 debian: switch to uwsgi (#41204) 2020-03-31 20:39:40 +02:00
Nicolas Roche 44afd83770 tests: add tests on feed_mail command (#40816) 2020-03-20 09:37:59 +01:00
Nicolas Roche 126e137998 tests: add tests on mail views (#40816) 2020-03-20 09:37:07 +01:00
Nicolas Roche 3380e678b6 tests: add tests on contacts views (#40816) 2020-03-19 15:25:52 +01:00
Nicolas Roche 563b6ae83c tests: add tests on main views.py (#40816) 2020-03-19 15:25:52 +01:00
Nicolas Roche 6ee1285701 python3: convert .values to list before accessing (#40830) 2020-03-19 15:23:01 +01:00
Nicolas Roche 5e731e5b59 templates: add a login template (#40802) 2020-03-18 10:28:11 +01:00
Frédéric Péters 8b939a281e tox: bump pytest version 2020-03-17 19:21:10 +01:00
Frédéric Péters 537e37d371 jenkins: switch to mergeJunitResults 2020-03-17 19:14:28 +01:00
Frédéric Péters 62d08f4243 python3: always pass strings to json.loads, for 3.5 compatibility (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters 5e6226e167 python3: update tox to check against both python versions (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters a78e567583 python3: convert .keys to list before comparing (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters e2eb510f87 python3: update maarch tests (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters 9a5edc0b00 python3: encode response.content in tests (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters b562bef51f python3: use key function to sort categories (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters a8e12b9630 python3: pass bytes for hmac (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters 90c29f0fa0 python3: use relative imports (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters e91178a2e1 python3: use modern except syntax (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters 8112d57b0a python3: replace unicode references (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters 46113c03dd python3: replace iteritems (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters 27af09b510 python3: get urllib/urlparse/htmlparser from six (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters 4f9bb8b161 python3: use exec(open(... to replace execfile (#39092) 2020-03-17 19:06:44 +01:00
Frédéric Péters f165c7d12d setup: limit django-reversion to version 2 (#39092) 2020-03-17 19:04:55 +01:00
Thomas NOËL c94b37c09d debian: log tenants names on migrate_schemas 2020-02-12 15:27:13 +01:00
103 changed files with 1706 additions and 965 deletions

6
.git-blame-ignore-revs Normal file
View File

@ -0,0 +1,6 @@
# trivial: apply black
ce7f2dd5000cf1eb462ae18aeeb5ab66913b452f
# trivial: apply isort & pyupgrade
250ef2e74ae674f7039318787ba85b63f2825c7d
# misc: apply double-quote-string-fixer (#79788)
103dff03cb7862a6dde22b1a358a7af7f86424fc

22
.pre-commit-config.yaml Normal file
View File

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

18
Jenkinsfile vendored
View File

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

@ -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
debian/compat vendored
View File

@ -1 +0,0 @@
9

38
debian/control vendored
View File

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

View File

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

3
debian/py3dist-overrides vendored Normal file
View File

@ -0,0 +1,3 @@
django_ckeditor python3-django-ckeditor
gadjo python3-gadjo
xstatic_select2 python3-xstatic-select2

View File

@ -1 +0,0 @@
django_ckeditor python-django-ckeditor

View File

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

View File

@ -1,2 +0,0 @@
usr/bin/manage.py /usr/lib/welco
usr/lib/python2*/*-packages

View File

@ -1 +0,0 @@
usr/share/javascript/pdf usr/lib/python2.7/dist-packages/welco/sources/mail/static/pdf

1
debian/python3-welco.links vendored Normal file
View File

@ -0,0 +1 @@
usr/share/javascript/pdf usr/lib/python3/dist-packages/welco/sources/mail/static/pdf

10
debian/rules vendored
View File

@ -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
debian/settings.py vendored
View File

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

31
debian/uwsgi.ini vendored Normal file
View File

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

4
debian/welco-manage vendored
View File

@ -18,9 +18,9 @@ fi
if test $# -eq 0
then
python ${MANAGE} help
python3 ${MANAGE} help
exit 1
fi
python ${MANAGE} "$@"
python3 ${MANAGE} "$@"

2
debian/welco.cron.d vendored
View File

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

View File

@ -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
debian/welco.dirs vendored
View File

@ -1,5 +1,6 @@
/etc/welco
/usr/lib/welco
/var/lib/welco/collectstatic
/var/lib/welco/spooler
/var/lib/welco/tenants
/var/log/welco

48
debian/welco.init vendored
View File

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

View File

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

View File

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

16
debian/welco.service vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

183
tests/test_mail_manager.py Normal file
View File

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

208
tests/test_manager.py Normal file
View File

@ -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([{')

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,6 +16,7 @@
from django.apps import AppConfig
class KbAppConfig(AppConfig):
name = 'welco.kb'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 dun 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 ny 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 davis 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 dappels"
#~ msgid "Log In"
#~ msgstr "Connexion"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View File

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

View File

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

View File

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

View File

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

View File

@ -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'),
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -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'},
),
]

View File

@ -1,6 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -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',
]
)

View File

@ -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'),
]

View File

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

View File

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

View File

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

View File

@ -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')),
],

View File

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

View File

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

View File

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

View File

@ -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'),
]

View File

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

View File

@ -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 */
}

View File

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

View File

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