Compare commits

..

1 Commits

Author SHA1 Message Date
Benjamin Dauvergne d7231c9002 Jenkinsfile: use mergeJunitResults() (#38317) 2019-12-09 15:42:08 +01:00
103 changed files with 963 additions and 1704 deletions

View File

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

View File

@ -1,22 +0,0 @@
# 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@main') import eo.Utils
@Library('eo-jenkins-lib@master') import eo.Utils
pipeline {
agent any
@ -23,18 +23,10 @@ pipeline {
stage('Packaging') {
steps {
script {
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}"
if (env.JOB_NAME == 'welco' && env.GIT_BRANCH == 'origin/master') {
sh 'sudo -H -u eobuilder /usr/local/bin/eobuilder welco'
} else if (env.GIT_BRANCH.startsWith('hotfix/')) {
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d bullseye,bookworm --branch ${env.GIT_BRANCH} --hotfix ${SHORT_JOB_NAME}"
sh "sudo -H -u eobuilder /usr/local/bin/eobuilder -d stretch --branch ${env.GIT_BRANCH} --hotfix welco"
}
}
}

21
README
View File

@ -36,27 +36,6 @@ 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 Normal file
View File

@ -0,0 +1 @@
9

38
debian/control vendored
View File

@ -2,36 +2,34 @@ Source: welco
Maintainer: Frederic Peters <fpters@entrouvert.com>
Section: python
Priority: optional
Build-Depends: python3-setuptools, python3-all, debhelper-compat (= 12), python3-django, dh-python
Build-Depends: python-setuptools (>= 0.6b3), python-all (>= 2.6.6-3), debhelper (>= 9), python-django, dh-python, dh-systemd
Standards-Version: 3.9.6
X-Python-Version: >= 2.7
Package: python3-welco
Package: python-welco
Architecture: all
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),
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),
libjs-pdf (<< 1.1)
Recommends: python3-django-mellon
Recommends: python-django-mellon
Description: Multichannel request processing (Python module)
Package: welco
Architecture: all
Depends: ${misc:Depends},
python3-welco (= ${binary:Version}),
python3-hobo,
python3-django-tenant-schemas,
python3-psycopg2,
python3-django-mellon,
python3-uwsgidecorators,
python3-xstatic-select2,
uwsgi,
uwsgi-plugin-python3,
python-welco (= ${binary:Version}),
python-hobo,
python-django-tenant-schemas,
python-psycopg2,
python-django-mellon,
python-xstatic-select2,
gunicorn,
graphicsmagick
Recommends: nginx
Suggests: postgresql
Breaks: python-welco (<< 0.73.post12)
Replaces: python-welco (<< 0.73.post12)
Description: Multichannel request processing

View File

@ -1,4 +1,4 @@
# This file is sourced by "exec(open(..." from welco.settings
# This file is sourced by "execfile" from welco.settings
import os
@ -10,12 +10,12 @@ INSTALLED_APPS += ('mellon',)
#
# hobotization (multitenant)
#
exec(open('/usr/lib/hobo/debian_config_common.py').read())
execfile('/usr/lib/hobo/debian_config_common.py')
#
# local settings
#
exec(open(os.path.join(ETC_DIR, 'settings.py')).read())
execfile(os.path.join(ETC_DIR, 'settings.py'))
# run additional settings snippets
exec(open('/usr/lib/hobo/debian_config_settings_d.py').read())
execfile('/usr/lib/hobo/debian_config_settings_d.py')

View File

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

1
debian/pydist-overrides vendored Normal file
View File

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

1
debian/python-welco.dirs vendored Normal file
View File

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

2
debian/python-welco.install vendored Normal file
View File

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

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

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

View File

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

10
debian/rules vendored
View File

@ -1,12 +1,8 @@
#!/usr/bin/make -f
# -*- makefile -*-
export PYBUILD_NAME=welco
export PYBUILD_DISABLE=test
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
%:
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
dh $@ --with python2,systemd

9
debian/settings.py vendored
View File

@ -9,22 +9,21 @@
# WARNING! Quick-start development settings unsuitable for production!
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# This file is sourced by "exec(open(...).read())" from
# /usr/lib/welco/debian_config.py
# This file is sourced by "execfile" from /usr/lib/welco/debian_config.py
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
TEMPLATE_DEBUG = False
# ADMINS = (
#ADMINS = (
# # ('User 1', 'watchdog@example.net'),
# # ('User 2', 'janitor@example.net'),
# )
#)
# ALLOWED_HOSTS must be correct in production!
# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts
ALLOWED_HOSTS = [
'*',
'*',
]
# Databases

31
debian/uwsgi.ini vendored
View File

@ -1,31 +0,0 @@
[uwsgi]
auto-procname = true
procname-prefix-spaced = welco
plugin = python3
module = welco.wsgi:application
http-socket = /run/welco/welco.sock
chmod-socket = 666
vacuum = true
spooler-processes = 3
spooler-python-import = hobo.provisionning.spooler
spooler-max-tasks = 20
master = true
processes = 5
harakiri = 120
enable-threads = true
buffer-size = 32768
py-tracebacker = /run/welco/py-tracebacker.sock.
stats = /run/welco/stats.sock
memory-report = true
ignore-sigpipe = true
if-file = /etc/welco/uwsgi-local.ini
include = /etc/welco/uwsgi-local.ini
endif =

4
debian/welco-manage vendored
View File

@ -18,9 +18,9 @@ fi
if test $# -eq 0
then
python3 ${MANAGE} help
python ${MANAGE} help
exit 1
fi
python3 ${MANAGE} "$@"
python ${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 -v0
*/10 6-22 * * * welco /usr/bin/welco-manage tenant_command feed_mail_maarch --all-tenants

View File

@ -1,3 +1,3 @@
#!/bin/sh
/sbin/runuser -u welco /usr/bin/welco-manage -- tenant_command clearsessions --all-tenants -v0
/sbin/runuser -u welco /usr/bin/welco-manage -- tenant_command clearsessions --all-tenants

1
debian/welco.dirs vendored
View File

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

48
debian/welco.init vendored
View File

@ -16,12 +16,14 @@
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Multichannel request processing"
NAME=welco
DAEMON=/usr/bin/uwsgi
DAEMON=/usr/bin/gunicorn
RUN_DIR=/run/$NAME
PIDFILE=$RUN_DIR/$NAME.pid
LOG_DIR=/var/log/$NAME
SCRIPTNAME=/etc/init.d/$NAME
BIND=unix:$RUN_DIR/$NAME.sock
WORKERS=5
TIMEOUT=30
WELCO_SETTINGS_FILE=/usr/lib/$NAME/debian_config.py
MANAGE_SCRIPT="/usr/bin/$NAME-manage"
@ -35,11 +37,17 @@ GROUP=$NAME
# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME
DAEMON_ARGS=${DAEMON_ARGS:-"--pidfile=$PIDFILE
--uid $USER --gid $GROUP
--ini /etc/$NAME/uwsgi.ini
--spooler /var/lib/$NAME/spooler/
--daemonize /var/log/uwsgi.$NAME.log"}
DAEMON_ARGS=${DAEMON_ARGS:-"--pid $PIDFILE \
--user $USER --group $GROUP \
--daemon \
--access-logfile $LOG_DIR/gunicorn-access.log \
--log-file $LOG_DIR/gunicorn-error.log \
--bind=$BIND \
--workers=$WORKERS \
--worker-class=sync \
--timeout=$TIMEOUT \
--name $NAME \
$NAME.wsgi:application"}
# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh
@ -65,7 +73,9 @@ do_start()
# 0 if daemon has been started
# 1 if daemon was already running
# 2 if daemon could not be started
start-stop-daemon --start --quiet --user $USER --exec $DAEMON -- \
start-stop-daemon --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
|| return 1
start-stop-daemon --start --quiet --exec $DAEMON -- \
$DAEMON_ARGS \
|| return 2
}
@ -80,16 +90,32 @@ do_stop()
# 1 if daemon was already stopped
# 2 if daemon could not be stopped
# other if a failure occurred
$DAEMON --stop $PIDFILE
start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE
RETVAL="$?"
[ "$RETVAL" = 2 ] && return 2
# Wait for children to finish too if this is a daemon that forks
# and if the daemon is only ever run from this initscript.
# If the above conditions are not satisfied then add some other code
# that waits for the process to drop all resources that could be
# needed by services started subsequently. A last resort is to
# sleep for some time.
start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
[ "$?" = 2 ] && return 2
# Many daemons don't delete their pidfiles when they exit.
rm -f $PIDFILE
return 0 # hopefully
return "$RETVAL"
}
#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
$DAEMON --reload $PIDFILE
#
# If the daemon can reload its configuration without
# restarting (for example, when it is sent a SIGHUP),
# then implement that here.
#
start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name `basename $DAEMON`
return 0
}
@ -125,7 +151,7 @@ case "$1" in
esac
;;
status)
status_of_proc -p $PIDFILE "$DAEMON" "$NAME" && exit 0 || exit $?
status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
;;
reload|force-reload)
#

