Compare commits

...

60 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
100 changed files with 955 additions and 959 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']

16
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
@ -23,10 +23,18 @@ pipeline {
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

37
debian/control vendored
View File

@ -2,35 +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,
python3-welco (= ${binary:Version}),
python3-hobo,
python3-django-tenant-schemas,
python3-psycopg2,
python3-django-mellon,
python3-uwsgidecorators,
python3-xstatic-select2,
uwsgi,
uwsgi-plugin-python,
uwsgi-plugin-python3,
graphicsmagick
Recommends: nginx
Suggests: postgresql
Breaks: python-welco (<< 0.73.post12)
Replaces: python-welco (<< 0.73.post12)
Description: Multichannel request processing

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

6
debian/settings.py vendored
View File

@ -16,15 +16,15 @@
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

7
debian/uwsgi.ini vendored
View File

@ -2,13 +2,17 @@
auto-procname = true
procname-prefix-spaced = welco
plugin = python
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
@ -18,6 +22,7 @@ buffer-size = 32768
py-tracebacker = /run/welco/py-tracebacker.sock.
stats = /run/welco/stats.sock
memory-report = true
ignore-sigpipe = true

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

1
debian/welco.init vendored
View File

@ -38,6 +38,7 @@ GROUP=$NAME
DAEMON_ARGS=${DAEMON_ARGS:-"--pidfile=$PIDFILE
--uid $USER --gid $GROUP
--ini /etc/$NAME/uwsgi.ini
--spooler /var/lib/$NAME/spooler/
--daemonize /var/log/uwsgi.$NAME.log"}
# Load the VERBOSE setting and other rcS variables

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

View File

@ -1,16 +1,18 @@
[Unit]
Description=Welco
After=network.target syslog.target postgresql.service
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 --verbosity 1
ExecStartPre=/usr/bin/welco-manage collectstatic --noinput
ExecStart=/usr/bin/uwsgi --ini /etc/%p/uwsgi.ini
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
@ -18,7 +20,6 @@ PrivateTmp=true
Restart=on-failure
RuntimeDirectory=welco
Type=notify
StandardError=syslog
NotifyAccess=all
[Install]

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.11,<2.3',
install_requires=[
'django>=2.2,<3.3',
'gadjo',
'django-ckeditor<4.5.4',
'django-haystack<2.8',
'django-reversion>=2.0,<3',
'django-taggit',
'djangorestframework>=3.3, <3.8',
'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

View File

@ -14,11 +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/>.
from unittest import mock
import httmock
import mock
import pytest
import requests
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
@ -29,14 +29,9 @@ 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')
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)
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'
@ -46,9 +41,8 @@ def test_post_contacts_zone_view(app, db):
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)
'/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'
@ -90,20 +84,24 @@ def test_search_json_view(settings, app, user, mail_group):
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
'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)
@ -123,6 +121,7 @@ def test_contact_detail_fragment_view(settings, app, db):
}
}
}
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
def response(url, request):
headers = {'content-type': 'application/json'}
@ -132,12 +131,9 @@ def test_contact_detail_fragment_view(settings, app, db):
'user_var_phone': '0123456789',
'user_var_mobile': '0612345789',
'user_id': '42',
'user_roles': [{
'name': 'Agent',
'text': 'Agent',
'slug': 'agent',
'id': '8d73434814484aa0b8555ac9c68a9300'
}],
'user_roles': [
{'name': 'Agent', 'text': 'Agent', 'slug': 'agent', 'id': '8d73434814484aa0b8555ac9c68a9300'}
],
}
return httmock.response(200, content, headers)
@ -148,15 +144,12 @@ def test_contact_detail_fragment_view(settings, app, db):
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')
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)
'/ajax/contacts/42/', params={'source_type': source_type, 'source_pk': mail.pk}, status=200
)
assert resp.html.find('h3').text == 'John Doe'
@ -183,7 +176,7 @@ def test_post_contact_add_view(mocked_sleep, settings, app, db):
'orig': 'http://welco.example.net/',
'secret': 'xxx',
}
}
},
}
# normal case
@ -210,7 +203,8 @@ def test_post_contact_add_view(mocked_sleep, settings, app, db):
'first_name': 'John',
'last_name': 'Doe',
},
status=200)
status=200,
)
assert resp.content_type == 'application/json'
assert resp.json['data']['user_id'] == '43'