View File

@ -1,5 +1,4 @@
debian/welco-manage /usr/bin
debian/settings.py /etc/welco
debian/uwsgi.ini /etc/welco
debian/debian_config.py /usr/lib/welco
debian/welco.service /lib/systemd/system

View File

@ -20,7 +20,6 @@ case "$1" in
# ensure dirs ownership
chown $USER:$GROUP /var/log/$NAME
chown $USER:$GROUP /var/lib/$NAME/collectstatic
chown $USER:$GROUP /var/lib/$NAME/spooler
chown $USER:$GROUP /var/lib/$NAME/tenants
# create a secret file
SECRET_FILE=$CONFIG_DIR/secret

16
debian/welco.service vendored
View File

@ -4,23 +4,23 @@ After=network.target postgresql.service
Wants=postgresql.service
[Service]
SyslogIdentifier=uwsgi/%p
Environment=WELCO_SETTINGS_FILE=/usr/lib/%p/debian_config.py
Environment=LANG=C.UTF-8
User=%p
Group=%p
ExecStartPre=/usr/bin/welco-manage migrate_schemas --noinput --verbosity 1
ExecStartPre=/usr/bin/welco-manage migrate_schemas --noinput
ExecStartPre=/usr/bin/welco-manage collectstatic --noinput
ExecStartPre=/bin/mkdir -p /var/lib/welco/spooler/%m/
ExecStart=/usr/bin/uwsgi --ini /etc/%p/uwsgi.ini --spooler /var/lib/welco/spooler/%m/
ExecStart=/usr/bin/gunicorn \
--bind unix:/run/%p/%p.sock \
--worker-class=sync \
--workers 5 \
--timeout=30 \
--name %p \
%p.wsgi:application
ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGQUIT
TimeoutStartSec=0
PrivateTmp=true
Restart=on-failure
RuntimeDirectory=welco
Type=notify
NotifyAccess=all
[Install]
WantedBy=multi-user.target

View File

@ -2,8 +2,8 @@
import os
import sys
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'welco.settings')
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "welco.settings")
from django.core.management import execute_from_command_line

View File

@ -3,4 +3,5 @@ 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,14 +1,15 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
import os
import subprocess
import sys
from distutils.cmd import Command
from setuptools.command.install_lib import install_lib as _install_lib
from distutils.command.build import build as _build
from distutils.command.sdist import sdist
from setuptools import find_packages, setup
from setuptools.command.install_lib import install_lib as _install_lib
from distutils.cmd import Command
from setuptools import setup, find_packages
class eo_sdist(sdist):
@ -24,18 +25,16 @@ 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') as v:
with open('VERSION', 'r') 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
@ -46,7 +45,9 @@ 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'
@ -64,7 +65,6 @@ 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,19 +106,20 @@ 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>=2.2,<3.3',
install_requires=['django>=1.8,<1.12',
'gadjo',
'django-ckeditor<4.5.4',
'django-haystack<3.2.2',
'django-taggit<3.1.1',
'djangorestframework>=3.3,<3.15',
'django-ckeditor<=4.5.3',
'django-haystack<2.8',
'django-reversion>=2.0',
'django-taggit',
'djangorestframework>=3.3, <3.7',
'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 django_webtest
import pytest
import django_webtest
@pytest.fixture
@ -51,47 +51,3 @@ def mail_group(db, settings, user):
mail_roles = channel_roles.setdefault('mail', [])
mail_roles.append('mail')
return group
@pytest.fixture
def phone_group(db, settings, user):
from django.contrib.auth.models import Group
# add mail group to default user
group = Group.objects.create(name='phone')
user.groups.add(group)
# define authorization of phone group on phone channel
channel_roles = getattr(settings, 'CHANNEL_ROLES', {})
phone_roles = channel_roles.setdefault('phone', [])
phone_roles.append('phone')
return group
@pytest.fixture
def counter_group(db, settings, user):
from django.contrib.auth.models import Group
# add mail group to default user
group = Group.objects.create(name='counter')
user.groups.add(group)
# define authorization of counter group on counter channel
channel_roles = getattr(settings, 'CHANNEL_ROLES', {})
counter_roles = channel_roles.setdefault('counter', [])
counter_roles.append('counter')
return group
@pytest.fixture
def kb_group(db, settings, user):
from django.contrib.auth.models import Group
# add mail group to default user
group = Group.objects.create(name='kb')
user.groups.add(group)
# define authorization of kb group to manage then knowledge base
kb_manage_roles = getattr(settings, 'KB_MANAGE_ROLES', [])
kb_manage_roles.append('kb')
return group

View File

@ -1,229 +0,0 @@
# welco - multichannel request processing
# Copyright (C) 2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from unittest import mock
import httmock
import pytest
import requests
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
from welco.sources.mail.models import Mail
def test_get_contacts_zone_view(app, db):
resp = app.get('/ajax/contacts', status=200)
assert resp.html.find('button')['data-url'] == '/contacts/add/'
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), contact_id='42')
source_type = ContentType.objects.get_for_model(Mail).pk
resp = app.get('/ajax/contacts', params={'source_type': source_type, 'source_pk': mail.pk}, status=200)
assert resp.html.find('a').text == '...'
assert resp.html.find('a')['data-page-slug'] == '42'
def test_post_contacts_zone_view(app, db):
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
assert not mail.contact_id
source_type = ContentType.objects.get_for_model(Mail).pk
resp = app.post(
'/ajax/contacts', params={'source_type': source_type, 'source_pk': mail.pk, 'user_id': 42}, status=200
)
assert resp.text == 'ok'
assert Mail.objects.get(id=mail.pk).contact_id == '42'
def test_search_json_view_without_channel(app):
app.get('/contacts/search/json/', status=403)
def test_search_json_view_without_query(app, user, mail_group):
app.set_user(user.username)
resp = app.get('/contacts/search/json/', status=200)
assert resp.content_type == 'application/json'
assert resp.json == {'data': []}
def test_search_json_view(settings, app, user, mail_group):
settings.KNOWN_SERVICES = {
'wcs': {
'demarches': {
'url': 'http://wcs.example.net/',
'orig': 'http://welco.example.net/',
'secret': 'xxx',
}
}
}
app.set_user(user.username)
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/', method='GET')
def response(url, request):
headers = {'content-type': 'application/json'}
content = {'err': 1, 'msg': 'oups'}
return httmock.response(200, content, headers)
with httmock.HTTMock(response):
with pytest.raises(Exception, match='oups'):
resp = app.get('/contacts/search/json/', params={'q': 'Doe'})
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/', method='GET')
def response(url, request):
headers = {'content-type': 'application/json'}
content = {
'data': [
{
'user_display_name': 'John Doe',
'user_email': 'john@example.net',
'user_var_phone': '0123456789',
'user_var_mobile': '0612345789',
'user_id': '42',
'user_roles': [
{
'name': 'Agent',
'text': 'Agent',
'slug': 'agent',
'id': '8d73434814484aa0b8555ac9c68a9300',
}
],
}
],
'err': 0,
}
return httmock.response(200, content, headers)
with httmock.HTTMock(response):
resp = app.get('/contacts/search/json/', params={'q': 'Doe'}, status=200)
assert resp.content_type == 'application/json'
assert resp.json['data'][0]['user_display_name'] == 'John Doe'
def test_contact_detail_fragment_view(settings, app, db):
settings.KNOWN_SERVICES = {
'wcs': {
'demarches': {
'url': 'http://wcs.example.net/',
'orig': 'http://welco.example.net/',
'secret': 'xxx',
}
}
}
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
def response(url, request):
headers = {'content-type': 'application/json'}
content = {
'user_display_name': 'John Doe',
'user_email': 'john@example.net',
'user_var_phone': '0123456789',
'user_var_mobile': '0612345789',
'user_id': '42',
'user_roles': [
{'name': 'Agent', 'text': 'Agent', 'slug': 'agent', 'id': '8d73434814484aa0b8555ac9c68a9300'}
],
}
return httmock.response(200, content, headers)
with httmock.HTTMock(response):
resp = app.get('/ajax/contacts/42/', status=200)
assert resp.html.find('h3').text == 'John Doe'
assert resp.html.find('p').text == 'Agent'
assert resp.html.find('li').text == 'Phone: 0123456789'
# unused 'is_pinned_user' context
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), contact_id='42')
source_type = ContentType.objects.get_for_model(Mail).pk
with httmock.HTTMock(response):
resp = app.get(
'/ajax/contacts/42/', params={'source_type': source_type, 'source_pk': mail.pk}, status=200
)
assert resp.html.find('h3').text == 'John Doe'
def test_get_contact_add_view(app):
resp = app.get('/contacts/add/', status=200)
assert resp.html.find('select')['name'] == 'title'
assert resp.html.find('input', {'id': 'id_first_name'})['name'] == 'first_name'
@mock.patch('welco.contacts.views.time.sleep')
def test_post_contact_add_view(mocked_sleep, settings, app, db):
settings.CONTACT_SEND_REGISTRATION_EMAIL = True
settings.KNOWN_SERVICES = {
'authentic': {
'connexion': {
'url': 'http://authentic.example.net/',
'orig': 'http://welco.example.net/',
'secret': 'xxx',
}
},
'wcs': {
'demarches': {
'url': 'http://wcs.example.net/',
'orig': 'http://welco.example.net/',
'secret': 'xxx',
}
},
}
# normal case
@httmock.urlmatch(netloc='authentic.example.net', path='/api/users/', method='POST')
def authentic_response(url, request):
headers = {'content-type': 'application/json'}
content = {'uuid': '42'}
return httmock.response(200, content, headers)
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
def wcs_response(url, request):
headers = {'content-type': 'application/json'}
content = {
'user_display_name': 'John Doe',
'id': '43',
}
return httmock.response(200, content, headers)
with httmock.HTTMock(authentic_response, wcs_response):
resp = app.post(
'/contacts/add/',
params={
'title': 'Mr',
'first_name': 'John',
'last_name': 'Doe',
},
status=200,
)
assert resp.content_type == 'application/json'
assert resp.json['data']['user_id'] == '43'
# timeout
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
def wcs_no_response(url, request):
return httmock.response(404)
with httmock.HTTMock(authentic_response, wcs_no_response):
resp = app.post('/contacts/add/', status=200)
assert resp.content_type == 'application/json'
assert resp.json['err'] == 1
assert resp.json['data'] == 'timeout when calling wcs'
# error
@httmock.urlmatch(netloc='wcs.example.net', path='/api/users/42/', method='GET')
def wcs_no_response(url, request):
return httmock.response(500)
with httmock.HTTMock(authentic_response, wcs_no_response):
with pytest.raises(requests.HTTPError):
resp = app.post('/contacts/add/', status=200)

View File

@ -1,37 +0,0 @@
# welco - multichannel request processing
# Copyright (C) 2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import pytest
from django.core.management import call_command
from django.core.management.base import CommandError
from welco.sources.mail.models import Mail
def test_feed_mail_command(settings, tmpdir, db):
settings.MEDIA_ROOT = str(tmpdir)
path1 = '%s/mail1.txt' % str(tmpdir)
path2 = '%s/mail2.txt' % str(tmpdir)
path3 = '%s/mail3.txt' % str(tmpdir)
open(path2, 'w').write('not a PDF')
open(path3, 'w').write('%PDF- is a PDF')
call_command('feed_mail', '--category', 'foobar', path1, path2, path3)
Mail.objects.count() == 1
Mail.objects.all()[0].scanner_category == 'foobar'
with pytest.raises(CommandError, match='nothing got imported'):
call_command('feed_mail', '--category', path1, path2)

View File

@ -1,183 +0,0 @@
# welco - multichannel request processing
# Copyright (C) 2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import json
import httmock
import requests
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
from django.utils.encoding import force_str
from webtest import Upload
from welco.sources.mail.models import Mail
def test_viewer_view(app):
resp = app.get('/mail/viewer/', status=302)
assert resp.location == '?file='
resp = resp.follow()
assert resp.html.find('title').text == 'PDF.js viewer'
resp = app.get('/mail/viewer/', {'file': 'tests/test.pdf'}, status=200)
assert resp.html.find('title').text == 'PDF.js viewer'
def test_get_feeder_view(app, user):
resp = app.get('/mail/feeder/', status=302)
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
resp = app.get('/mail/feeder/', status=200)
assert resp.html.find('h2').text == 'Mail Feeder'
def test_post_feeder_view(app, user):
app.set_user(user.username)
resp = app.post('/mail/feeder/', params={'mail': Upload('filename.txt', b'contents')}, status=302)
assert resp.location == '/mail/feeder/'
resp = resp.follow()
assert resp.html.find('li', {'class': 'info'}).text == '1 files uploaded successfully.'
def test_qualification_save_view(settings, app, db):
settings.KNOWN_SERVICES = {
'wcs': {
'demarches': {
'url': 'http://wcs.example.net/',
}
}
}
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), subject='spam')
assert not mail.contact_id
source_type = ContentType.objects.get_for_model(Mail).pk
resp = app.post(
'/ajax/qualification-mail-save',
params={'source_type': source_type, 'source_pk': mail.pk, 'subject': 'eggs'},
status=302,
)
assert resp.location == '/ajax/qualification?source_type=%s&source_pk=%s' % (source_type, mail.pk)
@httmock.urlmatch(netloc='wcs.example.net', path='/api/formdefs/', method='GET')
def response_get(url, request):
headers = {'content-type': 'application/json'}
content = {
'err': 0,
'data': [
{
'title': 'Foo',
'slug': 'foo',
}
],
}
return httmock.response(200, content, headers)
with httmock.HTTMock(response_get):
resp = resp.follow()
assert resp.html.find('option', {'value': 'demarches:foo'}).text == 'Foo'
assert Mail.objects.get(id=mail.pk).subject == 'eggs'
def test_edit_note_view(app, user):
resp = app.get('/ajax/mail/edit-note/', status=302)
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), note='spam')
resp = app.get('/ajax/mail/edit-note/', params={'mail': mail.pk}, status=200)
assert resp.html.find('h2').text == 'Note'
assert resp.html.find('textarea', {'name': 'note'}).text == 'spam'
resp.form['note'] = 'eggs'
resp = resp.form.submit()
assert resp.content_type == 'text/html'
assert resp.text == '{"result": "ok"}'
assert Mail.objects.get(id=mail.pk).note == 'eggs'
def test_note_view(app, user):
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
resp = app.get('/ajax/mail/note/%s' % mail.pk, status=302)
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
resp = app.get('/ajax/mail/note/%s' % mail.pk, status=200)
assert resp.text == '+'
assert not Mail.objects.get(id=mail.pk).note # mail object is unchanged
def test_reject_view(settings, app, user):
settings.MAARCH_FEED = {
'URL': 'http://maarch.example.net',
'ENABLE': True,
'USERNAME': 'xxx',
'PASSWORD': 'yyy',
'STATUS_REFUSED': 'FOO',
}
resp = app.post('/ajax/mail/reject', status=302)
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), external_id='maarch-42')
@httmock.urlmatch(netloc='maarch.example.net', path='/rest/res/resource/status', method='PUT')
def response_ok(url, request):
assert json.loads(force_str(request.body)) == {'status': 'FOO', 'resId': ['42']}
headers = {'content-type': 'application/json'}
content = {'maarch_say': 'ok'}
return httmock.response(200, content, headers)
with httmock.HTTMock(response_ok):
resp = app.post('/ajax/mail/reject', params={'source_pk': mail.pk}, status=200)
assert Mail.objects.count() == 0
# errors
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'), external_id='maarch-42')
@httmock.urlmatch(netloc='maarch.example.net', path='/rest/res/resource/status', method='PUT')
def response_error1(url, request):
raise requests.RequestException
with httmock.HTTMock(response_error1):
resp = app.post('/ajax/mail/reject', params={'source_pk': mail.pk})
assert Mail.objects.get(id=mail.pk)
@httmock.urlmatch(netloc='maarch.example.net', path='/rest/res/resource/status', method='PUT')
def response_error2(url, request):
return httmock.response(500)
with httmock.HTTMock(response_error2):
resp = app.post('/ajax/mail/reject', params={'source_pk': mail.pk})
assert Mail.objects.get(id=mail.pk)
@httmock.urlmatch(netloc='maarch.example.net', path='/rest/res/resource/status', method='PUT')
def response_error3(url, request):
return httmock.response(200, 'not a json')
with httmock.HTTMock(response_error3):
resp = app.post('/ajax/mail/reject', params={'source_pk': mail.pk})
assert Mail.objects.get(id=mail.pk)
def test_mail_count_view(app, user):
resp = app.get('/ajax/count/mail/', status=302)
assert resp.location.startswith('/login/?next=')
Mail.objects.create(content=ContentFile('foo', name='bar.txt'), status='done-42')
Mail.objects.create(content=ContentFile('foo', name='bar.txt'), status='43')
app.set_user(user.username)
resp = app.get('/ajax/count/mail/', status=200)
assert resp.content_type == 'application/json'
assert resp.json == {'count': 1}