View File

@ -15,7 +15,6 @@
# 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

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/>.
import httmock
import json
import requests
from webtest import Upload
import httmock
import requests
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
from django.utils.encoding import force_text
from django.utils.encoding import force_str
from webtest import Upload
from welco.sources.mail.models import Mail
@ -46,10 +46,7 @@ def test_get_feeder_view(app, user):
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)
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.'
@ -63,27 +60,28 @@ def test_qualification_save_view(settings, app, db):
}
}
}
mail = Mail.objects.create(
content=ContentFile('foo', name='bar.txt'),
subject='spam')
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)
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",
}]}
'err': 0,
'data': [
{
'title': 'Foo',
'slug': 'foo',
}
],
}
return httmock.response(200, content, headers)
with httmock.HTTMock(response_get):
@ -97,9 +95,7 @@ def test_edit_note_view(app, user):
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
mail = Mail.objects.create(
content=ContentFile('foo', name='bar.txt'),
note='spam')
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'
@ -123,7 +119,7 @@ def test_note_view(app, user):
def test_reject_view(settings, app, user):
settings.MAARCH_FEED= {
settings.MAARCH_FEED = {
'URL': 'http://maarch.example.net',
'ENABLE': True,
'USERNAME': 'xxx',
@ -134,15 +130,13 @@ def test_reject_view(settings, app, user):
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')
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_text(request.body)) == {'status': 'FOO', 'resId': ['42']}
assert json.loads(force_str(request.body)) == {'status': 'FOO', 'resId': ['42']}
headers = {'content-type': 'application/json'}
content = {"maarch_say": "ok"}
content = {'maarch_say': 'ok'}
return httmock.response(200, content, headers)
with httmock.HTTMock(response_ok):
@ -150,9 +144,7 @@ def test_reject_view(settings, app, user):
assert Mail.objects.count() == 0
# errors
mail = Mail.objects.create(
content=ContentFile('foo', name='bar.txt'),
external_id='maarch-42')
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):

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/>.
import httmock
import mock
import pytest
from unittest import mock
import httmock
import pytest
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
@ -112,15 +112,11 @@ 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)
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)
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({')
@ -130,8 +126,8 @@ 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)
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)
@ -154,8 +150,8 @@ def test_create_formdata_view(settings, 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)
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=')
@ -185,7 +181,8 @@ def test_create_formdata_view(settings, app, mail_group, user):
'data': {
'id': 42,
'backoffice_url': 'http://example.net',
}}
},
}
return httmock.response(200, content, headers)
with httmock.HTTMock(response_get, response_post):
@ -204,8 +201,7 @@ def test_menu_json_view(app, user, mail_group, phone_group, counter_group, kb_gr
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']
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'

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,14 +17,12 @@
import json
import pytest
from django.contrib.auth.models import User
from django.utils.encoding import force_text
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()
@ -51,7 +49,7 @@ class BaseMock(object):
class MaarchMock(BaseMock):
def list_endpoint(self, url, request):
self.requests.append(('list_endpoint', url, request, json.loads(force_text(request.body))))
self.requests.append(('list_endpoint', url, request, json.loads(force_str(request.body))))
return {
'content': json.dumps(self.next_response()),
'headers': {
@ -59,15 +57,17 @@ class MaarchMock(BaseMock):
},
'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(force_text(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(force_text(request.body))))
self.requests.append(('update_status', url, request, json.loads(force_str(request.body))))
return {
'content': json.dumps(self.next_response()),
'headers': {
@ -75,10 +75,12 @@ class MaarchMock(BaseMock):
},
'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(force_text(request.body))))
self.requests.append(('post_courrier', url, request, json.loads(force_str(request.body))))
post_courrier.path = '^/rest/res$'
@ -96,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$'
@ -160,8 +175,10 @@ 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)
@ -172,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': force_text(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
@ -214,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
@ -243,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
@ -260,24 +285,24 @@ def test_feed(settings, app, maarch, wcs, user):
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,11 +18,9 @@ import json
import re
import pytest
from django.urls import reverse
from django.test import override_settings
from django.utils import six
from django.utils.encoding import force_text
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
@ -38,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 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 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 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 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):
@ -136,10 +143,11 @@ 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 response.json() == {'err': 0}
@ -156,10 +164,10 @@ def test_current_calls(user, client):
assert response['content-type'] == 'application/json'
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)
@ -167,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'], six.string_types)
assert isinstance(call['callee'], six.string_types)
assert isinstance(call['start'], six.string_types)
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, six.string_types)]) == 5
assert len([call for call in data['all-lines'] if isinstance(call, six.string_types)]) == 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):
@ -184,9 +192,9 @@ def test_current_calls(user, client):
assert response['content-type'] == 'application/json'
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
@ -199,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 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 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 force_text(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 force_text(response.content)
assert '<li>102' in force_text(response.content)
assert 'data-callee="102"' in force_text(response.content)
currents = re.search('<div id="source-mainarea" '
'data-current-calls="/api/phone/current-calls/">'
'(.*?)</div>', force_text(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 force_text(response.content)
assert '<h1>Current Call: <strong>003369999999</strong></h1>' in force_str(response.content)
# simulate a mellon user
session = client.session
@ -250,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 force_text(response.content)
assert 'data-callee="agent007"' not in force_text(response.content)
assert '<li>102' in force_text(response.content)
assert 'data-callee="102"' in force_text(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 force_text(response.content)
assert 'agent007' in force_text(response.content)
assert 'data-callee="agent007"' in force_text(response.content)
assert '<li>102' in force_text(response.content)
assert 'data-callee="102"' in force_text(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
@ -284,15 +290,14 @@ def test_call_expiration(user, client):
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 = 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):
@ -300,7 +305,7 @@ def test_call_expiration(user, client):
assert response.status_code == 200
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

22
tox.ini
View File

@ -1,5 +1,7 @@
[tox]
envlist = py27-django111-coverage-pylint,py3-django111,py3-django22
envlist =
py3-django32-drf314
py3-django32-black-coverage-pylint-drf312
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/welco/{env:BRANCH_NAME:}
[testenv]
@ -8,25 +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
django22: django>=2.2,<2.3
django32: django>=3.2,<3.3
pytest-cov
pytest-django
pytest!=5.3.3
attrs<19.2
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 =
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()
@ -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.urls 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,13 +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 django.utils.html import strip_tags
from django.utils.six.moves.html_parser 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)
@ -32,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.urls 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.urls 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(

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,13 +14,13 @@
# 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.urls 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):
@ -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',
@ -61,7 +59,6 @@ MIDDLEWARE = (
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'reversion.middleware.RevisionMiddleware',
)
ROOT_URLCONF = 'welco.urls'
@ -92,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/
@ -101,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/'
@ -133,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',
},
}
@ -192,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
@ -202,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
@ -216,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):
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

@ -15,12 +15,10 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
from dateutil.parser import parse as parse_datetime
from django.utils import six
from django.utils.six.moves.urllib import parse as urlparse
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
@ -29,7 +27,7 @@ class MaarchError(Exception):
pass
class MaarchCourrier(object):
class MaarchCourrier:
url = None
username = None
password = None
@ -44,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
@ -89,7 +87,7 @@ class MaarchCourrier(object):
data = {key: self.__dict__[key] for key in self.__dict__ if key not in excluded_keys}
if data:
for key, value in data.items():
if isinstance(value, six.string_types):
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'})
@ -125,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)
@ -153,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:
@ -175,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.urls 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.urls 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.urls 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,20 +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.utils import six
from django.utils.encoding import force_text
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):
@ -57,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(force_text(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'], six.string_types), 'caller must be a string'
assert isinstance(payload['callee'], six.string_types), '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) as e:
return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
force_text(e)}),
content_type='application/json')
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 = {
@ -129,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)
@ -177,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')
@ -192,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(force_text(request.body))
payload = json.loads(force_str(request.body))
assert isinstance(payload, dict), 'payload is not a JSON object'
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_text(e)}),
content_type='application/json')
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(force_text(request.body))
payload = json.loads(force_str(request.body))
assert isinstance(payload, dict), 'payload is not a JSON object'
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_text(e)}),
content_type='application/json')
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

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