View File

@ -1,208 +0,0 @@
# welco - multichannel request processing
# Copyright (C) 2020 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from unittest import mock
import httmock
import pytest
from django.contrib.contenttypes.models import ContentType
from django.core.files.base import ContentFile
from welco.qualif.models import Association
from welco.sources.mail.models import Mail
def login(app, username='toto', password='toto'):
login_page = app.get('/login/')
login_form = login_page.forms[0]
login_form['username'] = username
login_form['password'] = password
resp = login_form.submit()
assert resp.status_int == 302
return app
@pytest.fixture
def logged_app(app, user):
return login(app)
def test_unlogged_access(app):
# connect while not being logged in
assert app.get('/', status=302).location.endswith('/login/?next=/')
def test_no_channel_access(logged_app):
logged_app.get('/', status=403)
def test_access(logged_app, mail_group):
resp = logged_app.get('/', status=302)
assert resp.location == 'mail/'
def test_logout(logged_app):
app = logged_app
app.get('/logout/')
assert app.get('/', status=302).location.endswith('/login/?next=/')
@mock.patch('welco.views.get_idps', return_value=[{'METADATA': '...'}])
@mock.patch('welco.views.resolve_url', return_value='foo-url')
def test_mellon_idp_redirections(mocked_resolv_url, mocked_get_idps, app):
resp = app.get('/login/', status=302)
assert resp.location == 'foo-url'
resp = app.get('/login/?next=http://foo/?bar', status=302)
assert resp.location == 'foo-url?next=http%3A//foo/%3Fbar'
resp = app.get('/logout/', status=302)
assert resp.location == 'foo-url'
def test_mail_view(app, user, mail_group):
resp = app.get('/mail/', status=302)
assert resp.location == '/login/?next=/mail/'
app.set_user(user.username)
resp = app.get('/mail/', status=200)
assert resp.html.find('h2').text == 'Mails'
def test_no_channel_access_on_mail_view(app, user):
app.set_user(user.username)
app.get('/mail/', status=403)
def test_phone_view(app, user, phone_group):
resp = app.get('/phone/', status=302)
assert resp.location == '/login/?next=/phone/'
app.set_user(user.username)
resp = app.get('/phone/', status=200)
assert resp.html.find('h2').text == 'Phone Call'
def test_counter_view(app, user, counter_group):
resp = app.get('/counter/', status=302)
assert resp.location == '/login/?next=/counter/'
app.set_user(user.username)
resp = app.get('/counter/', status=200)
assert resp.html.find('h2').text == 'Counter'
def test_kb_view(app, user, kb_group):
resp = app.get('/kb/', status=302)
assert resp.location == '/login/?next=/kb/'
app.set_user(user.username)
resp = app.get('/kb/', status=200)
assert resp.html.find('h2').text == 'Knowledge Base'
def test_wcs_summary_view(app, mail_group, user):
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
source_type = ContentType.objects.get_for_model(Mail).pk
resp = app.get('/ajax/summary/%s/%s/?callback=spam' % (source_type, mail.pk), status=302)
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
resp = app.get('/ajax/summary/%s/%s/?callback=spam' % (source_type, mail.pk), status=200)
assert resp.content_type == 'application/javascript'
assert 'bar' in resp.text
assert resp.text.startswith('spam({')
def test_remove_association_view(app, mail_group, user):
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
source_type = ContentType.objects.get_for_model(Mail).pk
association = Association.objects.create(
source_type=ContentType.objects.get(id=source_type), source_pk=mail.pk
)
assert Association.objects.filter(id=association.pk).count() == 1
resp = app.get('/ajax/remove-association/%s' % association.pk, status=302)
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
resp = app.get('/ajax/remove-association/%s' % association.pk, status=302)
assert resp.location == '/'
assert Association.objects.filter(id=association.pk).count() == 0
def test_create_formdata_view(settings, app, mail_group, user):
settings.KNOWN_SERVICES = {
'wcs': {
'demarches': {
'url': 'http://wcs.example.net/',
}
}
}
mail = Mail.objects.create(content=ContentFile('foo', name='bar.txt'))
source_type = ContentType.objects.get_for_model(Mail).pk
association = Association.objects.create(
source_type=ContentType.objects.get(id=source_type), source_pk=mail.pk
)
resp = app.get('/ajax/create-formdata/%s' % association.pk, status=302)
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
resp = app.get('/ajax/create-formdata/%s' % association.pk, status=200)
assert resp.content_type == 'application/json'
assert resp.json == {'err': 1}
resp = app.post('/ajax/create-formdata/%s' % association.pk, status=200)
assert resp.json == {'err': 1, 'msg': "'NoneType' object has no attribute 'split'"}
association.formdef_reference = 'demarches:bar'
association.save()
@httmock.urlmatch(netloc='wcs.example.net', path='/api/formdefs/bar/schema', method='GET')
def response_get(url, request):
headers = {'content-type': 'application/json'}
content = {}
return httmock.response(200, content, headers)
@httmock.urlmatch(netloc='wcs.example.net', path='/api/formdefs/bar/submit', method='POST')
def response_post(url, request):
headers = {'content-type': 'application/json'}
content = {
'err': 0,
'data': {
'id': 42,
'backoffice_url': 'http://example.net',
},
}
return httmock.response(200, content, headers)
with httmock.HTTMock(response_get, response_post):
resp = app.post('/ajax/create-formdata/%s' % association.pk, status=200)
assert resp.content_type == 'application/json'
assert resp.json == {
'result': 'ok',
'url': 'http://wcs.example.net/backoffice/management/bar/42/',
}
def test_menu_json_view(app, user, mail_group, phone_group, counter_group, kb_group):
resp = app.get('/menu.json', status=302)
assert resp.location.startswith('/login/?next=')
app.set_user(user.username)
resp = app.get('/menu.json', status=200)
assert resp.content_type == 'application/json'
assert sorted(x['label'] for x in resp.json) == ['Call Center', 'Counter', 'Knowledge Base', 'Mails']
resp = app.get('/menu.json?callback=foo', status=200)
assert resp.content_type == 'application/javascript'
assert resp.text.startswith('foo([{')

View File

@ -14,34 +14,36 @@
# 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 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,12 +17,13 @@
import json
import pytest
from django.contrib.auth.models import User
from django.utils.encoding import force_str
from httmock import HTTMock, urlmatch
from httmock import urlmatch, HTTMock
class BaseMock:
class BaseMock(object):
def __init__(self, netloc):
self.netloc = netloc
self.clear()
@ -49,38 +50,32 @@ class BaseMock:
class MaarchMock(BaseMock):
def list_endpoint(self, url, request):
self.requests.append(('list_endpoint', url, request, json.loads(force_str(request.body))))
self.requests.append(('list_endpoint', url, request, json.loads(request.body)))
return {
'content': json.dumps(self.next_response()),
'headers': {
'content-type': 'application/json',
},
'status_code': 200,
}
list_endpoint.path = '^/rest/res/list$'
def update_external_infos(self, url, request):
self.requests.append(('update_external_infos', url, request, json.loads(force_str(request.body))))
self.requests.append(('update_external_infos', url, request, json.loads(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_str(request.body))))
self.requests.append(('update_status', url, request, json.loads(request.body)))
return {
'content': json.dumps(self.next_response()),
'headers': {
'content-type': 'application/json',
},
'status_code': 200,
}
update_status.path = '^/rest/res/resource/status$'
def post_courrier(self, url, request):
self.requests.append(('post_courrier', url, request, json.loads(force_str(request.body))))
self.requests.append(('post_courrier', url, request, json.loads(request.body)))
post_courrier.path = '^/rest/res$'
@ -98,50 +93,37 @@ 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$'
@ -170,15 +152,13 @@ def test_utils(maarch):
assert welco_maarch_obj.grc_refused_status == 'GRCREFUSED'
PDF_MOCK = b'%PDF-1.4 ...'
PDF_MOCK = '%PDF-1.4 ...'
def test_feed(settings, app, maarch, wcs, user):
import base64
from django.contrib.contenttypes.models import ContentType
from django.core.management import call_command
from django.contrib.contenttypes.models import ContentType
from welco.sources.mail.models import Mail
app.set_user(user.username)
@ -189,16 +169,14 @@ 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_str(base64.b64encode(PDF_MOCK)),
}
],
}
)
maarch.responses.append({
'resources': [
{
'res_id': 1,
'fileBase64Content': base64.b64encode(PDF_MOCK),
}
],
})
# update status request
maarch.responses.append({})
# last list request
@ -233,26 +211,20 @@ 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
@ -268,7 +240,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
@ -278,31 +250,31 @@ def test_feed(settings, app, maarch, wcs, user):
user.set_password('test')
user.save()
# verify authentication error
response = app.post_json('/api/mail/response/', params={}, status=(401, 403))
response = app.post_json('/api/mail/response/', params={}, status=403)
app.authorization = ('Basic', ('test', 'test'))
# verify serializer error
response = app.post_json('/api/mail/response/', params={}, status=400)
assert response.json['err'] == 1
# verify error when maarch feed is not configured
settings.MAARCH_FEED['ENABLE'] = False
response = app.post_json(
'/api/mail/response/', params={'mail_id': 'maarch-1', 'content': 'coucou'}, status=200
)
response = app.post_json('/api/mail/response/',
params={'mail_id': 'maarch-1', 'content': 'coucou'},
status=200)
assert response.json['err'] == 1
assert response.json['err_desc'] == 'maarch is unconfigured'
settings.MAARCH_FEED['ENABLE'] = True
# verify error when mail_id is unknown
response = app.post_json(
'/api/mail/response/', params={'mail_id': 'maarch-231', 'content': 'coucou'}, status=404
)
response = app.post_json('/api/mail/response/',
params={'mail_id': 'maarch-231', 'content': 'coucou'},
status=404)
assert response.json['err'] == 1
# successfull call
maarch.responses.append({})
with maarch.ctx_manager:
response = app.post_json(
'/api/mail/response/', params={'mail_id': 'maarch-1', 'content': 'coucou'}, status=200
)
response = app.post_json('/api/mail/response/',
params={'mail_id': 'maarch-1', 'content': 'coucou'},
status=200)
assert maarch.requests[0][3] == {
'historyMessage': 'coucou',
'resId': [1],

View File

@ -18,9 +18,9 @@ import json
import re
import pytest
from django.core.urlresolvers import reverse
from django.test import override_settings
from django.urls import reverse
from django.utils.encoding import force_str
from django.utils.timezone import now, timedelta
from welco.sources.phone import models
@ -36,104 +36,95 @@ 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 json.loads(response.content) == {'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 json.loads(response.content) == {'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 json.loads(response.content) == {'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 json.loads(response.content) == {'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):
@ -143,14 +134,13 @@ 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}
assert json.loads(response.content) == {'err': 0}
# register user to some lines
# then remove from some
@ -162,12 +152,12 @@ def test_current_calls(user, client):
response = client.get(reverse('phone-current-calls'))
assert response.status_code == 200
assert response['content-type'] == 'application/json'
payload = response.json()
payload = json.loads(response.content)
assert isinstance(payload, dict)
assert set(payload.keys()) == {'err', 'data'}
assert set(payload.keys()) == set(['err', 'data'])
assert payload['err'] == 0
data = payload['data']
assert set(data.keys()) == {'calls', 'lines', 'all-lines'}
assert set(data.keys()) == set(['calls', 'lines', 'all-lines'])
assert isinstance(data['calls'], list)
assert isinstance(data['lines'], list)
assert isinstance(data['all-lines'], list)
@ -175,14 +165,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()) <= {'caller', 'callee', 'start', 'data'}
assert isinstance(call['caller'], str)
assert isinstance(call['callee'], str)
assert isinstance(call['start'], str)
assert set(call.keys()) <= set(['caller', 'callee', 'start', 'data'])
assert isinstance(call['caller'], unicode)
assert isinstance(call['callee'], unicode)
assert isinstance(call['start'], unicode)
if 'data' in call:
assert isinstance(call['data'], dict)
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
assert len([call for call in data['lines'] if isinstance(call, unicode)]) == 5
assert len([call for call in data['all-lines'] if isinstance(call, unicode)]) == 10
# unregister user to all remaining lines
for number in range(0, 5):
@ -190,11 +180,11 @@ def test_current_calls(user, client):
response = client.get(reverse('phone-current-calls'))
assert response.status_code == 200
assert response['content-type'] == 'application/json'
payload = response.json()
payload = json.loads(response.content)
assert isinstance(payload, dict)
assert set(payload.keys()) == {'err', 'data'}
assert set(payload.keys()) == set(['err', 'data'])
assert payload['err'] == 0
assert set(payload['data'].keys()) == {'calls', 'lines', 'all-lines'}
assert set(payload['data'].keys()) == set(['calls', 'lines', 'all-lines'])
assert len(payload['data']['calls']) == 0
assert len(payload['data']['lines']) == 0
assert len(payload['data']['all-lines']) == 10
@ -207,49 +197,50 @@ 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 json.loads(response.content) == {'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 json.loads(response.content) == {'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_str(response.content)
assert 'You do not have a phoneline configured' in 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_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 'You do not have a phoneline configured' not in response.content
assert '<li>102' in response.content
assert 'data-callee="102"' in response.content
currents = re.search('<div id="source-mainarea" '
'data-current-calls="/api/phone/current-calls/">'
'(.*?)</div>', response.content, flags=re.DOTALL)
assert 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_str(response.content)
assert '<h1>Current Call: <strong>003369999999</strong></h1>' in response.content
# simulate a mellon user
session = client.session
@ -257,26 +248,27 @@ def test_phone_zone(user, client):
session.save()
response = client.get(reverse('phone-zone'))
assert response.status_code == 200
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)
assert 'agent007' not in response.content
assert 'data-callee="agent007"' not in response.content
assert '<li>102' in response.content
assert 'data-callee="102"' in response.content
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_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)
assert '<h1>Current Call: <strong>003369999999</strong></h1>' in response.content
assert 'agent007' in response.content
assert 'data-callee="agent007"' in response.content
assert '<li>102' in response.content
assert 'data-callee="102"' in response.content
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
@ -285,27 +277,28 @@ def test_call_expiration(user, client):
client.login(username='toto', password='toto')
response = client.get(reverse('phone-current-calls'))
assert response.status_code == 200
payload = response.json()
payload = json.loads(response.content)
assert payload['err'] == 0
assert len(payload['data']['calls']) == 1
# start call 10 minutes ago
models.PhoneCall.objects.filter(stop__isnull=True).update(start=now() - timedelta(minutes=10))
models.PhoneCall.objects.filter(stop__isnull=True).update(
start=now()-timedelta(minutes=10))
# get list of calls without expiration
response = client.get(reverse('phone-current-calls'))
assert response.status_code == 200
payload = response.json()
payload = json.loads(response.content)
assert payload['err'] == 0
assert len(payload['data']['calls']) == 1 # still here
assert len(payload['data']['calls']) == 1 # still here
# get list of calls with an expiration of 2 minutes (< 10 minutes)
with override_settings(PHONE_MAX_CALL_DURATION=2):
response = client.get(reverse('phone-current-calls'))
assert response.status_code == 200
payload = response.json()
payload = json.loads(response.content)
assert payload['err'] == 0
assert len(payload['data']['calls']) == 0 # call is expired
assert len(payload['data']['calls']) == 0 # call is expired
assert models.PhoneCall.objects.filter(stop__isnull=True).count() == 0 # active calls
assert models.PhoneCall.objects.filter(stop__isnull=False).count() == 1 # stopped calls
assert models.PhoneCall.objects.filter(stop__isnull=False).count() == 1 # stopped calls

30
tox.ini
View File

@ -1,8 +1,6 @@
[tox]
envlist =
py3-django32-drf314
py3-django32-black-coverage-pylint-drf312
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/welco/{env:BRANCH_NAME:}
envlist = py27-django111
toxworkdir = {env:TMPDIR:/tmp}/tox-{env:USER}/welco/
[testenv]
usedevelop =
@ -10,29 +8,23 @@ 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 =
django32: django>=3.2,<3.3
django111: django>=1.11,<1.12
pytest-cov
pytest-django
pytest!=5.3.3
pytest<4.1
attrs<19.2
WebTest
mock<4
mock
httmock
python-dateutil
pylint<3
astroid<3
pylint-django
django-webtest
pylint<1.8
pylint-django<0.8.1
django-webtest>=1.9.6
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
django111: ./pylint.sh welco/
django111: py.test {posargs: --junitxml=junit-{envname}.xml --cov-report xml --cov-report html --cov=welco/ tests/}