View File

@ -16,25 +16,26 @@
import base64
import datetime
import hmac
import hashlib
import hmac
import json
import random
import re
import requests
import urllib.parse
import requests
from django.conf import settings
from django.core.cache import cache
from django.http import HttpResponse, HttpResponseBadRequest
from django.utils.encoding import smart_bytes
from django.utils.http import urlencode, quote
from django.utils.six.moves.urllib import parse as urlparse
from django.utils.http import quote, urlencode
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None):
parsed = urlparse.urlparse(url)
parsed = urllib.parse.urlparse(url)
new_query = sign_query(parsed.query, key, algo, timestamp, nonce)
return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])
return urllib.parse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])
def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
if timestamp is None:
@ -45,22 +46,22 @@ def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
new_query = query
if new_query:
new_query += '&'
new_query += urlencode((
('algo', algo),
('timestamp', timestamp),
('nonce', nonce)))
new_query += urlencode((('algo', algo), ('timestamp', timestamp), ('nonce', nonce)))
signature = base64.b64encode(sign_string(new_query, key, algo=algo))
new_query += '&signature=' + quote(signature)
return new_query
def sign_string(s, key, algo='sha256', timedelta=30):
digestmod = getattr(hashlib, algo)
hash = hmac.HMAC(smart_bytes(key), digestmod=digestmod, msg=smart_bytes(s))
return hash.digest()
def get_wcs_services():
return settings.KNOWN_SERVICES.get('wcs')
def get_wcs_json(wcs_url, path, wcs_site, params={}):
if not wcs_url.endswith('/'):
wcs_url += '/'
@ -70,13 +71,13 @@ def get_wcs_json(wcs_url, path, wcs_site, params={}):
response_json = cache.get(url)
if response_json is None:
signed_url = sign_url(url, wcs_site.get('secret'))
response_json = requests.get(signed_url, headers={'accept': 'application/json'},
timeout=10).json()
response_json = requests.get(signed_url, headers={'accept': 'application/json'}, timeout=10).json()
if not isinstance(response_json, dict):
response_json = {'data': response_json}
cache.set(url, response_json)
return response_json
def get_wcs_options(url, condition=None, params={}):
categories = {}
for wcs_key, wcs_site in get_wcs_services().items():
@ -103,6 +104,7 @@ def get_wcs_options(url, condition=None, params={}):
options.append((category, sorted(categories[category], key=lambda x: x[1])))
return options
def get_wcs_formdef_details(formdef_reference):
wcs_key, form_slug = formdef_reference.split(':')
wcs_site = get_wcs_services()[wcs_key]
@ -113,6 +115,7 @@ def get_wcs_formdef_details(formdef_reference):
return form
return None
def push_wcs_formdata(request, formdef_reference, context=None):
wcs_key, form_slug = formdef_reference.split(':')
wcs_site = get_wcs_services()[wcs_key]
@ -121,7 +124,7 @@ def push_wcs_formdata(request, formdef_reference, context=None):
wcs_site_url += '/'
url = wcs_site_url + 'api/formdefs/%s/schema' % form_slug
response = requests.get(url)
create_draft = not(bool('welco-direct' in (response.json().get('keywords') or '')))
create_draft = not (bool('welco-direct' in (response.json().get('keywords') or '')))
url = wcs_site_url + 'api/formdefs/%s/submit?' % form_slug
data = {
@ -139,8 +142,7 @@ def push_wcs_formdata(request, formdef_reference, context=None):
url = sign_url(url, wcs_site.get('secret'))
response = requests.post(url, data=json.dumps(data),
headers={'Content-type': 'application/json'})
response = requests.post(url, data=json.dumps(data), headers={'Content-type': 'application/json'})
if response.json().get('err') != 0:
raise Exception('error %r' % response.content)
data = response.json()['data']
@ -169,6 +171,7 @@ def get_wcs_data(endpoint, params=None):
json_response = {'data': json_response}
return json_response
def response_for_json(request, data):
json_str = json.dumps(data)
for variable in ('jsonpCallback', 'callback'):

View File

@ -16,20 +16,20 @@
import json
from django import template
from django.conf import settings
from django.contrib.auth import logout as auth_logout
from django.contrib.auth import views as auth_views
from django.contrib.auth.decorators import login_required
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied
from django.urls import reverse
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import resolve_url
from django import template
from django.template import RequestContext
from django.utils.encoding import force_text
from django.urls import reverse
from django.utils.encoding import force_str
from django.utils.http import quote
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
@ -38,13 +38,14 @@ if 'mellon' in settings.INSTALLED_APPS:
else:
get_idps = lambda: []
from .sources.mail.views import Home as MailHome
from .sources.phone.views import Home as PhoneHome
from .sources.counter.views import Home as CounterHome
from .qualif.models import Association
from .kb.views import HomeZone as KbHomeZone, check_user_perms as check_kb_user_perms
from .contacts.views import HomeZone as ContactsHomeZone
from .forms import QualificationForm
from .kb.views import HomeZone as KbHomeZone
from .kb.views import check_user_perms as check_kb_user_perms
from .qualif.models import Association
from .sources.counter.views import Home as CounterHome
from .sources.mail.views import Home as MailHome
from .sources.phone.views import Home as PhoneHome
class LoginView(auth_views.LoginView):
@ -59,7 +60,7 @@ class LoginView(auth_views.LoginView):
except KeyError:
return HttpResponseBadRequest('invalid value for "next" parameter')
return HttpResponseRedirect(resolve_url('mellon_login') + '?next=' + quoted_next_url)
return super(LoginView, self).dispatch(request, *args, **kwargs)
return super().dispatch(request, *args, **kwargs)
login = LoginView.as_view()
@ -80,7 +81,7 @@ class Qualification(TemplateView):
template_name = 'welco/qualification_no_validation.html'
def get_context_data(self, **kwargs):
context = super(Qualification, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['form'] = QualificationForm(self.request.user)
context['source_type'] = self.request.GET['source_type']
source_type = ContentType.objects.get(id=self.request.GET['source_type'])
@ -88,19 +89,22 @@ class Qualification(TemplateView):
context['source_pk'] = self.request.GET['source_pk']
if self.request.GET.get('source_pk'):
context['associations'] = Association.objects.filter(
source_type=ContentType.objects.get(id=self.request.GET['source_type']),
source_pk=self.request.GET['source_pk']).order_by('id')
source_type=ContentType.objects.get(id=self.request.GET['source_type']),
source_pk=self.request.GET['source_pk'],
).order_by('id')
return context
def post(self, request, *args, **kwargs):
association = Association(
source_type=ContentType.objects.get(id=request.POST['source_type']),
source_pk=request.POST['source_pk'])
source_type=ContentType.objects.get(id=request.POST['source_type']),
source_pk=request.POST['source_pk'],
)
association.formdef_reference = request.POST['formdef_reference']
association.save()
request.GET = request.POST
return self.get(request)
qualification = csrf_exempt(Qualification.as_view())
@ -109,22 +113,21 @@ class ChannelHome(TemplateView):
source_klass = MailHome
def check_user_ok(self):
user_groups = set([x.name for x in self.request.user.groups.all()])
user_groups = {x.name for x in self.request.user.groups.all()}
channel_groups = set(settings.CHANNEL_ROLES[self.source_klass.source_key])
return user_groups.intersection(channel_groups)
def get_context_data(self, **kwargs):
if not self.check_user_ok():
raise PermissionDenied()
context = super(ChannelHome, self).get_context_data(**kwargs)
context['panels'] = [
{'key': x, 'zone_url': x + '-zone'} for x in settings.SCREEN_PANELS]
context = super().get_context_data(**kwargs)
context['panels'] = [{'key': x, 'zone_url': x + '-zone'} for x in settings.SCREEN_PANELS]
context['source'] = self.source_klass(self.request, **kwargs)
context['kb'] = KbHomeZone(self.request)
context['contacts'] = ContactsHomeZone(self.request)
context['channels'] = []
user_groups = set([x.name for x in self.request.user.groups.all()])
user_groups = {x.name for x in self.request.user.groups.all()}
for channel in settings.CHANNEL_ROLES:
channel_groups = set(settings.CHANNEL_ROLES[channel])
if user_groups.intersection(channel_groups):
@ -134,27 +137,32 @@ class ChannelHome(TemplateView):
@login_required
def home(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):
return HttpResponseRedirect('%s/' % channel)
raise PermissionDenied()
class HomePhone(ChannelHome):
source_klass = PhoneHome
home_phone = login_required(HomePhone.as_view())
class HomeMail(ChannelHome):
source_klass = MailHome
home_mail = login_required(HomeMail.as_view())
class HomeCounter(ChannelHome):
source_klass = CounterHome
home_counter = login_required(HomeCounter.as_view())
@ -174,11 +182,13 @@ def wcs_summary(request, *args, **kwargs):
break
return HttpResponse(json_str, content_type='application/javascript')
@login_required
def remove_association(request, *args, **kwargs):
Association.objects.filter(id=kwargs.get('pk')).delete()
return HttpResponseRedirect(resolve_url('home'))
@login_required
@csrf_exempt
def create_formdata(request, *args, **kwargs):
@ -196,10 +206,11 @@ def create_formdata(request, *args, **kwargs):
json.dump({'result': 'ok', 'url': qualif.formdata_url}, response)
return response
@login_required
def menu_json(request):
response = HttpResponse(content_type='application/json')
user_groups = set([x.name for x in request.user.groups.all()])
user_groups = {x.name for x in request.user.groups.all()}
menu = []
labels = {
'mail': _('Mails'),
@ -209,17 +220,21 @@ def menu_json(request):
for channel in settings.CHANNEL_ROLES:
channel_groups = set(settings.CHANNEL_ROLES[channel])
if user_groups.intersection(channel_groups):
menu.append({
'label': force_text(labels.get(channel)),
'slug': channel,
'url': request.build_absolute_uri(reverse('home-%s' % channel)),
})
menu.append(
{
'label': force_str(labels.get(channel)),
'slug': channel,
'url': request.build_absolute_uri(reverse('home-%s' % channel)),
}
)
if check_kb_user_perms(request.user, access=True):
menu.append({
'label': force_text(_('Knowledge Base')),
'slug': 'book',
'url': request.build_absolute_uri(reverse('kb-home'))
})
menu.append(
{
'label': force_str(_('Knowledge Base')),
'slug': 'book',
'url': request.build_absolute_uri(reverse('kb-home')),
}
)
json_str = json.dumps(menu)
for variable in ('jsonpCallback', 'callback'):
if variable in request.GET:

View File

@ -8,7 +8,9 @@ https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "welco.settings")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'welco.settings')
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()