View File

@ -15,8 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.apps import apps
from django.urls import include, re_path
from django.conf.urls import include, url
def register_urls(urlpatterns):
pre_urls = []
@ -25,9 +24,9 @@ def register_urls(urlpatterns):
if hasattr(app, 'get_before_urls'):
urls = app.get_before_urls()
if urls:
pre_urls.append(re_path('^', include(urls)))
pre_urls.append(url('^', include(urls)))
if hasattr(app, 'get_after_urls'):
urls = app.get_after_urls()
if urls:
post_urls.append(re_path('^', include(urls)))
post_urls.append(url('^', 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 gettext_lazy as _
from django.utils.translation import pgettext_lazy
from django.utils.translation import ugettext_lazy as _, pgettext_lazy
DEFAULT_TITLE_CHOICES = (
('', ''),
@ -24,15 +24,14 @@ 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,14 +27,13 @@ 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 FormView, TemplateView
from django.views.generic import TemplateView, FormView
from welco.utils import get_wcs_data, sign_url
from .forms import ContactAddForm
class HomeZone:
class HomeZone(object):
def __init__(self, request):
self.request = request
@ -47,28 +46,29 @@ class ContactsZone(TemplateView):
template_name = 'contacts/zone.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(ContactsZone, self).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 = {x.name for x in request.user.groups.all()}
user_groups = set([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,12 +83,8 @@ 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']])
@ -104,7 +100,7 @@ class ContactDetailFragmentView(TemplateView):
template_name = 'contacts/contact_detail_fragment.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(ContactDetailFragmentView, self).get_context_data(**kwargs)
user_id = self.kwargs.get('slug').split('-')[-1]
user_details = get_wcs_data('api/users/%s/' % user_id)
@ -113,13 +109,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()
@ -137,7 +133,7 @@ class ContactAdd(FormView):
msg['password'] = str(random.SystemRandom().random())
msg['send_registration_email'] = getattr(settings, 'CONTACT_SEND_REGISTRATION_EMAIL', True)
authentic_site = list(settings.KNOWN_SERVICES.get('authentic').values())[0]
authentic_site = settings.KNOWN_SERVICES.get('authentic').values()[0]
authentic_url = authentic_site.get('url')
authentic_orig = authentic_site.get('orig')
authentic_secret = authentic_site.get('secret')
@ -148,8 +144,9 @@ 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')
@ -168,5 +165,4 @@ 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 gettext_lazy as _
from django.utils.translation import ugettext_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().__init__(*args, **kwargs)
super(QualificationForm, self).__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,7 +16,6 @@
from django.apps import AppConfig
class KbAppConfig(AppConfig):
name = 'welco.kb'

View File

@ -19,7 +19,6 @@ from django.utils.text import slugify
from .models import Page
class PageForm(forms.ModelForm):
class Meta:
model = Page
@ -38,4 +37,4 @@ class PageForm(forms.ModelForm):
i += 1
slug = '%s-%s' % (base_slug, i)
self.instance.slug = slug
return super().save(commit=commit)
return super(PageForm, self).save(commit=commit)

View File

@ -1,19 +1,20 @@
# -*- 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,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,5 +1,8 @@
# -*- 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):
@ -13,13 +16,7 @@ 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,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -14,18 +14,22 @@
# 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.fields import RichTextField
from django.core.urlresolvers import reverse
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from ckeditor.fields import RichTextField
import reversion
from taggit.managers import TaggableManager
@reversion.register
class Page(models.Model):
title = models.CharField(_('Title'), max_length=200)
slug = models.SlugField(_('Slug'))
content = RichTextField(_('Text'))
tags = TaggableManager(_('Keywords'), blank=True, help_text=_('A comma-separated list of tags.'))
tags = TaggableManager(_('Keywords'), blank=True,
help_text=_('A comma-separated list of tags.'))
class Meta:
ordering = ['title']

View File

@ -14,14 +14,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import html
from HTMLParser import HTMLParser
from django.utils.html import strip_tags
from haystack import indexes
from .models import Page
class PageIndex(indexes.SearchIndex, indexes.Indexable):
title = indexes.CharField(model_attr='title', boost=3)
text = indexes.CharField(document=True)
@ -33,7 +33,7 @@ class PageIndex(indexes.SearchIndex, indexes.Indexable):
return Page
def prepare_text(self, obj):
return obj.title + ' ' + self.prepare_tags(obj) + ' ' + html.unescape(strip_tags(obj.content))
return obj.title + ' ' + self.prepare_tags(obj) + ' ' + HTMLParser().unescape(strip_tags(obj.content))
def prepare_text_auto(self, obj):
return self.prepare_text(obj)

View File

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

@ -0,0 +1,24 @@
{% 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

@ -0,0 +1,26 @@
{% 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,31 +20,32 @@ from django import template
from django.conf import settings
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse_lazy
from django.db.models import Count
from django.http import HttpResponse, HttpResponseRedirect
from django.template import RequestContext
from django.urls import reverse_lazy
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import CreateView, DeleteView, DetailView, ListView, TemplateView, UpdateView
from django.views.generic import (DetailView, CreateView, UpdateView,
ListView, DeleteView, TemplateView)
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 .forms import PageForm
from .models import Page
from .forms import PageForm
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 = {x.name for x in user.groups.all()}
allowed_roles.append(settings.KB_ROLE) # legacy
user_groups = set([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()
@ -55,15 +56,14 @@ class PageListView(ListView):
def dispatch(self, request, *args, **kwargs):
check_request_perms(request, access=True)
return super().dispatch(request, *args, **kwargs)
return super(PageListView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(PageListView, self).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,8 +73,7 @@ class PageAddView(CreateView):
def dispatch(self, request, *args, **kwargs):
check_request_perms(request)
return super().dispatch(request, *args, **kwargs)
return super(PageAddView, self).dispatch(request, *args, **kwargs)
page_add = login_required(PageAddView.as_view())
@ -85,8 +84,7 @@ class PageEditView(UpdateView):
def dispatch(self, request, *args, **kwargs):
check_request_perms(request)
return super().dispatch(request, *args, **kwargs)
return super(PageEditView, self).dispatch(request, *args, **kwargs)
page_edit = login_required(PageEditView.as_view())
@ -96,10 +94,10 @@ class PageDetailView(DetailView):
def dispatch(self, request, *args, **kwargs):
check_request_perms(request, access=True)
return super().dispatch(request, *args, **kwargs)
return super(PageDetailView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context = super(PageDetailView, self).get_context_data(**kwargs)
context['can_manage'] = check_user_perms(self.request.user)
return context
@ -111,7 +109,6 @@ class PageDetailFragmentView(DetailView):
model = Page
template_name = 'kb/page_detail_fragment.html'
page_detail_fragment = PageDetailFragmentView.as_view()
@ -121,8 +118,7 @@ class PageDeleteView(DeleteView):
def dispatch(self, request, *args, **kwargs):
check_request_perms(request)
return super().dispatch(request, *args, **kwargs)
return super(PageDeleteView, self).dispatch(request, *args, **kwargs)
page_delete = login_required(PageDeleteView.as_view())
@ -133,22 +129,58 @@ class PageSearchView(SearchView):
def dispatch(self, request, *args, **kwargs):
check_request_perms(request, access=True)
return super().dispatch(request, *args, **kwargs)
return super(PageSearchView, self).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().get_context_data(**kwargs)
context = super(KbZone, self).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]
@ -170,7 +202,6 @@ class KbZone(TemplateView):
tag.font_size = 'x-large'
return context
zone = csrf_exempt(KbZone.as_view())
@ -189,7 +220,7 @@ def page_search_json(request):
return response
class HomeZone:
class HomeZone(object):
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: 2020-04-14 09:29+0200\n"
"PO-Revision-Date: 2017-01-13 16:08+0100\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 dun autre usager"
msgstr "Retourner à la recherche d'un autre usager"
#: contrib/alfortville/models.py:40
#: contrib/alfortville/templates/alfortville/mail-table.html:44
@ -155,7 +155,7 @@ msgstr "Cliquer pour (dé)sélectionner tous les courriers"
#: contrib/alfortville/templates/alfortville/mail-table-waiting.html:39
#: contrib/alfortville/templates/alfortville/mail-table.html:68
msgid "There is currently no mail in this list."
msgstr "Il ny a pour le moment aucun courrier dans cette liste."
msgstr "Il n'y a pour le moment aucun courrier dans cette liste."
#: contrib/alfortville/templates/alfortville/mail-table-waiting.html:11
msgid "Pending Mails"
@ -379,7 +379,7 @@ msgstr "Avis requis"
#: sources/mail/templates/welco/mail_summary.html:46
#: sources/mail/templates/welco/mail_summary.html:58
msgid "Waiting for avis."
msgstr "Pas davis rédigé."
msgstr "Pas d'avis rédigé."
#: sources/mail/templates/welco/mail_summary.html:54
msgid "Avis"
@ -487,7 +487,7 @@ msgstr "Transmettre"
#: views.py:236
msgid "Call Center"
msgstr "Centre dappels"
msgstr "Centre d'appels"
#~ 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/>.
import ckeditor.widgets
from django.core.urlresolvers import reverse
from django.forms.utils import flatatt
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.encoding import force_str
from django.utils.encoding import force_text
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, renderer=None):
def ckeditor_render(self, name, value, attrs=None):
if value is None:
value = ''
final_attrs = {'name': name}
@ -40,22 +40,14 @@ def ckeditor_render(self, name, value, attrs=None, renderer=None):
self.config['language'] = get_language()
# Force to text to evaluate possible lazy objects
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),
},
)
)
external_plugin_resources = [[force_text(a), force_text(b), force_text(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_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,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
@ -11,37 +14,31 @@ 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(
@ -59,7 +56,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='association',
name='source_type',
field=models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE),
field=models.ForeignKey(to='contenttypes.ContentType'),
preserve_default=True,
),
]

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

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

View File

@ -14,17 +14,17 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey
from django.core.urlresolvers import reverse
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from welco.utils import get_wcs_formdef_details, get_wcs_services, push_wcs_formdata
from welco.utils import get_wcs_formdef_details, push_wcs_formdata, get_wcs_services
class Association(models.Model):
source_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
source_type = models.ForeignKey(ContentType)
source_pk = models.PositiveIntegerField()
source = GenericForeignKey('source_type', 'source_pk')
comments = models.TextField(blank=True, verbose_name=_('Comments'))
@ -38,12 +38,10 @@ 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,3 +1,5 @@
# -*- coding: utf-8 -*-
"""
Django settings for welco project.
@ -9,9 +11,8 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
"""
import os
from django.conf import global_settings
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
@ -40,6 +41,7 @@ INSTALLED_APPS = (
'django.contrib.staticfiles',
'ckeditor',
'haystack',
'reversion',
'taggit',
'welco.sources.counter',
'welco.sources.mail',
@ -57,8 +59,10 @@ MIDDLEWARE = (
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'reversion.middleware.RevisionMiddleware',
)
ROOT_URLCONF = 'welco.urls'
@ -89,7 +93,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/
@ -98,7 +102,9 @@ 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/'
@ -128,18 +134,12 @@ CKEDITOR_UPLOAD_PATH = 'uploads/'
CKEDITOR_CONFIGS = {
'default': {
'toolbar_Own': [
['Source', 'Format', '-', 'Bold', 'Italic'],
['NumberedList', 'BulletedList'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
['Link', 'Unlink'],
[
'Image',
],
[
'RemoveFormat',
],
],
'toolbar_Own': [['Source', 'Format', '-', 'Bold', 'Italic'],
['NumberedList', 'BulletedList'],
['JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
['Link', 'Unlink'],
['Image',],
['RemoveFormat',]],
'toolbar': 'Own',
},
}
@ -193,7 +193,7 @@ CHANNEL_ROLES = {
}
# role allowed to manage knowledge base
KB_ROLE = None # deprecated
KB_ROLE = None # deprecated
KB_MANAGE_ROLES = []
# roles allowed to visit knowledge base
@ -203,7 +203,9 @@ 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
@ -215,8 +217,7 @@ 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())
execfile(local_settings_file)

View File

@ -1,18 +1,19 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
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,19 +16,20 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_lazy as _
from welco.qualif.models import Association
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.urls import path
from django.conf.urls import url
from . import views
urlpatterns = [
path('ajax/counter/zone/', views.zone, name='counter-zone'),
url(r'^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:
class Home(object):
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().get_context_data(**kwargs)
context = super(CounterZone, self).get_context_data(**kwargs)
context['source_type'] = ContentType.objects.get_for_model(CounterPresence)
new_source = CounterPresence()
new_source.save()
@ -54,7 +54,6 @@ class CounterZone(TemplateView):
context['useful_links'] = settings.COUNTER_LINKS
return context
zone = csrf_exempt(CounterZone.as_view())

View File

@ -22,15 +22,14 @@ 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
from welco.qualif.models import Association
signals.post_save.connect(self.association_post_save, sender=Association)
signals.post_save.connect(self.association_post_save,
sender=Association)
def association_post_save(self, sender, instance, **kwargs):
from .utils import get_maarch
@ -48,8 +47,6 @@ 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,9 +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 _
from django.conf import settings
from django.utils.translation import gettext_lazy as _
class MailQualificationForm(forms.Form):
post_date = forms.DateTimeField(label=_('Post Date (*)'), required=False)

View File

@ -14,11 +14,12 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urlparse
import base64
import urllib.parse
from dateutil.parser import parse as parse_datetime
import requests
from dateutil.parser import parse as parse_datetime
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
@ -27,7 +28,7 @@ class MaarchError(Exception):
pass
class MaarchCourrier:
class MaarchCourrier(object):
url = None
username = None
password = None
@ -42,7 +43,7 @@ class MaarchCourrier:
def __repr__(self):
return '<MaarchCourrier url:%s>' % self.url
class Courrier:
class Courrier(object):
content = None
format = None
status = None
@ -86,8 +87,8 @@ class MaarchCourrier:
excluded_keys = ['content', 'format', 'status', 'maarch_courrier', 'pk']
data = {key: self.__dict__[key] for key in self.__dict__ if key not in excluded_keys}
if data:
for key, value in data.items():
if isinstance(value, str):
for key, value in data.iteritems():
if isinstance(value, basestring):
d.append({'column': key, 'value': value, 'type': 'string'})
elif isinstance(value, int):
d.append({'column': key, 'value': str(value), 'type': 'int'})
@ -123,7 +124,7 @@ class MaarchCourrier:
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)
@ -151,19 +152,19 @@ class MaarchCourrier:
@property
def list_url(self):
return urllib.parse.urljoin(self.url, 'rest/res/list')
return urlparse.urljoin(self.url, 'rest/res/list')
@property
def update_external_infos_url(self):
return urllib.parse.urljoin(self.url, 'rest/res/externalInfos')
return urlparse.urljoin(self.url, 'rest/res/externalInfos')
@property
def update_status_url(self):
return urllib.parse.urljoin(self.url, 'rest/res/resource/status')
return urlparse.urljoin(self.url, 'rest/res/resource/status')
@property
def post_courrier_url(self):
return urllib.parse.urljoin(self.url, 'rest/res')
return urlparse.urljoin(self.url, 'rest/res')
def get_courriers(self, clause, fields=None, limit=None, include_file=False, order_by=None):
if fields:
@ -173,16 +174,13 @@ class MaarchCourrier:
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/>.
import os
from optparse import make_option
import os
from django.core.files.base import ContentFile
from django.core.files import File
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=ContentFile(open(filepath).read(), name=os.path.basename(filepath)))
mail = Mail(content=File(open(filepath)))
mail.scanner_category = kwargs.get('category')
mail.save()
count += 1

View File

@ -14,26 +14,25 @@
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from optparse import make_option
import os
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,18 +1,19 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
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,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

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

View File

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

View File

@ -1,3 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
@ -15,6 +18,7 @@ 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,3 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models

View File

@ -15,30 +15,32 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import requests
import subprocess
import requests
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import ugettext_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)
@ -50,7 +52,8 @@ 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)
@ -58,7 +61,6 @@ class Mail(models.Model):
@classmethod
def get_qualification_form_class(cls):
from .forms import MailQualificationForm
return MailQualificationForm
def get_qualification_form(self):
@ -109,13 +111,5 @@ 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,17 +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 path, re_path
from django.conf.urls import url
from .views import edit_note, feeder, mail_count, mail_response, note, qualification_save, reject, viewer
from .views import (viewer, feeder, qualification_save, edit_note, note,
reject, mail_count, mail_response)
urlpatterns = [
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'),
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'),
]

View File

@ -20,19 +20,10 @@ 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().__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(WelcoMaarchCourrier, self).__init__(url, username, password)
self.grc_status = grc_status
self.grc_received_status = grc_received_status
self.grc_send_status = grc_send_status
@ -45,11 +36,10 @@ 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)
@ -84,5 +74,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,33 +18,35 @@ 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.db.transaction import atomic
from django.http import HttpResponse, HttpResponseRedirect
from django.contrib import messages
from django.core.urlresolvers import reverse
from django.template import RequestContext
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView
from rest_framework import authentication, permissions, serializers, status
from django.db.transaction import atomic
from rest_framework import authentication, serializers, permissions, status
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
from welco.utils import response_for_json
from .forms import MailQualificationForm
from .models import Mail
from .utils import MaarchError, get_maarch
from .forms import MailQualificationForm
from .utils import get_maarch, MaarchError
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)
@ -55,14 +57,13 @@ 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:
class Home(object):
source_key = 'mail'
display_filter = True
allow_reject = True
@ -100,17 +101,16 @@ 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().get_context_data(**kwargs)
context = super(EditNote, self).get_context_data(**kwargs)
context['mail'] = Mail.objects.get(id=self.request.GET['mail'])
return context
@ -120,7 +120,6 @@ class EditNote(TemplateView):
mail.save()
return HttpResponse(json.dumps({'result': 'ok'}))
edit_note = login_required(csrf_exempt(EditNote.as_view()))
@ -193,5 +192,4 @@ class MailResponseAPIView(GenericAPIView):
return Response({'err': 1, 'err_desc': str(e)})
return Response({'err': 0})
mail_response = MailResponseAPIView.as_view()

View File

@ -1,18 +1,19 @@
# -*- 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,8 +1,11 @@
import datetime
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
from django.utils.timezone import now, utc
from django.utils.timezone import utc
from django.utils.timezone import now
import datetime
from django.conf import settings
class Migration(migrations.Migration):
@ -16,10 +19,7 @@ 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,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -1,4 +1,7 @@
from django.db import migrations, models
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):

View File

@ -18,15 +18,15 @@ import logging
from django.conf import settings
from django.contrib.contenttypes.fields import GenericRelation
from django.core.urlresolvers import reverse
from django.db import models
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
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 Meta:
verbose_name = _('Phone Call')
@ -39,7 +39,8 @@ 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)
@ -60,13 +61,14 @@ 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):
@ -78,15 +80,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.urls import path, re_path
from django.conf.urls import url
from . import views
urlpatterns = [
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'),
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'),
]

View File

@ -19,19 +19,18 @@ import logging
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, 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.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest, HttpResponse
from django.utils.timezone import now
from django.views.generic import TemplateView
from .models import PhoneCall, PhoneLine
class Home:
class Home(object):
source_key = 'phone'
def __init__(self, request, **kwargs):
@ -56,63 +55,59 @@ 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().get_context_data(**kwargs)
context = super(PhoneZone, self).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())
@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_str(request.body))
payload = json.loads(request.body)
assert isinstance(payload, dict), 'payload is not a JSON object'
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 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 payload['event'] in ('start', 'stop'), 'event must be "start" or "stop"'
assert isinstance(payload['caller'], str), 'caller must be a string'
assert isinstance(payload['callee'], str), 'callee must be a string'
assert isinstance(payload['caller'], unicode), 'caller must be a string'
assert isinstance(payload['callee'], unicode), '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_str(e)}), content_type='application/json'
)
except (TypeError, ValueError, AssertionError), e:
return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
unicode(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 = {
@ -132,39 +127,40 @@ 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)
@ -179,13 +175,11 @@ 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')
@ -196,40 +190,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_str(request.body))
payload = json.loads(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_str(e)}), content_type='application/json'
)
assert payload.keys() == ['callee'], 'payload must have only one key: callee'
except (TypeError, ValueError, AssertionError), e:
return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
unicode(e)}),
content_type='application/json')
PhoneLine.take(payload['callee'], request.user)
logger.info('user %s took line %s', request.user, payload['callee'])
logger.info(u'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_str(request.body))
payload = json.loads(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_str(e)}), content_type='application/json'
)
assert payload.keys() == ['callee'], 'payload must have only one key: callee'
except (TypeError, ValueError, AssertionError), e:
return HttpResponseBadRequest(json.dumps({'err': 1, 'msg':
unicode(e)}),
content_type='application/json')
PhoneLine.release(payload['callee'], request.user)
logger.info('user %s released line %s', request.user, payload['callee'])
logger.info(u'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.welco-home[data-environment-label] div#main-content {
body[data-environment-label] div#main-content {
height: calc(100vh - 71px); /* #top 60px + #top border 1px + #header bottom-border 10px */
}

View File

@ -1,10 +0,0 @@
{% extends "welco/base.html" %}
{% load i18n %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form }}
<button>{% trans 'Log in' %}</button>
</form>
{% endblock %}

View File

@ -14,69 +14,68 @@
# 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
import welco.contacts.views
import welco.kb.views
import welco.views
from ckeditor import views as ckeditor_views
from . import apps
from .kb.views import kb_manager_required
import welco.views
import welco.contacts.views
import welco.kb.views
urlpatterns = [
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'
),
url(r'^$', welco.views.home, name='home'),
url(r'^mail/$', welco.views.home_mail, name='home-mail'),
url(r'^phone/$', welco.views.home_phone, name='home-phone'),
url(r'^counter/$', welco.views.home_counter, name='home-counter'),
url(r'^', include('welco.sources.phone.urls')),
url(r'^', include('welco.sources.counter.urls')),
url(r'^ajax/qualification$', welco.views.qualification, name='qualif-zone'),
url(r'^ajax/remove-association/(?P<pk>\w+)$',
welco.views.remove_association, name='ajax-remove-association'),
url(r'^ajax/create-formdata/(?P<pk>\w+)$',
welco.views.create_formdata, name='ajax-create-formdata'),
url(r'^ajax/kb$', welco.kb.views.zone, name='kb-zone'),
url(r'^kb/$', welco.kb.views.page_list, name='kb-home'),
url(r'^kb/add/$', welco.kb.views.page_add, name='kb-page-add'),
url(r'^kb/search/$', welco.kb.views.page_search, name='kb-page-search'),
url(r'^kb/search/json/$', welco.kb.views.page_search_json, name='kb-page-search-json'),
url(r'^kb/(?P<slug>[\w-]+)/$', welco.kb.views.page_detail, name='kb-page-view'),
url(r'^ajax/kb/(?P<slug>[\w-]+)/$', welco.kb.views.page_detail_fragment, name='kb-page-fragment'),
url(r'^kb/(?P<slug>[\w-]+)/edit$', welco.kb.views.page_edit, name='kb-page-edit'),
url(r'^kb/(?P<slug>[\w-]+)/delete$', welco.kb.views.page_delete, name='kb-page-delete'),
url(r'^kb/(?P<slug>[\w-]+)/history$', welco.kb.views.page_history, name='kb-page-history'),
url(r'^kb/(?P<slug>[\w-]+)/version/(?P<version>\w+)/$', welco.kb.views.page_version, name='kb-page-version'),
url(r'^ajax/contacts$', welco.contacts.views.zone, name='contacts-zone'),
url(r'^contacts/search/json/$', welco.contacts.views.search_json, name='contacts-search-json'),
url(r'^ajax/contacts/(?P<slug>[\w-]+)/$',
welco.contacts.views.contact_detail_fragment, name='contact-page-fragment'),
url(r'^contacts/add/$', welco.contacts.views.contact_add, name='contacts-add'),
url(r'^ajax/summary/(?P<source_type>\w+)/(?P<source_pk>\w+)/$',
welco.views.wcs_summary, name='wcs-summary'),
url(r'^admin/', include(admin.site.urls)),
url(r'^logout/$', welco.views.logout, name='auth_logout'),
url(r'^login/$', welco.views.login, name='auth_login'),
url(r'^menu.json$', welco.views.menu_json, name='menu_json'),
url(r'^ckeditor/upload/', kb_manager_required(ckeditor_views.upload), name='ckeditor_upload'),
url(r'^ckeditor/browse/', never_cache(kb_manager_required(ckeditor_views.browse)), name='ckeditor_browse'),
]
if 'mellon' in settings.INSTALLED_APPS:
urlpatterns.append(re_path(r'^accounts/mellon/', include('mellon.urls')))
urlpatterns.append(url(r'^accounts/mellon/', include('mellon.urls')))
# static and media files
urlpatterns += staticfiles_urlpatterns()

Some files were not shown because too many files have changed in this diff Show More